Bridging WebAssembly and JavaScript
WebAssembly modules often need to reach beyond their sandbox to call web APIs or third-party libraries. When that happens during C++ development with Emscripten, you need mechanisms to invoke external functions, retain the values they return, and pass those values into later calls. For asynchronous work, Asyncify lets synchronous C/C++ code pause, await a promise, and then resume once the result is ready.
Emscripten ships with several approaches for this kind of interop:
emscripten::valfor holding and manipulating JavaScript values directly from C++.EM_JSfor embedding JavaScript implementations behind C/C++ function declarations.EM_ASYNC_JS, a variant ofEM_JStailored for asynchronous JavaScript snippets.EM_ASMfor executing short inline snippets without a formal function declaration.--js-libraryfor grouping many JavaScript functions into a custom library file.
Working with emscripten::val
Backed by Embind, the emscripten::val class bridges C++ and JavaScript types, letting you invoke global APIs and convert values in either direction. The example below combines it with Asyncify's .await() method to fetch and parse JSON:
#include <emscripten/val.h>
using namespace emscripten;
val fetch_json(const char *url) {
// Get and cache a binding to the global `fetch` API in each thread.
thread_local const val fetch = val::global("fetch");
// Invoke fetch and await the returned `Promise<Response>`.
val response = fetch(url).await();
// Ask to read the response body as JSON and await the returned `Promise<any>`.
val json = response.call<val>("json").await();
// Return the JSON object.
return json;
}
// Example URL.
val example_json = fetch_json("https://httpbin.org/json");
// Now we can extract fields, e.g.
std::string author = json["slideshow"]["author"].as<std::string>();
That pattern works, but each val operation goes through a fairly heavy pipeline: C++ arguments become an intermediate format, the JavaScript side translates and executes them, and the return value is converted back before C++ can use it. Every await() additionally unwinds the module's entire call stack, waits on the promise, and later reconstructs the stack. When the C++ side is essentially just coordinating JavaScript calls, it's worth asking whether that logic should move to JavaScript entirely, cutting down the overhead.
The EM_JS macro
With EM_JS, you can move that coordination code into JavaScript while keeping a C/C++ function declaration. Because WebAssembly functions only accept numeric parameters and return values, any other types must be converted explicitly. Here's how that plays out:
- Numbers pass through unchanged:
// Passing numbers, doesn't need any conversion. EM_JS(int, add_one, (int x), { return x + 1; }); int x = add_one(41); - For strings, use the conversion and allocation helpers from preamble.js:
EM_JS(void, log_string, (const char *msg), { console.log(UTF8ToString(msg)); }); EM_JS(const char *, get_input, (), { let str = document.getElementById('myinput').value; // Returns heap-allocated string. // C/C++ code is responsible for calling `free` once unused. return allocate(intArrayFromString(str), 'i8', ALLOC_NORMAL); }); - Arbitrary value types can leverage the JavaScript API behind
val, converting values to intermediate handles that C++ can understand:EM_JS(void, log_value, (EM_VAL val_handle), { let value = Emval.toValue(val_handle); console.log(value); }); EM_JS(EM_VAL, find_myinput, (), { let input = document.getElementById('myinput'); return Emval.toHandle(input); }); val obj = val::object(); obj.set("x", 1); obj.set("y", 2); log_value(obj.as_handle()); // logs { x: 1, y: 2 } val myinput = val::take_ownership(find_input()); // Now you can store the `find_myinput` DOM element for as long as you like, and access it later like: std::string value = input["value"].as<std::string>();
With those conversion tools in hand, the JSON fetching example can be rewritten to stay on the JavaScript side for most of its work:
EM_JS(EM_VAL, fetch_json, (const char *url), {
return Asyncify.handleAsync(async () => {
url = UTF8ToString(url);
// Invoke fetch and await the returned `Promise<Response>`.
let response = await fetch(url);
// Ask to read the response body as JSON and await the returned `Promise<any>`.
let json = await response.json();
// Convert JSON into a handle and return it.
return Emval.toHandle(json);
});
});
// Example URL.
val example_json = val::take_ownership(fetch_json("https://httpbin.org/json"));
// Now we can extract fields, e.g.
std::string author = json["slideshow"]["author"].as<std::string>();
The result still has conversions at the entry and exit boundaries, but the core logic is plain JavaScript. Unlike the val-based version, this implementation can be optimized directly by the JavaScript engine, and it pauses the C++ side only once for the entire set of asynchronous operations.
EM_ASYNC_JS for async snippets
The explicit Asyncify.handleAsync wrapper in the last example exists just to let an async function run under Asyncify. Because that need is so common, the EM_ASYNC_JS macro was introduced to combine the declaration and the wrapper. This produces the final version of the fetch example:
EM_ASYNC_JS(EM_VAL, fetch_json, (const char *url), {
url = UTF8ToString(url);
// Invoke fetch and await the returned `Promise<Response>`.
let response = await fetch(url);
// Ask to read the response body as JSON and await the returned `Promise<any>`.
let json = await response.json();
// Convert JSON into a handle and return it.
return Emval.toHandle(json);
});
// Example URL.
val example_json = val::take_ownership(fetch_json("https://httpbin.org/json"));
// Now we can extract fields, e.g.
std::string author = json["slideshow"]["author"].as<std::string>();
When to reach for EM_ASM
The EM_JS macro is generally the preferred approach, as it declares a dedicated, typed function that behaves like any other JavaScript import. For quick one-off statements, though—a console.log call, a debugger; breakpoint, or something equally short—setting up a full function is overkill. The EM_ASM macro family (EM_ASM, EM_ASM_INT, and EM_ASM_DOUBLE) runs code inline at the insertion point.
Because there's no function prototype, the return type is chosen via the macro suffix: EM_ASM behaves like a void function, EM_ASM_INT returns an integer, and EM_ASM_DOUBLE returns a floating-point number. Arguments are referenced as $0, $1, and so on, and are limited to numeric values just like other WebAssembly interop. Here's how to log a JavaScript value:
val obj = val::object();
obj.set("x", 1);
obj.set("y", 2);
// executes inline immediately
EM_ASM({
// convert handle passed under $0 into a JavaScript value
let obj = Emval.fromHandle($0);
console.log(obj); // logs { x: 1, y: 2 }
}, obj.as_handle());
The --js-library route
For advanced integration, Emscripten supports putting JavaScript code in a separate file using its own library format:
mergeInto(LibraryManager.library, {
log_value: function (val_handle) {
let value = Emval.toValue(val_handle);
console.log(value);
}
});
Matching prototypes must then be declared on the C++ side:
extern "C" void log_value(EM_VAL val_handle);
The two are connected at link time by passing the file via the --js-library option to emcc.
Be aware that this module format is non-standard and requires careful dependency annotations, so it's mainly intended for scenarios where a larger collection of JavaScript functions is needed in one place.



