C Structs in JavaScript

SQLite3 uses a number of C structs in its interface, most of which are "opaque." That is, their contents are invisible to client code and they may change in any give version of the library without violating compatibility constraints. Some structs, however, are not opaque and are used by the sqlite3 API to implement two-way communication with client code. Examples include sqlite3_module and sqlite3_vfs.

Opaque types, like sqlite3 and sqlite3_stmt, are only ever represented in this API by their WASM pointer values (integers). Non-opaque types may have two distinct representations in this API:

How the latter is done in this library is covered in detail in the documentation for Jaccwabyt, a spin-off sub-project of this one which was created in order to support this library but which does not rely on this project (it can be used by arbitrary WASM/JS clients). This document provides a high-level overview of that support and describes how such bindings are used in this library. It does not cover the whole Jaccwabyt API, just the parts which may be relevant for sqlite3 clients.

In short, this feature works by creating, in C, JSON-format descriptions of C structs, importing them into JS, then generating mappings which use JS property interceptors and JS's DataView API to proxy access to/from the C-level memory via the WASM heap.

The following non-opaque sqlite3 structs are mapped into JS:

All of those are available in JS as constructor functions named sqlite3.capi.TheStructName except for the inner classes of sqlite3_index_info, all of which are properties of sqlite3_index_info1.

(De)Allocating Instances and Wrapping Existing Instances

Each of the JS-wrapped structs has two distinct uses in this library:

Both uses are fairly common, and they differ only in how they manage (or not) the struct's memory.

So long as a struct instance is active, its pointer property resolves to its WASM heap memory address. That value can be passed to any C routines which take pointers of that type. For example:

const m = new MyStruct();
functionTakingMyStructPointers( m.pointer );

When client code is finished with an instance, and no C-level code is using its memory, the struct instance must be cleaned up by calling theStruct.dispose(). Calling dispose() multiple times is harmless - calls after the first are no-ops. Calling dipose() is not strictly required for wrapped instances, as their WASM heap memory is owned elsewhere, but it is good practice to call it because each instance may own memory other than the struct memory, as describes in the next section.

Custom cleanup: ondispose

If a given JS-side struct instance has a property named ondispose, that property is used when dispose() is called in order to free up any additional resources which may be associated with the struct (e.g. C-allocated strings or other struct instances).

ondispose is not set by default but may be set by the client to one of the following:

Any exceptions thrown by ondispose callbacks are ignored but may induce a warning in the console.

Client code may call aStructInstance.addOnDispose() to push one or more arguments onto the disposal list. That function will create an ondispose array if needed, or move a non-array ondispose value into a newly-created ondispose array. It returns its this.

Accessing Struct Members

C struct members are accessed from JS using conventional JS property access operators.

The one glaring difference between the C structs and their JS counterparts is that C-level struct members all have a $ name prefix in JS. Thus myVfs->xOpen in C is myVfs.$xOpen in JS. This prefix exists to make it easy for authors and readers of JS code to distinguish C-level members from JS-level members, as well as to avoid any naming collisions between C- and conventional JS-level members3.

When a struct-level member is accessed from JS, property interceptors will either fetch or assign the underlying C memory and perform the appropriate type and endianness conversion (throwing if assigning a value it cannot sensibly convert).

Accessing C-string Values

Members which are specifically tagged as being C-style strings have a couple of options which other members don't:

The following constraints apply to these methods:

sqlite3-specific API Extensions

The APIs and features listed above are all part of the Jaccwabyt framework. This section covers features added to the struct framework specifically for the sqlite3 API.

Installing JS Functions as Method Pointers

All StructType instances inherit the following methods to assist in installing C-side member function pointers which refer to JavaScript functions.

installMethod()

Uses:

  1. function installMethod(name, func, applyArgcCheck = false)
  2. structTypeInstance installMethod(methodsObject, applyArgcCheck = false)

The second form behaves exactly like installMethods().

Installs a StructBinder-bound function pointer member of the given name and function in this object.

It creates a WASM proxy for the given function and arranges for that proxy to be cleaned up when this.dispose() is called. Throws on the slightest hint of error, e.g., the given name does not map to a struct-bound member.

As a special case, if the given function is a pointer, then wasm.functionEntry() is used to validate that it is a known function. If so, it is used as-is with no extra level of proxying or cleanup, else an exception is thrown. It is legal to pass a value of 0, indicating a NULL pointer, with the caveat that 0 is a legal function pointer in WASM but it will not be accepted as such here. (Justification: the function at address zero must be one which initially came from the WASM module, not a method we want to bind to client-level extension code.)

This function returns a proxy for itself which is bound to this and takes 2 args (name,func). That function returns the same thing as this one, permitting calls to be chained.

If called with only 1 arg, it has no side effects but returns a func with the same signature as described above.

ACHTUNG:⚠ because we cannot generically know how to transform JS exceptions into result codes, the installed functions do no automatic catching of exceptions. It is critical, to avoid undefined behavior in the C layer, that methods mapped via this function do not throw. The exception, as it were, to that rule is...

If applyArgcCheck is true then each JS function (as opposed to function pointers) gets wrapped in a proxy which asserts that it is passed the expected number of arguments, throwing if the argument count does not match expectations. That is only intended for dev-time usage for sanity checking, as exceptions passing through such methods will leave the C environment in an undefined state.

installMethods()

structBinderInstance installMethods(methods, applyArgcCheck = false)

Installs methods into this StructType-type instance. Each entry in the given methods object must map to a known member of the given StructType, else an exception will be triggered. See installMethod() for more details, including the semantics of the second argument.

As an exception to the above, if any two or more methods in the methods object are the exact same function, installMethod() is not called for the 2nd and subsequent instances, and instead those instances get assigned the same method pointer which is created for the first instance. This optimization is primarily to accommodate special handling of sqlite3_module::xConnect and xCreate methods.

On success, returns this object. Throws on error.


  1. ^ In C those structs are defined inline in sqlite3_index_info, so having them as member properties of that JS class seems appropriate.
  2. ^ Noting that, if needed, the ondispose mechanism could be used to effectively transfer the memory to JS, but in practice this has never been necessary.
  3. ^ Noting that client code is free to add JS-only properties with a $ prefix, but may cause future code maintenance confusion when doing so.