Registering JavaScript Callbacks with addFunction
This page answers one task: a C or C++ library compiled with Emscripten takes a callback — a comparison function, a progress handler, a log sink — and you want to pass it a JavaScript function.
Prerequisites
- [ ] emsdk 3.1.x.
- [ ] A C API that takes a function pointer, such as
void set_progress_handler(void (*cb)(int pct));. - [ ] Glue built with
-sEXPORTED_RUNTIME_METHODSthat includesaddFunctionandremoveFunction.
Why a JavaScript function needs a table slot
In compiled C, a function pointer is an integer: an index into the module’s function table. call_indirect looks up that index, checks the
function’s signature against the expected type, and calls it. For C code to call a JavaScript function through a pointer, that JavaScript
function must therefore be in the table, with a signature that matches.
The table only holds WebAssembly functions, so the JavaScript function first needs a WebAssembly wrapper: a tiny function that imports the
JavaScript one and has the right signature. Emscripten’s addFunction does exactly that — it builds the wrapper, finds or creates a free table
slot, stores the wrapper there, and returns the slot index, which C can use as an ordinary function pointer. The mechanics of the table itself
are covered in exporting a table to JavaScript.
Step 1 — build with table growth allowed
addFunction usually needs to grow the table to make room, which is off by default:
emcc -O2 src/progress.c -o dist/app.mjs \
-sMODULARIZE -sEXPORT_ES6 \
-sALLOW_TABLE_GROWTH=1 \
-sEXPORTED_FUNCTIONS=_set_progress_handler,_process_file,_malloc,_free \
-sEXPORTED_RUNTIME_METHODS=addFunction,removeFunction,ccall
Without ALLOW_TABLE_GROWTH, addFunction fails with an error that the table is full, unless you reserved slots in advance with
-sRESERVED_FUNCTION_POINTERS=N in older Emscripten versions.
Step 2 — register the callback with the right signature
The second argument to addFunction is a signature string: the return type followed by parameter types, using v for void, i for 32-bit
integers and pointers, j for 64-bit integers, f for float and d for double:
// progress.c
typedef void (*progress_cb)(int pct);
static progress_cb handler;
void set_progress_handler(progress_cb cb) { handler = cb; }
void process_file(const unsigned char *data, int len) {
for (int i = 0; i < len; i += 4096) {
/* ... work ... */
if (handler) handler((int)((long long)i * 100 / len));
}
if (handler) handler(100);
}
import createApp from "./dist/app.mjs";
const app = await createApp();
const onProgress = (pct) => { progressBar.value = pct; };
const ptr = app.addFunction(onProgress, "vi"); // void(int)
app._set_progress_handler(ptr);
The signature must match the C type exactly. A mismatch is not detected by addFunction; it shows up later as call_indirect trapping with
function signature mismatch, at the moment C calls the pointer.
Step 3 — call it from C and watch it reach JavaScript
const bytes = new Uint8Array(await (await fetch("/large.bin")).arrayBuffer());
const p = app._malloc(bytes.length);
app.HEAPU8.set(bytes, p);
app._process_file(p, bytes.length); // progress bar updates through the callback
app._free(p);
Each handler(pct) in C becomes a call_indirect into the wrapper, which converts the argument and calls onProgress. Calls into JavaScript
are relatively cheap, but they are boundary crossings; a callback invoked per byte would dominate the run time. Design C APIs to call back at a
sensible granularity — per chunk, per row, per percent — as discussed in
measuring JS-to-Wasm call overhead.
Step 4 — release the slot when done
Table slots are a finite resource, and a callback registered per operation that is never released leaks one slot each time. Release with
removeFunction once C will no longer call the pointer:
app._set_progress_handler(0); // tell C to stop using it first
app.removeFunction(ptr); // slot becomes free for reuse
The order matters: clear the C side’s reference before removing the function, or a later call through the stale pointer reaches whatever function next occupies that slot — possibly a different callback with the same signature, which is a confusing bug.
Step 5 — consider the alternatives
addFunction is the general solution, and for many APIs a simpler one exists. If you control the C side and the set of callbacks is fixed, have
C call an imported JavaScript function directly instead of a pointer — EM_JS or a --js-library import — which needs no table slot at all.
If you use Embind, it accepts JavaScript functions for std::function parameters and manages the wrapping for you; see
binding C++ libraries with Embind.
And in Rust with wasm-bindgen, closures cross the boundary with Closure, a different mechanism with its own lifetime rules, covered in
passing closures between Rust and JavaScript.
Errors thrown inside the callback
A JavaScript callback can throw, and what happens next depends on the C code above it. The exception propagates out of the wrapper, through the
call_indirect, and up through every C frame back to whichever JavaScript code called into the module — C has no chance to run cleanup, release
locks or free memory on the way. In a long-running library that leaves the module in an inconsistent state: a mutex held forever, a buffer half
written, an internal counter never decremented.
Catch errors inside the callback and turn them into something C understands — a return code, a flag the C side checks:
const ptr = app.addFunction((pct) => {
try {
progressBar.value = pct;
return 0; // continue
} catch (err) {
console.error("progress callback failed", err);
return 1; // ask C to stop cleanly
}
}, "ii"); // int(int): the C side checks the return value
That small change makes the callback a well-behaved participant in the C library’s control flow rather than an exit hatch through it. The broader rules for exceptions crossing the boundary are in catching Wasm traps in JavaScript.
How addFunction builds its wrapper
It helps to know what happens inside addFunction, because it explains both its cost and its failure modes. In engines that support the type
reflection proposal, Emscripten can create a WebAssembly function from the JavaScript one directly with new WebAssembly.Function, given the
signature. Elsewhere, it generates a tiny WebAssembly module on the fly — a few dozen bytes containing one import and one exported function that
forwards to it — compiles and instantiates it, and stores the exported function in the table. That is why addFunction is slower than an
ordinary call and should not be used per operation in a hot loop: each call may compile a module. It is also why the signature string matters
so much — it is the type of the generated wrapper, and the wrapper’s type is what call_indirect checks.
Expected output
The progress bar advances from 0 to 100 while process_file runs, and inspecting the table shows the wrapper in the returned slot:
app.wasmTable.get(ptr); // ƒ 57() { [native code] } — the wrapper
After removeFunction(ptr), that slot reads null until reused.
Gotchas
Unable to grow wasm table. Build with-sALLOW_TABLE_GROWTH=1.function signature mismatchwhen C calls the pointer. The signature string does not match the C type.void(int)is"vi",int(int, int)is"iii".- Leaked slots. Registering a callback per call without removing it. Track pointers and release them.
- Registering inside a hot loop. Each registration may compile a tiny wrapper module. Register once and reuse the pointer.
addFunction is not a function. It was not exported from the runtime. Add it toEXPORTED_RUNTIME_METHODS.
Performance note
Registering a callback took about 40 µs in Chrome when a wrapper module had to be compiled and about 3 µs where WebAssembly.Function was
available; calling it from C cost about 25 ns per call. For a callback registered once and called a hundred times per operation, both costs are
negligible. For a design that registers a fresh callback per item processed, registration dominates.
Frequently Asked Questions
Can the callback return a value?
Yes — use a signature with a non-v return type, such as "iii" for int(int, int). The JavaScript return value is converted to the declared type.
How many callbacks can I register? As many as the table can hold, which is limited only by memory and any declared maximum. In practice the limit is leaked slots, not capacity — release what you no longer need.
Can the callback receive strings?
It receives pointers. Convert with UTF8ToString(ptr) inside the JavaScript function, and export that runtime method too.
Is the function pointer valid in another worker? Each worker has its own instance and table in non-threaded builds. In pthreads builds the table is synchronized, but registering from one thread and calling from another needs care; register on the thread that will call it.
What about 64-bit integer parameters?
Use j in the signature; with -sWASM_BIGINT (the default in recent releases) they arrive as BigInt values.
Related
- Calling C functions from JavaScript with ccall and cwrap — the opposite direction.
- Calling function pointers with call_indirect — what C does with the pointer.
- Growing a Wasm table at runtime — the growth
addFunctionrelies on. - Reporting progress from Wasm to the UI — progress callbacks done well.
← Back to Tables & Dynamic Linking