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::val for holding and manipulating JavaScript values directly from C++.
  • EM_JS for embedding JavaScript implementations behind C/C++ function declarations.
  • EM_ASYNC_JS, a variant of EM_JS tailored for asynchronous JavaScript snippets.
  • EM_ASM for executing short inline snippets without a formal function declaration.
  • --js-library for 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.