- Introduction
- (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
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:
- Their WASM pointers.
- A JavaScript-level wrapper which reflects the structure of the C-level counterpart and permits inspection and manipulation of the contents of individual struct instances.
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:
- VFS-related:
- Virtual Table-related:
sqlite3_module
sqlite3_vtab_cursor
sqlite3_vtab
sqlite3_index_info
and its inner types:sqlite3_index_constraint
sqlite3_index_orderby
sqlite3_index_constraint_usage
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_info
1.
(De)Allocating Instances and Wrapping Existing Instances
Each of the JS-wrapped structs has two distinct uses in this library:
- Calling a constructor with no arguments creates a new instance in the WASM heap in order to connect it to C. In this case, client JavaScript code owns the memory for the instance unless some API explicitly takes it over.
- 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 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:
- 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
dispose()
method called. - A number is assumed to be a WASM pointer, which gets
freed using
sqlite3.wasm.dealloc()
. - 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 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:
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'sondispose
state for cleanup whendispose()
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 theondispose
list.structInstance.memberToJsString(memberName)
fetches the member's value. If it's NULL,null
is 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:
- The
memberName
argument must be the name of a JS/C-bound struct member. It may optionally include the$
prefix character. - 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
setMemberCString()
andmemberToJsString()
but notmemberIsString()
.
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:
function installMethod(name, func, applyArgcCheck = false)
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.
- ^
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
ondispose
mechanism 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.