WebIDL Binder

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.

备注

An alternative to the WebIDL Binder is to use Embind. For more information see Binding C++ and JavaScript — WebIDL Binder and Embind.

A quick example

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.

Defining the WebIDL file

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 (see Foo above).

  • The type names in WebIDL are not identical to those in C++ (for example, int maps to long above). For more information about the mappings see WebIDL types.

备注

structs are defined in the same way as the classes above — using the interface keyword.

Generating the bindings glue code

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

Compiling the project (using the bindings glue code)

To use the glue code files (glue.cpp and glue.js) in a project:

  1. 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.

  2. 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>

备注

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.

  1. 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.

Modular output

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.

Using C++ classes in JavaScript

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();

重要

Always access objects through the 模块对象 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;.

重要

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

备注

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.

Attributes

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.

Pointers, References, Value types (Ref and Value)

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);

备注

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);

Const

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.

小技巧

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].

Un-deletable classes (NoDelete)

If a class cannot be deleted (because the destructor is private), specify [NoDelete] in the IDL file.

[NoDelete]
interface Foo {
...
};

Defining inner classes and classes inside namespaces (Prefix)

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 {
..
};

Operators

You can bind to C++ operators using [Operator=]:

[Operator="+="] TYPE1 add(TYPE2 x);

备注

  • 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

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();
};

Sub-classing C++ base classes in JavaScript (JSImplementation)

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.

备注

  • 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.

Pointers and comparisons

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.

    备注

    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.

备注

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.

NULL

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.

void*

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).

WebIDL types

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

bool

boolean

float

float

double

double

char

byte

char*

DOMString (represents a JavaScript string)

unsigned char

octet

int

long

long

long

unsigned short

unsigned short

unsigned long

unsigned long

long long

long long

void

void

void*

any or VoidPtr (see void*)

备注

The WebIDL types are fully documented in this W3C specification.

Test and example code

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.