Documentation menu

Callbacks

On this page
Guides0.1.0-alpha.35

A callback fact passes a JavaScript function where the library takes a function pointer. data names the void * parameter the library hands back to the callback; it disappears from the JavaScript signature:

native/sqlite.ffi.ts
functions: {
	sqlite3_create_collation_v2: {
		params: {
			xCompare: {
				callback: {
					data: "pArg",
					release: "xDestroy",
					params: { 3: { bytes: { length: "2" } }, 5: { bytes: { length: "4" } } },
				},
			},
		},
	},
},

Without a retention fact, the library may call the function only until the C function returns, as iteration and comparison functions do. An exception in the callback skips its remaining calls and rethrows when the C function returns. Such a callback binds without data too, as qsort's comparator does: callback: {}.

Kept callbacks

release names the void (*)(void *) parameter that takes the destructor for data, for a library that keeps the callback, such as SQLite's xDestroy. The library may call the function until it calls the destructor, and an exception rethrows from the library call that triggered it.

Replaced callbacks

replaced: true describes a setter that keeps its callback until its next call replaces it, such as GLFW's glfwSetKeyCallback or SQLite's sqlite3_update_hook. data is then optional:

native/glfw.ffi.ts
functions: {
	glfwSetKeyCallback: { params: { callback: { callback: { replaced: true }, nullable: true } } },
},

When the setter's first parameter is a handle, each handle keeps its own callback until its resource is destroyed. Without data, the callback must receive that handle, as GLFW's window callbacks do. With nullable, null removes the callback. A result that is the replaced callback, or with data its user data, is dropped.

A key has one pending setter at a time, including synchronous setters that call back before returning. Another setter for that key throws; setters for other keys can run. Await an async setter before replacing its registration again.

Registration owners

Use owner when destroying a resource ends a registration on one of its borrowed children, without an independent unregister operation. It roots a callback the library keeps through release or replaced:

typescript
subscribe: {
	params: { callback: { callback: { owner: "child", data: "data", release: "release" } } },
},

The selector names an input handle by C parameter name or 1-based position. Its actual destruction root must be a non-null, independently owned resource with a destructor. C must stop the registration and all its consumers before destruction returns. Other borrowed inputs must share that root.

A copied struct can be the root when the registration takes a pointer to its stable copy and its handle declares collect: true with an unconditional destructor. Native leases retain that storage after VM teardown until the last consumer releases it. Initialized caller storage can also be the root when it declares an unconditional collecting destructor. Passing the owner by value is unsupported.

The root keeps the closure and borrowed inputs alive. A closure that captures the root can still be collected with it. Active callback execution prevents closing the root; conditional destruction failure preserves the registration. Queued calls skip JavaScript after the root closes. Independent result leases still prevent destruction, and this registration cannot return a lease on its own root or descendants.

Close completions

closes names the input handle released by a one-shot completion, such as libuv's uv_close. Both the registration function and completion return void:

native/uv.ffi.ts
uv_close: { params: { close_cb: { callback: { closes: "handle" } } } },

The callback and handle are nonnullable. The callback receives the closing handle or routes through data; other arguments must be safe to copy. Foreign completions queue for the program's thread, so the program needs an event loop. Further C calls and disposal reject the handle while close is pending. Completion receives the same wrapper, already closed, and its retained buffers can then be transferred. Caller storage and backing bytes remain alive through the native completion even after VM teardown; detached completions release native ownership without running JavaScript. The library must invoke completion exactly once and keep the resource valid until then. release, replaced, closes, and once are alternative lifetimes; a callback states at most one.

One-shot callbacks

once: true describes a callback the library calls exactly once, during or after the function, as WebGPU's request functions do. It needs data or lookup. The registration stays alive until that call returns, then the library may forget the pointer; an exception rethrows from the library call that triggered it. With queued, a call from another thread queues for the program's thread and the event loop waits for it. A registration the library never calls stays alive until the program exits.

Callbacks in structs

Some libraries take the callback inside a struct passed by value, such as WebGPU's WGPURequestAdapterCallbackInfo. Name its function pointer member with field and its void * member with data; the parameter then takes a JavaScript function, and the binding builds the struct around the callback. Other members start zeroed unless constants fixes them:

typescript
functions: {
	request_device: {
		params: {
			info: {
				callback: {
					field: "callback",
					data: "userdata",
					once: true,
					constants: { mode: "REQUEST_DEFERRED" },
				},
			},
		},
	},
},

A callback record keeps its callback for the call or, with once, for its one call. One-shot and record callbacks register through synchronous calls only.

User data from a lookup

lookup names the function that returns data for a callback without a void * parameter, and the 1-based position of the callback parameter it takes. SQLite's user functions read it with sqlite3_user_data from their sqlite3_context *:

native/sqlite.ffi.ts
sqlite3_create_function_v2: {
	params: {
		xFunc: {
			callback: {
				data: "pApp",
				release: "xDestroy",
				lookup: { function: "sqlite3_user_data", param: "1" },
				params: { 3: { array: { length: "2" } } },
			},
		},
		xStep: { constant: "0" },
		xFinal: { constant: "0" },
	},
},

Callback arguments

Handles a callback receives are lent for that call: once it returns they read as closed, unless the program already held a handle for the same resource. A callback's own params, keyed by 1-based position, take string, bytes, array, and nullable facts. A length names another callback parameter, which the function does not receive, so the collation above receives two Uint8Array copies.

Calls from other threads

JavaScript callbacks run on the program's thread. A callback registered through a synchronous C call aborts if invoked from another thread unless it declares queued. Async exports route callbacks to the program's thread while the C caller waits; see Run blocking calls on a worker. A destructor may run on any thread; release completes on the program's thread.

queued: true marks a kept callback the library may call from its own threads, as audio and network libraries do. A call from another thread copies its arguments and queues for the program's thread, and the C caller continues at once; a call on the program's thread runs immediately. So the callback returns nothing, and takes values, strings, buffers, structs, and counted arrays of values or terminated strings. Array copies preserve fixed rows, native alignment, and null string entries. Resource-bearing elements need a waiting callback. An exception it throws is uncaught, as one in an event handler. The program needs an event loop, such as the web platform surface, and it keeps running while the callback is kept.

Use queued: "wait" when the C caller needs a result. The foreign thread waits until JavaScript returns and conversion succeeds. An exception returns zero to C and remains uncaught on the program's thread. Shutdown cancels waiting calls and rejects new ones with a zero result. Waiting callbacks borrow pointed-to arguments while C waits. A handle they receive is lent for that callback, unless the program already owns the same resource.

Callback results

For a callback returning text or bytes, describe callback.returns:

ts
callback: {
	data: "data",
	returns: { string: { length: "1" }, lifetime: "call" },
},

The length names a writable integer pointer by its callback parameter position. It is hidden from JavaScript and receives the returned UTF-8 byte count. Use string: true for NUL-terminated text, or bytes: { length: "1" } for a buffer result. nullable permits null; empty values keep a non-null pointer with a zero count.

Each result gets an independent native copy. All copies survive subsequent and nested callbacks, garbage collection, and source-buffer transfer until the enclosing binding finishes converting its outputs. C must stop using them before that binding returns. Async twins retain the same lifetime through worker execution and completion conversion. An exception returns a null pointer and zero count to C.

A retained callback uses lifetime: "registration" instead. Its copies survive until the library invokes release, the next setter replaces the callback, or its owning resource closes. C must stop using all returned pointers at that boundary, including ones held by pending consumers. All earlier results remain valid when the same callback returns again. A callback that releases or replaces itself cannot then publish another non-null result; conversion throws a TypeError and C receives null and a zero count. Native copies can outlive the VM when C still owns the registration; callbacks after VM shutdown return null without running JavaScript. One-shot closes completions cannot return pointers because their registration ends as the completion runs.

A callback can return a pointer handle with an explicit result lifetime. C borrows the resource; ownership stays with the canonical handle. The handle must belong to the program rather than being a callback loan. Disposal and destruction throw while C holds a result lease. Managed state also rejects field access and other C operations until the call finishes converting its outputs or the registration ends. Repeated results and compatible views share the same owner. Collection keeps the wrapper alive while the VM runs; native leases preserve storage and retained backing stores during teardown and follow the handle's existing collect policy when they retire. collect: false still requires explicit resource cleanup.

For lifetime: "registration", unregister the callback before closing resources it returned. Registration cleanup must be independent of those resources. A keyed callback cannot return its own registration owner because the resulting lease would prevent cleanup. The same constraint applies to indirect ownership cycles and release-backed registrations whose owners are opaque to the binding.

Stop or join callback producers through an async twin so the program's thread remains available to run their callbacks. A handle whose destructor joins such a producer also needs collect: false and asynchronous destruction.

Call returned functions

For a returned C function pointer, assert that its code and referenced state remain valid for the linked library:

typescript
functions: {
	choose_transform: {
		returns: { callable: { lifetime: "library" } },
	},
},

The result is a JavaScript function, or null for a null C pointer. Its arguments and result follow the header signature and the same conversions as ordinary bindings. The C compiler preserves the pointer's calling convention. call, apply, and bind work; construction throws.

Use callable.params, keyed by 1-based position, and callable.returns to describe pointers in that signature. For example, params: { "1": { bytes: { length: "2" } } } accepts a buffer and supplies its byte count. Nested function-pointer results need their own callable fact. The lifetime assertion includes state the function uses; do not use it for a function that depends on a resource the application can close.

For a function table, declare callable fields on its complete handle type:

typescript
types: {
	PluginApi: {
		handle: { destroy: "plugin_close" },
		fields: { transform: { callable: { lifetime: "owner" } } },
	},
},

Reading api.transform captures the current pointer and the handle. Saved functions keep the handle reachable; closing it makes later calls throw before argument conversion. Calls pin the owner through callbacks and result conversion, so reentrant disposal throws. If C changes the field, a new read sees the new pointer; an earlier function retains its snapshot. Explicit C self or user-data parameters remain explicit. Use "library" for a field whose target remains valid independently of the handle. Callable fields work through handle views, including aliases frozen before C supplies a more specific view. Each getter checks that C has supplied evidence for its declaring type before reading the field. Declare each callable property name on one type in the view family; aliases share one object and cannot select different same-named getters.

Nested results can use "owner" to inherit the same captured handle, or "library" to remain valid independently. A library-lifetime function cannot introduce an owner for its result.

Maligator 0.1.0-alpha.35 · Experimental · Source b48b7fd46efb
Edit this page · Compatibility

Search guides and API reference.

Browse the API · Troubleshooting