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.

EM_JS, EM_ASM and --js-library EM_JS defines a named JavaScript function with a C signature inside a C file. EM_ASM inlines an anonymous snippet at the call site. A JS library keeps JavaScript in its own file, declared from C with extern prototypes, and supports dependencies and shared helpers. EM_JS named function, C signature JavaScript in the C file easy to call repeatedly small typed helpers EM_ASM anonymous inline snippet args as $0, $1 … quick one-offs debugging, one-liners --js-library separate .js file dependencies via __deps lintable, reusable larger integrations

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.

How an EM_JS function is linked The EM_JS macro records the JavaScript body in the compiled object. At link time Emscripten extracts it into the generated glue as an import function. The C code calls it through the import, passing numbers and pointers, and JavaScript decodes strings from linear memory. EM_JS macro in a .c file emcc link extracts JS body glue import _set_status(msg, progress) C call numbers + pointers decode the string UTF8ToString in JS

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. stringToNewUTF8 allocates. Free the result.
  • Awaiting Promises in EM_JS. Plain EM_JS cannot wait. Use EM_ASYNC_JS with Asyncify or JSPI.
  • Many EM_ASM snippets. Each becomes a function. Use EM_JS or 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%.

Cost of calling JavaScript from C Nanoseconds per call from C into JavaScript through EM_JS with two integer arguments, with a 30-character string decoded by UTF8ToString, and per log line when 1,000 lines are batched into one call. ns per call or line EM_JS, two ints 20 ns EM_JS + UTF8ToString 150 ns batched logging (per line) 9 ns

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.

← Back to C/C++ to Wasm with Emscripten