The WebIDL Binder provides a simple and lightweight approach to binding C++, so that compiled code can be called from JavaScript as if it were a normal JavaScript library.
The WebIDL Binder uses WebIDL to define the bindings, an interface language that was specifically designed for gluing together C++ and JavaScript. Not only is this a natural choice for the bindings, but because it is low-level it is relatively easy to optimize.
The binder supports the subset of C++ types that can be expressed in WebIDL. This subset is more than sufficient for most use cases — examples of projects that have been ported using the binder include the Box2D and Bullet physics engines.
This topic shows how bind and use C++ classes, functions and other types using IDL.
Note
An alternative to the WebIDL Binder is to use Embind. For more information see Binding C++ and JavaScript — WebIDL Binder and Embind.
Binding using the WebIDL Binder is a three-stage process:
Create a WebIDL file that describes the C++ interface.
Use the binder to generate C++ and JavaScript “glue” code.
Compile this glue code with the Emscripten project.
The first step is to create a WebIDL file that describes the C++ types you are going to bind. This file will duplicate some of the information in the C++ header file, in a format that is explicitly designed both for easy parsing, and for representing code items.
For example, consider the following C++ classes:
class Foo {
public:
int getVal();
void setVal(int v);
};
class Bar {
public:
Bar(long val);
void doSomething();
};
The following IDL file can be used to describe them:
interface Foo {
void Foo();
long getVal();
void setVal(long v);
};
interface Bar {
void Bar(long val);
void doSomething();
};
The mapping between the IDL definition and the C++ is fairly obvious. The main things to notice are:
The IDL class definitions include a function returning
void
that has the same name as the interface. This constructor allows you to create the object from JavaScript, and must be defined in IDL even if the C++ uses the default constructor (seeFoo
above).The type names in WebIDL are not identical to those in C++ (for example,
int
maps tolong
above). For more information about the mappings see WebIDL types.
Note
structs
are defined in the same way as the classes above — using the interface
keyword.
The bindings generator (tools/webidl_binder.py) takes a Web IDL file name and an output file name as inputs, and creates C++ and JavaScript glue code files.
For example, to create the glue code files glue.cpp and glue.js for the IDL file my_classes.idl, you would use the following command:
tools/webidl_binder my_classes.idl glue
To use the glue code files (glue.cpp
and glue.js
) in a project:
Add --post-js glue.js
in your final emcc command. The post-js option adds the glue code at the end of the compiled output.
Create a file called something like my_glue_wrapper.cpp to #include
the headers of the classes you are binding and glue.cpp. This might have the following content:
#include <...> // Where "..." represents the headers for the classes we are binding. #include <glue.cpp>Note
The C++ glue code emitted by the bindings generator does not include the headers for the classes it binds because they are not present in the Web IDL file. The step above makes these available to the glue code. Another alternative would be to include the headers at the top of glue.cpp, but then they would be overwritten every time the IDL file is recompiled.
Add my_glue_wrapper.cpp to the final emcc command.
The final emcc command includes both the C++ and JavaScript glue code, which are built to work together:
emcc my_classes.cpp my_glue_wrapper.cpp --post-js glue.js -o output.js
The output now contains everything needed to use the C++ classes through JavaScript.
When using the WebIDL binder, often what you are doing is creating a library. In that case, the MODULARIZE option makes sense to use. It wraps the entire JavaScript output in a function, and returns a Promise which resolves to the initialized Module instance.
var instance;
Module().then(module => {
instance = module;
});
The promise is resolved when it is safe to run compiled code, i.e. after it has been has been downloaded and instantiated. The promise is resolved at the same time the onRuntimeInitialized callback is invoked, so there’s no need to use onRuntimeInitialized when using MODULARIZE.
You can use the EXPORT_NAME option to change Module to something else. This is good practice for libraries, as then they don’t include unnecessary things in the global scope, and in some cases you want to create more than one.
Once binding is complete, C++ objects can be created and used in JavaScript as though they were normal JavaScript objects. For example, continuing the above example, you can create the Foo
and Bar
objects and call methods on them.
var f = new Module.Foo();
f.setVal(200);
alert(f.getVal());
var b = new Module.Bar(123);
b.doSomething();
Important
Always access objects through the Module object object, as shown above.
While the objects are also available in the global namespace by default, there are cases where they will not be (for example, if you use the closure compiler to minify code or wrap compiled code in a function to avoid polluting the global namespace). You can of course use whatever name you like for the module by assigning it to a new variable: var MyModuleName = Module;
.
Important
You can only use this code when it is safe to call compiled code, see more details in that FAQ entry.
JavaScript will automatically garbage collect any of the wrapped C++ objects when there are no more references. If the C++ object doesn’t require specific clean up (i.e. it doesn’t have a destructor) then no other action needs to be taken.
If a C++ object does need to be cleaned up, you must explicitly call Module.destroy(obj)
to invoke its destructor — then drop all references to the object so that it can be garbage collected. For example, if Bar
were to allocate memory that requires cleanup:
var b = new Module.Bar(123);
b.doSomething();
Module.destroy(b); // If the C++ object requires clean up
Note
The C++ constructor is called transparently when a C++ object is created in JavaScript. There is no way, however, to tell if a JavaScript object is about to be garbage collected, so the binder glue code can’t automatically call the destructor.
You will usually need to destroy the objects which you create, but this depends on the library being ported.
Object attributes are defined in IDL using the attribute
keyword. These can then be accessed in JavaScript using either get_foo()
/set_foo()
accessor methods, or directly as a property of the object.
// C++
int attr;
// WebIDL
attribute long attr;
// JavaScript
var f = new Module.Foo();
f.attr = 7;
// Equivalent to:
f.set_attr(7);
console.log(f.attr);
console.log(f.get_attr());
For read-only attributes, see Const.
C++ arguments and return types can be pointers, references, or value types (allocated on the stack). The IDL file uses different decoration to represent each of these cases.
Undecorated argument and return values of a custom type in the IDL are assumed to be pointers in the C++:
// C++
MyClass* process(MyClass* input);
// WebIDL
MyClass process(MyClass input);
This assumption isn’t true for base types like void,int,bool,DOMString,etc.
References should be decorated using [Ref]
:
// C++
MyClass& process(MyClass& input);
// WebIDL
[Ref] MyClass process([Ref] MyClass input);
Note
If [Ref]
is omitted on a reference, the generated glue C++ will not compile (it fails when it tries to convert the reference — which it thinks is a pointer — to an object).
If the C++ returns an object (rather than a reference or a pointer) then the return type should be decorated using [Value]
. This will allocate a static (singleton) instance of that class and return it. You should use it immediately, and drop any references to it after use.
// C++
MyClass process(MyClass& input);
// WebIDL
[Value] MyClass process([Ref] MyClass input);
C++ arguments or return types that use const
can be specified in IDL using [Const]
.
For example, the following code fragments show the C++ and IDL for a function that returns a constant pointer object.
//C++
const myObject* getAsConst();
// WebIDL
[Const] myObject getAsConst();
Attributes that correspond to const data members must be specified with the readonly
keyword, not with [Const]
. For example:
//C++
const int numericalConstant;
// WebIDL
readonly attribute long numericalConstant;
This will generate a get_numericalConstant()
method in the bindings, but not a corresponding setter. The attribute will also be defined as read-only in JavaScript, meaning that trying to set it will have no effect on the value, and will throw an error in strict mode.
Tip
It is possible for a return type to have multiple specifiers. For example, a method that returns a constant reference would be marked up in the IDL using [Ref, Const]
.
If a class cannot be deleted (because the destructor is private), specify [NoDelete]
in the IDL file.
[NoDelete]
interface Foo {
...
};
C++ classes that are declared inside a namespace (or another class) must use the IDL file Prefix
keyword to specify the scope. The prefix is then used whenever the class is referred to in C++ glue code.
For example, the following IDL definition ensures that Inner
class is referred to as MyNameSpace::Inner
[Prefix="MyNameSpace::"]
interface Inner {
..
};
You can bind to C++ operators using [Operator=]
:
[Operator="+="] TYPE1 add(TYPE2 x);
Note
The operator name can be anything (add
is just an example).
Support is currently limited to operators that contain =
: +=
, *=
, -=
etc., and to the array indexing operator []
.
Enums are declared very similarly in C++ and IDL:
// C++
enum AnEnum {
enum_value1,
enum_value2
};
// WebIDL
enum AnEnum {
"enum_value1",
"enum_value2"
};
The syntax is slightly more complicated for enums declared inside a namespace:
// C++
namespace EnumNamespace {
enum EnumInNamespace {
e_namespace_val = 78
};
};
// WebIDL
enum EnumNamespace_EnumInNamespace {
"EnumNamespace::e_namespace_val"
};
When the enum is defined inside a class, the IDL definitions for the enum and class interface are separate:
// C++
class EnumClass {
public:
enum EnumWithinClass {
e_val = 34
};
EnumWithinClass GetEnum() { return e_val; }
EnumNamespace::EnumInNamespace GetEnumFromNameSpace() { return EnumNamespace::e_namespace_val; }
};
// WebIDL
enum EnumClass_EnumWithinClass {
"EnumClass::e_val"
};
interface EnumClass {
void EnumClass();
EnumClass_EnumWithinClass GetEnum();
EnumNamespace_EnumInNamespace GetEnumFromNameSpace();
};
The WebIDL Binder allows C++ base classes to be sub-classed in JavaScript. In the IDL fragment below, JSImplementation="Base"
means that the associated interface (ImplJS
) will be a JavaScript implementation of the C++ class Base
.
[JSImplementation="Base"]
interface ImplJS {
void ImplJS();
void virtualFunc();
void virtualFunc2();
};
After running the bindings generator and compiling, you can implement the interface in JavaScript as shown:
var c = new ImplJS();
c.virtualFunc = function() { .. };
When C++ code has a pointer to a Base
instance and calls virtualFunc()
, that call will reach the JavaScript code defined above.
Note
You must implement all the methods you mentioned in the IDL of the JSImplementation
class (ImplJS
) or compilation will fail with an error.
You will also need to provide an interface definition for the Base
class in the IDL file.
All the binding functions expect to receive wrapper objects (which contain a raw pointer) rather than a raw pointer. You shouldn’t normally need to deal with raw pointers (these are simply memory addresses/integers). If you do, the following functions in the compiled code can be useful:
wrapPointer(ptr, Class)
— Given a raw pointer (an integer), returns a wrapped object.
Note
If you do not pass the Class
, it will be assumed to be the root class — this probably isn’t what you want!
getPointer(object)
— Returns a raw pointer.
castObject(object, Class)
— Returns a wrapping of the same pointer but to another class.
compare(object1, object2)
— Compares two objects’ pointers.
Note
There is always a single wrapped object for a certain pointer to a certain class. This allows you to add data on that object and use it elsewhere using normal JavaScript syntax (object.attribute = someData
etc.)
compare()
should be used instead of direct pointer comparison because it is possible to have different wrapped objects with the same pointer if one class is a subclass of the other.
All the binding functions that return pointers, references, or objects will return wrapped pointers. The reason is that by always returning a wrapper, you can take the output and pass it to another binding function without that function needing to check the type of the argument.
One case where this can be confusing is when returning a NULL
pointer. When using bindings, the returned pointer will be NULL
(a global singleton with a wrapped pointer of 0) rather than null
(the JavaScript built-in object) or 0.
The void*
type is supported through a VoidPtr
type that you can use in IDL files. You can also use the any
type.
The difference between them is that VoidPtr
behaves like a pointer type in that you get a wrapper object, while any
behaves like a 32-bit integer (which is what raw pointers are in Emscripten-compiled code).
The type names in WebIDL are not identical to those in C++. This section shows the mapping for the more common types you’ll encounter.
C++ |
IDL |
---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Note
The WebIDL types are fully documented in this W3C specification.
For a complete working example, see test_webidl in the test suite. The test suite code is guaranteed to work and covers more cases than this article alone.
Another good example is ammo.js, which uses the WebIDL Binder to port the Bullet Physics engine to the Web.