Binding C++ Libraries with Embind
This guide shows how to expose a C++ class and its methods to JavaScript using Emscripten’s embind,
so you call new Module.Matrix(3, 3) from JS and get a real bound object — instead of hand-marshaling
pointers through ccall. embind (declared with the EMSCRIPTEN_BINDINGS macro) generates the glue
that maps C++ constructors, methods, free functions, and plain structs onto idiomatic JavaScript,
including type conversions for strings and std::vector. The cost is a little call overhead and a hard
rule about freeing objects; both are covered below.
The mechanism is worth understanding before you use it. EMSCRIPTEN_BINDINGS(name) { ... } is a macro
that registers a static initializer; when the module loads, that initializer runs and populates an
internal type registry the JavaScript glue reads to build wrapper objects. This is why the bindings
“just appear” on Module after instantiation — and why forgetting to link embind makes them vanish
silently, since the registry is never linked in. Unlike ccall/cwrap, where you describe the C
signature on the JavaScript side at call time, embind records the full type information at compile
time on the C++ side, so the JS API is generated rather than hand-declared.
Prerequisites
- [ ] emsdk activated —
source ./emsdk_env.sh, withem++ --version≥ 3.1.50. - [ ] C++ source compiled with
em++(notemcc) so the C++ standard library links correctly. - [ ] Familiarity with the pointer/length boundary —
embindhides the manual marshaling, but knowing what it hides (see Related, below) makes its overhead and ownership rules make sense. - [ ] A browser or Node ≥ 16 to run the modular ESM output.
Procedure
-
Write the C++ class you want to expose. Nothing here is Emscripten-specific yet — it is ordinary C++.
// matrix.cpp #include <vector> #include <string> struct Size { int rows; int cols; }; class Matrix { public: Matrix(int rows, int cols) : rows_(rows), cols_(cols), data_(rows * cols, 0.0) {} double get(int r, int c) const { return data_[r * cols_ + c]; } void set(int r, int c, double v) { data_[r * cols_ + c] = v; } Size size() const { return { rows_, cols_ }; } std::string label() const { return "Matrix " + std::to_string(rows_) + "x" + std::to_string(cols_); } private: int rows_, cols_; std::vector<double> data_; }; -
Declare the bindings in an
EMSCRIPTEN_BINDINGSblock. Useclass_for the class,value_objectfor the plain struct (it converts to/from a JS object by value), andfunctionfor free functions.#include <emscripten/bind.h> using namespace emscripten; double trace(const Matrix& m) { // a free function over the bound type double t = 0; for (int i = 0; i < m.size().rows; ++i) t += m.get(i, i); return t; } EMSCRIPTEN_BINDINGS(matrix_module) { value_object<Size>("Size") .field("rows", &Size::rows) .field("cols", &Size::cols); class_<Matrix>("Matrix") .constructor<int, int>() .function("get", &Matrix::get) .function("set", &Matrix::set) .function("size", &Matrix::size) .function("label", &Matrix::label); function("trace", &trace); } -
Compile with embind linked and the module exported as ESM. The
-lembindflag is mandatory — without it the bindings registry is never linked and nothing is exposed.em++ matrix.cpp -lembind -O2 \ -sMODULARIZE=1 -sEXPORT_ES6=1 -sEXPORT_NAME=createMatrixModule \ -o dist/matrix.mjs -
Use the bound types from JavaScript. Constructors become
new, methods become methods, and thevalue_objectarrives as a plain JS object. Free the C++ object when done.import createMatrixModule from "./dist/matrix.mjs"; const Module = await createMatrixModule(); const m = new Module.Matrix(3, 3); // calls the C++ constructor m.set(0, 0, 2.0); m.set(1, 1, 5.0); m.set(2, 2, 1.5); console.log(m.label()); // "Matrix 3x3" console.log(m.size()); // { rows: 3, cols: 3 } (value_object) console.log(Module.trace(m)); // 8.5 m.delete(); // free the underlying C++ object — REQUIRED
Expected output
Running the script against the compiled module prints the bound class in action:
Matrix 3x3
{ rows: 3, cols: 3 }
8.5
The size() call returns a real JavaScript object because Size was registered as a value_object,
so its fields are copied across the boundary by value rather than handed back as an opaque handle. The
trace(m) call passes the bound Matrix back into C++ by reference — embind resolves the JS wrapper
to the underlying C++ pointer automatically.
The distinction between class_ and value_object is the most important design choice you make.
A class_ registration produces a handle: JavaScript holds a reference to a C++ object that lives in
linear memory, methods call into that object, and you are responsible for its lifetime with
.delete(). A value_object registration produces a copy: the struct’s fields are read out into a
plain JavaScript object (or read in from one) at the boundary, with no ongoing C++ allocation and
nothing to free. Use class_ for stateful objects with behavior and identity; use value_object for
small plain-data records — points, sizes, colors, config bundles — that you want to pass by value. Get
this wrong and you either leak handles you forgot were handles, or pay copy overhead on objects you
meant to share by reference.
Gotchas
BindingError: Matrix has unknown type (or _embind_register... missing). You forgot -lembind,
so the binding registrations were dead-stripped. Add -lembind to the em++ command. This is the most
common embind failure and produces a runtime error, not a compile error.
Memory leaks because you never called .delete(). Every object created with new Module.X(...) (or
returned by a bound function as a raw object) holds a C++ allocation in linear memory that JavaScript’s
garbage collector cannot reclaim. You must call .delete() explicitly. Forgetting it leaks the C++ heap
silently until the module runs out of memory. Wrap usage in try/finally:
const m = new Module.Matrix(1024, 1024);
try {
/* ... use m ... */
} finally {
m.delete();
}
BindingError: function 'compute' called with X arguments, expected Y from overloads. embind
cannot dispatch on argument types, only on argument count. Two overloads with the same arity are
ambiguous. Disambiguate with select_overload when registering:
class_<Image>("Image")
.function("resize", select_overload<void(int)>(&Image::resize))
.function("resizeXY", select_overload<void(int,int)>(&Image::resize));
A bound function returning a raw pointer crashes or leaks. Decide ownership explicitly with a return
policy — allow_raw_pointers() to permit raw pointers at all, or return smart pointers
(std::shared_ptr) which embind ref-counts for you so .delete() decrements correctly.
TypeError: Cannot pass non-string to std::string. embind converts JS strings to std::string
automatically, but a std::string& non-const reference parameter cannot be bound, because there is no
JS object whose mutation would write back through the reference. Take std::string by value or by
const& in any method you expose. The same restriction applies to other by-value-convertible types:
expose them by value or const-reference, not by mutable reference.
Performance note
An embind call carries more overhead than a cwrap call: each invocation runs through a generic
type-conversion and dispatch layer that marshals every argument and the return value, whereas a cwrap
wrapper is a thin shim over the raw export. For a trivial method the embind path can be several times the
cost of the equivalent ccall. This is irrelevant for coarse-grained calls — constructing a matrix,
running a solver — but it matters in a hot loop. The fix is the same as everywhere on this boundary:
batch. Do the heavy work inside one bound method that processes a whole buffer, rather than calling a
bound getter per element. Keep embind for the ergonomic API surface and drop to _malloc +
HEAPU8 + ccall for the inner loop.
Concretely, a design that exposes a Matrix class with a per-cell get/set and loops over a million
cells from JavaScript will spend the bulk of its time in the conversion layer, not in arithmetic. The
same Matrix exposing a single multiply(other) method that does the whole product in C++ and returns
one bound result pays the embind cost exactly twice — once on the call, once on the return — and runs at
native speed in between. The principle generalizes across the whole boundary: minimize the number of
crossings, not the cost of any one crossing, and let embind give you a clean object model around
coarse-grained operations.
Frequently Asked Questions
Can I bind a std::vector directly?
Yes — register it with register_vector<double>("VectorDouble"), and embind exposes push_back,
get, set, and size on the JS side. The vector still lives in linear memory, so the .delete()
ownership rule applies to any vector instance you create.
Does embind work with -sMODULARIZE?
Yes, and you should use it. The bound types attach to the Module object the factory resolves to, so
new Module.Matrix(...) is available after you await createMatrixModule().
When should I prefer cwrap over embind?
Prefer cwrap for a small C ABI surface of primitive-typed functions, or for the performance-critical
inner loop where call overhead matters. Prefer embind when you are exposing C++ classes, want
idiomatic JS objects, or need automatic std::string/std::vector conversion. The two are not mutually
exclusive — a common pattern is an embind class for the public API plus a couple of raw ccall
hotspots underneath it.
How do I expose a static or free function, not a method?
Use function("name", &freeFn) at the top level of the EMSCRIPTEN_BINDINGS block for free functions,
and .class_function("name", &Class::staticFn) inside a class_ registration for static members. Both
attach to Module (or to the class constructor) without needing an instance, so there is nothing to
.delete().
Related
- Passing complex types across the boundary — the manual ABI that embind automates for strings and structs.
- ESM bindings & module generation — packaging the embind factory for a bundler.
- Migrating legacy C code to WebAssembly — porting the C/C++ source you are now binding.
← Back to C/C++ to Wasm with Emscripten