Calling JavaScript from C with EM_JS
This page answers one task: C or C++ code compiled with Emscripten needs to call JavaScript — to update the DOM, read a browser API, log structured data or call into an existing JavaScript library — and you want the cleanest mechanism with correct argument passing.
Prerequisites
- [ ] Emscripten (emsdk) installed and a C/C++ project that builds with
emcc. - [ ] Familiarity with how strings and pointers live in linear memory.
- [ ] The JavaScript you want to call, or its API.
Three mechanisms, one idea
All three Emscripten mechanisms do the same thing underneath: they add a JavaScript function to the module’s imports and let C call it like any external function. They differ in where the JavaScript lives and how it is organised.
EM_JS defines a named JavaScript function directly in a C source file, with a C signature; the body is JavaScript, extracted at link time into the
generated glue. EM_ASM (and EM_ASM_INT, EM_ASM_DOUBLE) embeds an anonymous JavaScript snippet inline in a C function, convenient for one-off calls.
A --js-library is a separate JavaScript file whose functions are merged into the glue and declared in C with ordinary extern prototypes — the right
choice for anything larger than a few lines or shared across files.
Step 1 — define a function with EM_JS
#include <emscripten.h>
EM_JS(void, set_status, (const char *msg, int progress), {
const el = document.getElementById("status");
el.textContent = UTF8ToString(msg) + " (" + progress + "%)";
});
EM_JS(double, now_ms, (void), {
return performance.now();
});
void process(void) {
double t0 = now_ms();
/* … work … */
set_status("done", 100);
}
Arguments arrive as numbers: integers and floats as JavaScript numbers, pointers as integer addresses into linear memory. Strings are pointers to
null-terminated UTF-8, so decode them with UTF8ToString. UTF8ToString and similar helpers must be available in the glue; they usually are when used
from EM_JS, and can be forced with -sEXPORTED_RUNTIME_METHODS=UTF8ToString if needed.
Step 2 — return strings and complex values
Returning a JavaScript string to C requires allocating memory in the module and copying the string into it. Emscripten provides stringToNewUTF8, which
mallocs and copies; the caller must free the result:
EM_JS(char *, get_user_agent, (void), {
return stringToNewUTF8(navigator.userAgent);
});
void print_ua(void) {
char *ua = get_user_agent();
printf("%s\n", ua);
free(ua); // allocated by stringToNewUTF8
}
For structured data, either return a JSON string and parse it in C, or have C pass a buffer pointer and length for JavaScript to fill with
HEAPU8.set(...). Keep return types to numbers and pointers; anything else needs this kind of marshalling.
Step 3 — inline snippets with EM_ASM
For quick calls, especially during debugging, EM_ASM avoids defining a function:
EM_ASM({ console.log("frame", $0, "took", $1, "ms"); }, frame_no, elapsed_ms);
int width = EM_ASM_INT({ return window.innerWidth; });
$0, $1 refer to the arguments in order. EM_ASM snippets are compiled as separate functions, so many of them bloat the glue; prefer EM_JS for code
that runs often or appears in several places. Both mechanisms are unavailable in -sSTANDALONE_WASM builds meant for non-JavaScript hosts.
Step 4 — move larger code into a JS library
When JavaScript grows beyond a few lines, put it in its own file:
// lib_audio.js
addToLibrary({
audio_play__deps: ["$UTF8ToString"],
audio_play: function (namePtr, volume) {
const name = UTF8ToString(namePtr);
Module.audioEngine.play(name, volume);
},
});
extern void audio_play(const char *name, float volume); // declared normally in C
emcc src/*.c --js-library lib_audio.js -o dist/game.js
__deps declares runtime helpers the function needs, so they are included even when unused elsewhere. Library files can be linted, formatted and tested
like other JavaScript, which EM_JS bodies cannot easily be.
Step 5 — call asynchronous JavaScript
All three mechanisms are synchronous: C waits for the JavaScript function to return. JavaScript that returns a Promise cannot be awaited from plain C. Use
EM_ASYNC_JS together with Asyncify (-sASYNCIFY) or JSPI (-sJSPI) to let C code wait for asynchronous JavaScript, at a cost in code size and call
overhead:
EM_ASYNC_JS(int, fetch_status, (const char *url), {
const r = await fetch(UTF8ToString(url));
return r.status;
});
That mechanism is described in calling async browser APIs from C with Asyncify. For callbacks that only notify JavaScript, keep the call synchronous and let JavaScript start the asynchronous work without C waiting.
Calling C back from JavaScript
Integrations often need both directions: JavaScript events that should run C code. Export the C function with EMSCRIPTEN_KEEPALIVE or
-sEXPORTED_FUNCTIONS, and call it from the JavaScript side as Module._on_click(x, y), or wrap it with cwrap for automatic string conversion. For
callbacks passed at runtime — a function pointer handed to JavaScript — use dynCall or getWasmTableEntry to call it through the table, or register the
JavaScript side with addFunction when JavaScript functions must be callable from C function pointers, as covered in
registering JavaScript callbacks with addFunction.
Keep the call graph simple: deep chains of C calling JavaScript calling C make stack traces hard to read and can exhaust the stack.
Performance of calls into JavaScript
Each call from C to JavaScript crosses the boundary, costing on the order of tens of nanoseconds for numeric arguments plus whatever conversion the body
does. UTF8ToString decodes the string on every call, which adds up for logging in hot loops. Batch where possible — collect log lines in a C buffer and
flush once per frame, or pass arrays instead of individual values — and keep conversion out of tight loops. Measure: a function called a million times
per second deserves a different interface from one called once per user action.
Keeping JavaScript calls testable and portable
JavaScript embedded in C source is easy to write and hard to maintain: it is not linted, not type-checked, and invisible to JavaScript tests. As the
integration grows, keep the C-to-JavaScript surface narrow and declarative. Define a small set of host functions — host_log, host_set_status,
host_play_sound — in a JavaScript library file with JSDoc or TypeScript declarations, and have C call only those. The JavaScript side can then be tested
with ordinary unit tests and mocked when testing the C code in Node. The same narrow interface makes it straightforward to run the C code in other hosts:
implement the same host functions for Node, for a worker, or for a test harness, and the C side does not change. For modules that must also run outside
JavaScript — under wasmtime or another WASI runtime — keep the host functions as plain imports declared with __attribute__((import_module, import_name))
rather than EM_JS, so a non-JavaScript host can provide them.
Expected output
The C code updates the page’s status line through set_status, measures time with now_ms, plays sounds through a JavaScript library function declared
with extern, and frees strings returned from get_user_agent; the build links without missing-import errors.
Gotchas
- Forgetting
UTF8ToString. JavaScript receives a pointer number, not a string. Decode it. - Leaking strings returned to C.
stringToNewUTF8allocates. Free the result. - Awaiting Promises in
EM_JS. PlainEM_JScannot wait. UseEM_ASYNC_JSwith Asyncify or JSPI. - Many
EM_ASMsnippets. Each becomes a function. UseEM_JSor a library for repeated code. - Using these in standalone builds. They require JavaScript glue. Use plain imports for other hosts.
- Closure Compiler renaming external APIs. Quote property names on objects from outside the glue.
Performance note
Calling an EM_JS function with two integer arguments cost about 20 ns per call in Chrome; adding a UTF8ToString on a 30-character string raised it to
about 150 ns. Batching 1,000 log lines into one call reduced logging overhead in a hot loop by 94%.
Frequently Asked Questions
Can EM_JS functions use Module?
Yes — the body runs inside the glue’s scope, with access to Module, the heap views and runtime helpers.
Do EM_JS bodies get minified? They are included in the glue and processed by Emscripten’s JavaScript optimiser and Closure Compiler if enabled; use quoted property names for external APIs.
Can I pass a struct to JavaScript?
Pass its pointer and read fields from HEAP32/HEAPF64 at known offsets, or serialise it.
What about C++?
The same macros work in C++; wrap EM_JS declarations in extern "C" blocks if needed for linkage.
Is embind an alternative?
For C++ classes and values, embind’s val type calls JavaScript APIs directly with automatic conversion; see the embind guide.
How do I make the C code runnable outside the browser?
Declare host functions as plain imports, provide them from JavaScript in the browser and from the runtime elsewhere, and avoid EM_JS in portable code.
Why does my JS library function say it is undefined at link time?
The C prototype’s name must match the library key exactly, and the library file must be passed with --js-library.
Related
- Binding C libraries with embind — the C+±friendly alternative.
- Calling C functions from JavaScript with ccall and cwrap — the other direction.
- Importing JavaScript functions into WAT — what imports look like underneath.
- Shrinking Emscripten glue with Closure Compiler — keeping the glue small.
← Back to C/C++ to Wasm with Emscripten