- (De)Allocating Instances and Wrapping Existing Instances
- Accessing Struct Members
- sqlite3-specific API Extensions
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_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:
- Their WASM pointers.
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
API to proxy access to/from the C-level memory via the WASM
The following non-opaque sqlite3 structs are mapped into JS:
- Virtual Table-related:
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
(De)Allocating Instances and Wrapping Existing Instances
Each of the JS-wrapped structs has two distinct uses in this library:
- Passing a WASM pointer to the constructor creates a JS-level wrapper for an existing instance of the struct (whether it comes from C or JS) without taking over ownership of that memory. This permits JS to manipulate instances created in C without taking over their memory2.
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
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
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.
If a given JS-side struct instance has a property named
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:
- If it's a function, it is called with no arguments and the
being-disposed object as its
this. It may perform arbitrary cleanup.
- If it's an array, each entry of the array may be any of:
- A function is called as described above.
- Any JS-bound struct instance has its
- A number is assumed to be a WASM pointer, which gets
- Any other value type is ignored. It is sometimes convenient to
annotate the array with string entries to assist in
understanding the code. For example:
x.ondispose = ["Wrapper for this.$next:", y]
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
into a newly-created
ondispose array. It returns its
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:
structInstance.setMemberCString(memberName, jsString)overwrites (without freeing) any existing value in that member, replaces it with a newly-allocated C-string, and stores that C-string in the instance's
ondisposestate for cleanup when
dispose()is called. The struct cannot know whether it is safe to free such strings when overwriting them, so instead adds each string set this way to the
structInstance.memberToJsString(memberName)fetches the member's value. If it's NULL,
nullis returned, else it is assumed to be a valid C-style string and a copy of it is returned as a JS string.
structInstance.memberIsString(memberName)returns true if the given member name is specifically tagged as a string.
The following constraints apply to these methods:
memberNameargument must be the name of a JS/C-bound struct member. It may optionally include the
- Any unknown struct member name will trigger an exception.
- A member which is not explicitly tagged as a string in the low-level
struct description will trigger an exception in
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
function installMethod(name, func, applyArgcCheck = false)
structTypeInstance installMethod(methodsObject, applyArgcCheck = false)
The second form behaves exactly like
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
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
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...
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.
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
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
On success, returns this object. Throws on error.
In C those structs are defined inline in
sqlite3_index_info, so having them as member properties of that JS class seems appropriate.
Noting that, if needed, the
ondisposemechanism could be used to effectively transfer the memory to JS, but in practice this has never been necessary.
Noting that client code is free to add JS-only properties with a
$prefix, but may cause future code maintenance confusion when doing so.