crossbind
GitHub
GUIDE · REFERENCE · RUNTIME API

Runtime API

initNative() boots the native runtime and resolves to the module. Every generated module exports it, one call serves every native import of the app, and its options are runtime choices: apart from the env a build declares, nothing in crossbind.config.js reaches it.

The 30-second model

At build time, crossbind build - or the bundler plugin that builds for you - reads crossbind.config.js and drives Emscripten, wasi-sdk, cargo, the Android NDK or Xcode to produce .wasm, .a or .xcframework files. At run time, your app calls initNative() once; when it resolves, every binding and the module helpers are there.

src/main.js
import { initNative, Native } from './native/native.h';
 
await initNative();
console.log(await Native.sample());

Named imports are null until the promise resolves. A module imported after the runtime booted is bound as soon as it loads.

Where it comes from

With a bundler plugin, import it from any header, .rs file, cargo: crate or conan: header you already import; they all export the same function. Each module registers its bindings when it is imported, and the first initNative() boots the runtime once and binds them all. Later calls resolve to the same module and ignore their options.

src/main.js
import { initNative, Matrix } from './native/Matrix.h';
import { Uuid } from 'cargo:uuid';
import { zlibVersion } from 'conan:zlib/zlib.h';
 
await initNative();

Without a bundler, import the built loader from the output folder (paths.output, set to dist in the examples) and call its default export. A Node.js or edge build also writes one ES module per runtime there - node/wasm.mjs, node/napi.mjs and edge/wasm.mjs - that exports initNative and every bound name, and a plain page loads the browser loader with a <script> tag, which defines a global initNative.

src/index.mjs
import initNative from '../dist/my-app-wasm-wasm32-st-release.node.js';
 
const { Native } = await initNative();
console.log(Native.sample());

Options

The wasm runtimes - browser, Node.js and edge - take the same options; only the defaults differ, and an option a runtime has no use for is ignored.

OptionTypeDefaultWhat it does
useWorkerbooleantrue where Worker exists, as in every browserBrowser: runs the module in a Web Worker, so every call returns a promise (worker mode). OPFS needs it.
workerUrlstringthe loader scriptBrowser, worker mode: the script the worker starts from.
fs.opfsbooleantrue in an mt browser build, false in an st oneBrowser: mounts the Origin Private File System under /opfs, inside the worker only. See Files.
envobject of stringsthe build's envEnvironment variables of the native process, merged over the build's. _CROSSBIND_DATA_PATH_ in a value becomes the data folder.
logHandler(text, 'stdout') => voidconsole.debug with a wasm stdout: prefixReceives each line the C++ writes to stdout.
errorHandler(text, 'stderr') => voidconsole.error with a wasm stderr: prefixReceives each line written to stderr.
onRuntimeInitialized(m) => void-Runs once the runtime is ready, before the promise resolves.
getWasmFunction() => WebAssembly.Module-Instantiates a module you already hold instead of fetching the .wasm. Edge runtimes import the .wasm as a module and need it; dist/edge/wasm.mjs passes it for you.
pathstringthe base the bundler serves from; / on a plain page; the loader's folder in Node.jsWhere the loader finds its .wasm, data and worker files: a URL, a root path or a path relative to the page.
paths{ wasm, data, js, worker }the build's file namesFile names under path. A bundler plugin sets them itself and ignores yours.
Function options do not cross into a worker
With useWorker: true, logHandler, errorHandler, onRuntimeInitialized and getWasmFunction stay on the page: functions cannot be sent to a worker, so the worker logs to its own console with the default prefixes. Pass useWorker: false when you need them.

A Node-API addon (-e node on a desktop platform) has no Emscripten runtime and takes three options: env, addonPath (default: the addon next to the loader, or in its platform package) and dataPath (default: data/ next to the loader, fixed once the addon has started). There, build-time env values never replace a variable the process already has; values passed to initNative() do. On React Native, initNative() takes no options and the build's env applies.

The module

The promise resolves to the module: the bound classes, functions, enums and constants as members - the same values the named imports receive - plus these helpers.

MemberAvailable onDoes
toArray(vector)every runtimereturns a bound std::vector as an array
toVector(Vector, array)every runtimereturns an array as a bound vector; wasm builds also take the class name as a string
FSbrowser and Node.js wasm buildsthe Emscripten virtual filesystem
getFileBytes(path), getFileList(start?)browser and Node.js wasm buildsa file as a Uint8Array; a recursive [{ path, size }] listing
getDefaultPath(), getFinalPath(path), getRandomPath(start?), autoMountFiles(files, parent?)browserOPFS and in-memory mounts, and files the user picked - see Files

A plain array passed where C++ takes a std::vector converts on every call in worker mode; on the page thread, in Node.js and on the edge it converts for module functions and constructors only, so pass toVector(...) to a method. A module that binds a pointer also exports the pointer helpers - cstring, allocBuffer, readBuffer and the rest - described under C++. Every header module exports AllSymbols as well: the module itself once booted. Importing it binds every function of that header.

A C++ exception thrown by a call reaches JavaScript as an Error. On wasm its message is <type>: <what()>, with the two parts in e.cppType and e.cppMessage; on React Native and in a Node-API addon it is the plain what() text.

Worker mode

In a browser the module runs in a Web Worker unless you pass useWorker: false, and the page talks to it through proxies. Every call, every new and every value read - constants and fields included - goes through the bridge, so each one returns a promise:

src/main.js
import { initNative, Matrix } from './native/Matrix.h';
 
await initNative();
 
const a = await new Matrix(1210000, 1);
const result = await a.multiple(await new Matrix(1210000, 2));
console.log(await result.get(0));
useWorker: falseuseWorker: true
A call returnsthe valuea promise of it
new X(...) returnsthe instancea promise - write await new X(...)
A returned std::vectora vector objecta plain array
A JavaScript function passed to C++workscannot cross into the worker
Function options of initNativeapplyignored
A path under /opfsthrowsworks while fs.opfs is on and the browser supports OPFS

Set dts: 'promise' in crossbind.config.js so the generated types return promises too. initNative.terminate() stops the worker; the next initNative() starts a fresh one.

Logging and environment

Without handlers, stdout lines go to console.debug and stderr lines to console.error, prefixed wasm stdout: and wasm stderr:. To route them into your own logger, keep the module on the page thread:

src/main.js
const m = await initNative({
useWorker: false,
logHandler: (text) => myLogger.info(`[wasm] ${text}`),
errorHandler: (text) => myLogger.error(`[wasm] ${text}`),
});

Runtime env values are strings, merged over the env the build declares in targetSpecs (Data files). _CROSSBIND_DATA_PATH_ in a value becomes the folder the data files live in: /crossbind in the browser and on the edge, data/ next to the loader in Node.js.

src/index.mjs
const m = await initNative({
env: { TMPDIR: '_CROSSBIND_DATA_PATH_/scratch' },
});

Object lifetime

You never call delete() on what crossbind hands you. Values cross by copy, containers convert at the boundary, and shared_ptr objects are reference-counted: JavaScript's reference goes when the garbage collector reclaims its handle, at a time you do not control. When a C++ object holds a file, a socket or a GPU buffer, give its class a close() method and call it. Memory a library returns through a raw pointer stays the library's: free it with the library's own function.

In the browser and Node.js wasm builds, initNative.terminate() forgets the booted module, and in worker mode stops the worker, so the next initNative() boots a new one. A Node-API addon cannot be unloaded: there the next call resolves the same module with its native state intact.

TypeScript

crossbind writes declarations for every native import under .crossbind/types/, .crossbind/rust-crates/types/ and .crossbind/conan/types/. Install the shared config and extend it once (TypeScript 5.5 or later):

shell
npm install -D @crossbind/typescript-config@beta
tsconfig.json
{ "extends": "@crossbind/typescript-config" }

From a C++ header, classes get typed constructors, fields and methods; other names are declared any, and initNative returns Promise<unknown>. In worker mode set dts: 'promise' in crossbind.config.js: every generated return becomes a Promise, while constructors stay synchronously typed because TypeScript has no async constructor - write await new X() anyway.

Common pitfalls

  • useWorker: true in crossbind.config.js does nothing. That file is read once by the build; runtime options belong to initNative().
  • Only the first initNative() call's options count; later calls resolve to the same module.
  • x.method is not a function in a browser usually means a new X() whose promise was never awaited.
  • A function of a dependency that you reach only through the module, as in m.GDALVersionInfo(), needs a named import too when you import other names from that header by name. Without one it fails with m.GDALVersionInfo is not a function on the page thread and crossbind: GDALVersionInfo is not bound in the native module in a worker.
  • Mounting /opfs/... from the page thread throws, and so does any /opfs path while fs.opfs is off.
  • runtime: 'mt' works in the dev server and fails in production when the host does not send the COOP/COEP headers - see Threads and workers.
  • Edge runtimes have no Web Workers: no useWorker, no OPFS and no mt.

init.md and lifecycle-and-types.md are the reference documents behind this page.

Type to search every guide page and section.
↑↓ navigate↵ openesc close