Embind

Embind 用于将 C++ 函数和类绑定到 JavaScript,以便编译后的代码可以以“普通”JavaScript 的自然方式使用。Embind 还支持 从 C++ 调用 JavaScript 类.

Embind 支持绑定大多数 C++ 结构,包括 C++11 和 C++14 中引入的结构。它唯一的重大限制是目前不支持 具有复杂生命周期语义的原始指针.

本文介绍如何使用 EMSCRIPTEN_BINDINGS() 块为函数、类、值类型、指针(包括原始指针和智能指针)、枚举和常量创建绑定,以及如何为可以在 JavaScript 中重写的抽象类创建绑定。它还简要解释了如何管理传递给 JavaScript 的 C++ 对象句柄的内存。

提示

除了本文中的代码

注意

Embind 的灵感来自 Boost.Python,并使用非常类似的方法来定义绑定。

一个简单的示例

以下代码使用 EMSCRIPTEN_BINDINGS() 块将简单的 C++ lerp() function() 公开到 JavaScript。

// quick_example.cpp
#include <emscripten/bind.h>

using namespace emscripten;

float lerp(float a, float b, float t) {
    return (1 - t) * a + t * b;
}

EMSCRIPTEN_BINDINGS(my_module) {
    function("lerp", &lerp);
}

要使用 embind 编译上述示例,我们需要使用 bind 选项调用 emcc

emcc -lembind -o quick_example.js quick_example.cpp

生成的 quick_example.js 文件可以作为节点模块加载,也可以通过 <script> 标记加载

<!doctype html>
<html>
  <script>
    var Module = {
      onRuntimeInitialized: function() {
        console.log('lerp result: ' + Module.lerp(1, 2, 0.5));
      }
    };
  </script>
  <script src="quick_example.js"></script>
</html>

注意

我们使用 onRuntimeInitialized 回调在运行时准备就绪时运行代码,这是一个异步操作(为了编译 WebAssembly)。

注意

打开开发者工具控制台以查看 console.log 的输出。

EMSCRIPTEN_BINDINGS() 块中的代码在 JavaScript 文件最初加载时运行(与全局构造函数同时)。embind 会自动推断函数 lerp() 的参数类型和返回类型。

embind 公开的所有符号都可以在 Emscripten Module 对象上找到。

重要

始终如上所示通过 Module 对象 对象访问对象。

虽然这些对象默认情况下也存在于全局命名空间中,但在某些情况下它们将不存在(例如,如果你使用 Closure 编译器 来压缩代码或将编译后的代码包装在一个函数中以避免污染全局命名空间)。当然,你可以使用你喜欢的任何名称来命名模块,方法是将其分配给一个新变量:var MyModuleName = Module;

绑定库

绑定代码作为静态构造函数运行,静态构造函数只有在包含目标文件时才运行,因此在为库文件生成绑定时,编译器必须明确指示包含目标文件。

例如,要为使用 Emscripten 编译的假设 library.a 生成绑定,请使用 --whole-archive 编译器标志调用 emcc

emcc -lembind -o library.js -Wl,--whole-archive library.a -Wl,--no-whole-archive

将类公开到 JavaScript 需要更复杂的绑定语句。例如

class MyClass {
public:
  MyClass(int x, std::string y)
    : x(x)
    , y(y)
  {}

  void incrementX() {
    ++x;
  }

  int getX() const { return x; }
  void setX(int x_) { x = x_; }

  static std::string getStringFromInstance(const MyClass& instance) {
    return instance.y;
  }

private:
  int x;
  std::string y;
};

// Binding code
EMSCRIPTEN_BINDINGS(my_class_example) {
  class_<MyClass>("MyClass")
    .constructor<int, std::string>()
    .function("incrementX", &MyClass::incrementX)
    .property("x", &MyClass::getX, &MyClass::setX)
    .property("x_readonly", &MyClass::getX)
    .class_function("getStringFromInstance", &MyClass::getStringFromInstance)
    ;
}

绑定块在临时 class_ 对象上定义了一系列成员函数调用(这种样式与 Boost.Python 中使用的样式相同)。这些函数注册类、它的 constructor()、成员 function()class_function()(静态)和 property().

注意

此绑定块绑定了该类及其所有方法。通常情况下,你应该只绑定真正需要的项,因为每个绑定都会增加代码大小。例如,很少会绑定私有方法或内部方法。

然后,可以在 JavaScript 中创建 MyClass 的实例并使用它,如下所示

var instance = new Module.MyClass(10, "hello");
instance.incrementX();
instance.x; // 11
instance.x = 20; // 20
Module.MyClass.getStringFromInstance(instance); // "hello"
instance.delete();

注意

Closure 编译器 无法识别通过 Embind 公开到 JavaScript 的符号的名称。为了防止 Closure 编译器在你的代码(例如通过使用 --pre-js--post-js 编译器标志)中重命名这些符号,需要对代码进行相应的注释。如果没有这些注释,生成的 JavaScript 代码将不再与 Embind 代码中使用的符号名称匹配,从而导致运行时错误。

为了防止 Closure 编译器重命名上述示例代码中的符号,需要将其重写如下

var instance = new Module["MyClass"](10, "hello");
instance["incrementX"]();
instance["x"]; // 11
instance["x"] = 20; // 20
Module["MyClass"]["getStringFromInstance"](instance); // "hello"
instance.delete();

请注意,这仅适用于优化器可以看到的代码,例如上面提到的 --pre-js--post-js 中的代码,或在 EM_ASMEM_JS 上。对于其他不受 Closure 编译器优化的代码,则不需要进行这些更改。如果你在没有 --closure 1 的情况下进行构建以启用 Closure 编译器,也不需要进行这些更改。

内存管理

提供 delete() JavaScript 方法来手动指示不再需要 C++ 对象并可以删除它

var x = new Module.MyClass;
x.method();
x.delete();

var y = Module.myFunctionThatReturnsClassInstance();
y.method();
y.delete();

注意

从 JavaScript 侧构造的 C++ 对象以及从 C++ 方法返回的 C++ 对象必须显式删除,除非使用了 reference 返回值策略(见下文)。

提示

可以利用 tryfinally JavaScript 结构来确保所有代码路径都能删除 C++ 对象句柄,无论是否提前返回或抛出错误。

function myFunction() {
    const x = new Module.MyClass;
    try {
        if (someCondition) {
            return; // !
        }
        someFunctionThatMightThrow(); // oops
        x.method();
    } finally {
        x.delete(); // will be called no matter what
    }
}

自动内存管理

JavaScript 直到 ECMAScript 2021 或 ECMA-262 版本 12 才开始支持 终结器。新的 API 称为 FinalizationRegistry,它仍然不能保证提供的终结回调会被调用。Embind 在可用时会将此用于清理,但仅用于智能指针,并且仅作为最后的手段。

警告

强烈建议 JavaScript 代码显式删除其收到的所有 C++ 对象句柄。

克隆和引用计数

在某些情况下,JavaScript 代码库的多个长期存在的部分需要在不同的时间段内保留对同一个 C++ 对象的引用。

为了适应这种情况,Emscripten 提供了一种 引用计数 机制,其中可以为同一个底层 C++ 对象生成多个句柄。只有当所有句柄都被删除时,该对象才会被销毁。

JavaScript clone() 方法返回一个新句柄。它最终也必须使用 delete() 进行处理

async function myLongRunningProcess(x, milliseconds) {
    // sleep for the specified number of milliseconds
    await new Promise(resolve => setTimeout(resolve, milliseconds));
    x.method();
    x.delete();
}

const y = new Module.MyClass;          // refCount = 1
myLongRunningProcess(y.clone(), 5000); // refCount = 2
myLongRunningProcess(y.clone(), 3000); // refCount = 3
y.delete();                            // refCount = 2

// (after 3000ms) refCount = 1
// (after 5000ms) refCount = 0 -> object is deleted

值类型

对基本类型进行手动内存管理非常繁琐,因此embind提供对值类型的支持。 Value arrays 被转换为 JavaScript 数组,反之亦然,而 value objects 被转换为 JavaScript 对象,反之亦然。

考虑下面的例子

struct Point2f {
    float x;
    float y;
};

struct PersonRecord {
    std::string name;
    int age;
};

// Array fields are treated as if they were std::array<type,size>
struct ArrayInStruct {
    int field[2];
};

PersonRecord findPersonAtLocation(Point2f);

EMSCRIPTEN_BINDINGS(my_value_example) {
    value_array<Point2f>("Point2f")
        .element(&Point2f::x)
        .element(&Point2f::y)
        ;

    value_object<PersonRecord>("PersonRecord")
        .field("name", &PersonRecord::name)
        .field("age", &PersonRecord::age)
        ;

    value_object<ArrayInStruct>("ArrayInStruct")
        .field("field", &ArrayInStruct::field) // Need to register the array type
        ;

    // Register std::array<int, 2> because ArrayInStruct::field is interpreted as such
    value_array<std::array<int, 2>>("array_int_2")
        .element(index<0>())
        .element(index<1>())
        ;

    function("findPersonAtLocation", &findPersonAtLocation);
}

JavaScript 代码不需要担心生命周期管理。

var person = Module.findPersonAtLocation([10.2, 156.5]);
console.log('Found someone! Their name is ' + person.name + ' and they are ' + person.age + ' years old');

高级类概念

对象所有权

JavaScript 和 C++ 具有截然不同的内存模型,这会导致在对象在两种语言之间移动时,不清楚哪种语言拥有对象并负责删除它。为了使对象所有权更加明确,embind 支持智能指针和返回值策略。返回值策略规定了 C++ 对象在返回到 JavaScript 时会发生什么。

要使用返回值策略,请将所需的策略传递到函数、方法或属性绑定中。例如

EMSCRIPTEN_BINDINGS(module) {
  function("createData", &createData, return_value_policy::take_ownership());
}

Embind 支持三种返回值策略,它们的行为根据函数的返回类型而不同。这些策略的工作原理如下

  • 默认(无参数) - 对于按值和引用返回,将使用对象的复制构造函数分配一个新对象。然后 JS 拥有该对象并负责删除它。默认情况下不允许返回指针(请使用下面的显式策略)。

  • return_value_policy::take_ownership - 所有权被转移到 JS。

  • return_value_policy::reference - 引用现有对象,但不获取所有权。必须注意在 JS 中仍然使用该对象时不要删除它。

更多详细信息见下文

返回类型

构造函数

清理

默认

值 (T)

复制

JS 必须删除复制的对象。

引用 (T&)

复制

JS 必须删除复制的对象。

指针 (T*)

n/a

指针必须显式地使用返回值策略。

take_ownership

值 (T)

移动

JS 必须删除移动的对象。

引用 (T&)

移动

JS 必须删除移动的对象。

指针 (T*)

JS 必须删除该对象。

reference

值 (T)

n/a

不允许引用值。

引用 (T&)

C++ 必须删除该对象。

指针 (T*)

C++ 必须删除该对象。

原始指针

由于原始指针具有不确定的生命周期语义,因此embind 要求使用 allow_raw_pointersreturn_value_policy 来标记它们的使用。如果函数返回指针,建议使用 return_value_policy 而不是通用的 allow_raw_pointers

例如

class C {};
C* passThrough(C* ptr) { return ptr; }
C* createC() { return new C(); }
EMSCRIPTEN_BINDINGS(raw_pointers) {
    class_<C>("C");
    function("passThrough", &passThrough, allow_raw_pointers());
    function("createC", &createC, return_value_policy::take_ownership());
}

注意

目前,对于指针参数的 allow_raw_pointers 仅用于允许使用原始指针,并表明您已经考虑过原始指针的使用。最终,我们希望实现 类似 Boost.Python 的原始指针策略,以便管理参数的对象所有权。

外部构造函数

有两种方法可以为类指定构造函数。

The 零参数模板形式 使用模板中指定的参数调用自然构造函数。例如

class MyClass {
public:
  MyClass(int, float);
  void someFunction();
};

EMSCRIPTEN_BINDINGS(external_constructors) {
  class_<MyClass>("MyClass")
    .constructor<int, float>()
    .function("someFunction", &MyClass::someFunction)
    ;
}

The 构造函数的第二种形式 接受函数指针参数,用于使用工厂函数构造自身的类。例如

class MyClass {
  virtual void someFunction() = 0;
};
MyClass* makeMyClass(int, float); //Factory function.

EMSCRIPTEN_BINDINGS(external_constructors) {
  class_<MyClass>("MyClass")
    .constructor(&makeMyClass, allow_raw_pointers())
    .function("someFunction", &MyClass::someFunction)
    ;
}

两种构造函数在 JavaScript 中构造对象时呈现完全相同的接口。继续上面的例子

var instance = new MyClass(10, 15.5);
// instance is backed by a raw pointer to a MyClass in the Emscripten heap

智能指针

为了使用智能指针管理对象生命周期,embind 必须了解智能指针类型。

例如,考虑使用 std::shared_ptr<C> 来管理类 C 的生命周期。最好的方法是使用 smart_ptr_constructor() 来注册智能指针类型

EMSCRIPTEN_BINDINGS(better_smart_pointers) {
    class_<C>("C")
        .smart_ptr_constructor("C", &std::make_shared<C>)
        ;
}

当构造此类型的对象时(例如使用 new Module.C()),它将返回一个 std::shared_ptr<C>

另一种方法是在 EMSCRIPTEN_BINDINGS() 块中使用 smart_ptr()

EMSCRIPTEN_BINDINGS(smart_pointers) {
    class_<C>("C")
        .constructor<>()
        .smart_ptr<std::shared_ptr<C>>("C")
        ;
}

使用此定义,函数可以返回 std::shared_ptr<C> 或将 std::shared_ptr<C> 作为参数,但 new Module.C() 仍然会返回一个原始指针。

unique_ptr

embind 内置支持类型为 std::unique_ptr 的返回值。

自定义智能指针

为了让embind 了解自定义智能指针模板,您必须专门化 smart_ptr_trait 模板。

JavaScript 原型上的非成员函数

JavaScript 类原型上的方法可以是非成员函数,只要实例句柄可以转换为非成员函数的第一个参数即可。最典型的例子是,暴露给 JavaScript 的函数的行为与 C++ 方法的行为不完全一致。

struct Array10 {
    int& get(size_t index) {
        return data[index];
    }
    int data[10];
};

val Array10_get(Array10& arr, size_t index) {
    if (index < 10) {
        return val(arr.get(index));
    } else {
        return val::undefined();
    }
}

EMSCRIPTEN_BINDINGS(non_member_functions) {
    class_<Array10>("Array10")
        .function("get", &Array10_get)
        ;
}

如果 JavaScript 使用无效索引调用 Array10.prototype.get,它将返回 undefined

在 JavaScript 中从 C++ 类派生

如果 C++ 类具有虚函数或抽象成员函数,则可以在 JavaScript 中覆盖它们。由于 JavaScript 不了解 C++ 的 vtable,因此embind 需要一些胶水代码将 C++ 虚函数调用转换为 JavaScript 调用。

抽象方法

让我们从一个简单的例子开始:必须在 JavaScript 中实现的纯虚函数。

struct Interface {
    virtual ~Interface() {}
    virtual void invoke(const std::string& str) = 0;
};

struct InterfaceWrapper : public wrapper<Interface> {
    EMSCRIPTEN_WRAPPER(InterfaceWrapper);
    void invoke(const std::string& str) {
        return call<void>("invoke", str);
    }
};

EMSCRIPTEN_BINDINGS(interface) {
    class_<Interface>("Interface")
        .function("invoke", &Interface::invoke, pure_virtual())
        .allow_subclass<InterfaceWrapper>("InterfaceWrapper")
        ;
}

allow_subclass() 在 Interface 绑定中添加了两个特殊方法:extendimplementextend 允许 JavaScript 以 Backbone.js 为例的方式进行子类化。 implement 用于当您有一个 JavaScript 对象时,它可能是由浏览器或其他库提供的,并且您想使用它来实现 C++ 接口。

注意

The pure_virtual 函数绑定的注释允许 JavaScript 在 JavaScript 类没有覆盖 invoke() 时抛出一个有用的错误。否则,您可能会遇到令人困惑的错误。

extend 示例

var DerivedClass = Module.Interface.extend("Interface", {
    // __construct and __destruct are optional.  They are included
    // in this example for illustration purposes.
    // If you override __construct or __destruct, don't forget to
    // call the parent implementation!
    __construct: function() {
        this.__parent.__construct.call(this);
    },
    __destruct: function() {
        this.__parent.__destruct.call(this);
    },
    invoke: function() {
        // your code goes here
    },
});

var instance = new DerivedClass;

implement 示例

var x = {
    invoke: function(str) {
        console.log('invoking with: ' + str);
    }
};
var interfaceObject = Module.Interface.implement(x);

现在 interfaceObject 可以传递给任何接受 Interface 指针或引用的函数。

非抽象虚方法

如果 C++ 类具有非纯虚函数,则可以覆盖它,但不是必须覆盖它。这需要一个略有不同的包装器实现

struct Base {
    virtual void invoke(const std::string& str) {
        // default implementation
    }
};

struct BaseWrapper : public wrapper<Base> {
    EMSCRIPTEN_WRAPPER(BaseWrapper);
    void invoke(const std::string& str) {
        return call<void>("invoke", str);
    }
};

EMSCRIPTEN_BINDINGS(interface) {
    class_<Base>("Base")
        .allow_subclass<BaseWrapper>("BaseWrapper")
        .function("invoke", optional_override([](Base& self, const std::string& str) {
            return self.Base::invoke(str);
        }))
        ;
}

当使用 JavaScript 对象实现 Base 时,覆盖 invoke 是可选的。对于 invoke 的特殊 lambda 绑定是必要的,以避免包装器和 JavaScript 之间的无限相互递归。

基类

基类绑定定义如下所示

EMSCRIPTEN_BINDINGS(base_example) {
    class_<BaseClass>("BaseClass");
    class_<DerivedClass, base<BaseClass>>("DerivedClass");
}

BaseClass 上定义的任何成员函数都可以访问 DerivedClass 的实例。此外,任何接受 BaseClass 实例的函数都可以接受 DerivedClass 的实例。

自动向下转型

如果 C++ 类是多态的(即它具有虚方法),那么embind 支持对函数返回值的自动向下转型。

class Base { virtual ~Base() {} }; // the virtual makes Base and Derived polymorphic
class Derived : public Base {};
Base* getDerivedInstance() {
    return new Derived;
}
EMSCRIPTEN_BINDINGS(automatic_downcasting) {
    class_<Base>("Base");
    class_<Derived, base<Base>>("Derived");
    function("getDerivedInstance", &getDerivedInstance, allow_raw_pointers());
}

从 JavaScript 调用 Module.getDerivedInstance 将返回一个 Derived 实例句柄,从中可以访问所有 Derived 的方法。

注意

Embind 必须了解完全派生的类型才能使自动向下转型起作用。

注意

Embind 仅在启用 RTTI 时才支持此功能。

重载函数

构造函数和函数可以根据参数数量进行重载,但embind 不支持根据类型进行重载。在指定重载时,请使用 select_overload() 帮助函数来选择合适的签名。

struct HasOverloadedMethods {
    void foo();
    void foo(int i);
    void foo(float f) const;
};

EMSCRIPTEN_BINDING(overloads) {
    class_<HasOverloadedMethods>("HasOverloadedMethods")
        .function("foo", select_overload<void()>(&HasOverloadedMethods::foo))
        .function("foo_int", select_overload<void(int)>(&HasOverloadedMethods::foo))
        .function("foo_float", select_overload<void(float)const>(&HasOverloadedMethods::foo))
        ;
}

枚举

Embindenumeration support 支持 C++98 枚举和 C++11 的“枚举类”。

enum OldStyle {
    OLD_STYLE_ONE,
    OLD_STYLE_TWO
};

enum class NewStyle {
    ONE,
    TWO
};

EMSCRIPTEN_BINDINGS(my_enum_example) {
    enum_<OldStyle>("OldStyle")
        .value("ONE", OLD_STYLE_ONE)
        .value("TWO", OLD_STYLE_TWO)
        ;
    enum_<NewStyle>("NewStyle")
        .value("ONE", NewStyle::ONE)
        .value("TWO", NewStyle::TWO)
        ;
}

两种情况下,JavaScript 都可以通过类型的属性访问枚举值。

Module.OldStyle.ONE;
Module.NewStyle.TWO;

常量

要将 C++ constant() 暴露给 JavaScript,只需编写

EMSCRIPTEN_BINDINGS(my_constant_example) {
    constant("SOME_CONSTANT", SOME_CONSTANT);
}

SOME_CONSTANT 可以是 embind 支持的任何类型。

类属性

警告

默认情况下,property() 对对象的绑定使用 return_value_policy::copy,这很容易导致内存泄漏,因为每次访问属性都会创建一个需要删除的新对象。或者,使用 return_value_policy::reference,这样就不会分配新对象,并且对对象的更改将反映在原始对象中。

类属性可以像下面这样用几种方法定义。

struct Point {
    float x;
    float y;
};

struct Person {
    Point location;
    Point getLocation() const { // Note: const is required on getters
        return location;
    }
    void setLocation(Point p) {
        location = p;
    }
};

EMSCRIPTEN_BINDINGS(xxx) {
    class_<Person>("Person")
        .constructor<>()
        // Bind directly to a class member with automatically generated getters/setters using a
        // reference return policy so the object does not need to be deleted JS.
        .property("location", &Person::location, return_value_policy::reference())
        // Same as above, but this will return a copy and the object must be deleted or it will
        // leak!
        .property("locationCopy", &Person::location)
        // Bind using a only getter method for read only access.
        .property("readOnlyLocation", &Person::getLocation, return_value_policy::reference())
        // Bind using a getter and setter method.
        .property("getterAndSetterLocation", &Person::getLocation, &Person::setLocation,
                  return_value_policy::reference());
    class_<Point>("Point")
        .property("x", &Point::x)
        .property("y", &Point::y);
}

int main() {
    EM_ASM(
        let person = new Module.Person();
        person.location.x = 42;
        console.log(person.location.x); // 42
        let locationCopy = person.locationCopy;
        // This is a copy so the original person's location will not be updated.
        locationCopy.x = 99;
        console.log(locationCopy.x); // 99
        // Important: delete any copies!
        locationCopy.delete();
        console.log(person.readOnlyLocation.x); // 42
        console.log(person.getterAndSetterLocation.x); // 42
        person.delete();
    );
}

内存视图

在某些情况下,将原始二进制数据直接暴露给 JavaScript 代码作为类型化数组很有价值,允许在不复制的情况下使用它。例如,这对于将大型 WebGL 纹理直接从堆上传很有用。

内存视图应像原始指针一样对待;运行时不会管理生存期和有效性,如果修改或释放底层对象,则很容易损坏数据。

#include <emscripten/bind.h>
#include <emscripten/val.h>

using namespace emscripten;

unsigned char *byteBuffer = /* ... */;
size_t bufferLength = /* ... */;

val getBytes() {
    return val(typed_memory_view(bufferLength, byteBuffer));
}

EMSCRIPTEN_BINDINGS(memory_view_example) {
    function("getBytes", &getBytes);
}

调用 JavaScript 代码将收到 emscripten 堆中的类型化数组视图

var myUint8Array = Module.getBytes()
var xhr = new XMLHttpRequest();
xhr.open('POST', /* ... */);
xhr.send(myUint8Array);

类型化数组视图将是适当匹配的类型,例如,对于 unsigned char 数组或指针,使用 Uint8Array。

使用 val 将 JavaScript 转译为 C++

Embind 提供了一个 C++ 类,emscripten::val,你可以用它将 JavaScript 代码转译为 C++。使用 val,你可以从 C++ 中调用 JavaScript 对象,读取和写入它们的属性,或者将它们强制转换为 C++ 值,例如 boolintstd::string

下面的示例展示了如何使用 val 从 C++ 中调用 JavaScript 的 Web Audio API

注意

此示例基于优秀的 Web Audio 教程:Making sine, square, sawtooth and triangle waves (stuartmemo.com)。在 emscripten::val 文档中有一个更简单的示例。

首先考虑下面的 JavaScript,它展示了如何使用 API

// Get web audio api context
var AudioContext = window.AudioContext || window.webkitAudioContext;

// Got an AudioContext: Create context and OscillatorNode
var context = new AudioContext();
var oscillator = context.createOscillator();

// Configuring oscillator: set OscillatorNode type and frequency
oscillator.type = 'triangle';
oscillator.frequency.value = 261.63; // value in hertz - middle C

// Playing
oscillator.connect(context.destination);
oscillator.start();

// All done!

可以使用 val 将代码转译为 C++,如下所示

#include <emscripten/val.h>
#include <stdio.h>
#include <math.h>

using namespace emscripten;

int main() {
  val AudioContext = val::global("AudioContext");
  if (!AudioContext.as<bool>()) {
    printf("No global AudioContext, trying webkitAudioContext\n");
    AudioContext = val::global("webkitAudioContext");
  }

  printf("Got an AudioContext\n");
  val context = AudioContext.new_();
  val oscillator = context.call<val>("createOscillator");

  printf("Configuring oscillator\n");
  oscillator.set("type", val("triangle"));
  oscillator["frequency"].set("value", val(261.63)); // Middle C

  printf("Playing\n");
  oscillator.call<void>("connect", context["destination"]);
  oscillator.call<void>("start", 0);

  printf("All done!\n");
}

首先,我们使用 global() 获取全局 AudioContext 对象(或 webkitAudioContext,如果不存在)。然后,我们使用 new_() 创建上下文,并从该上下文中我们可以创建 oscillator,使用 set() 设置其属性(再次使用 val),然后播放音调。

可以在 Linux/macOS 终端上使用以下命令编译示例

emcc -O2 -Wall -Werror -lembind -o oscillator.html oscillator.cpp

内置类型转换

开箱即用,embind 为许多标准 C++ 类型提供转换器

C++ 类型

JavaScript 类型

void

undefined

bool

true 或 false

char

Number

signed char

Number

unsigned char

Number

short

Number

unsigned short

Number

int

Number

unsigned int

Number

long

Number,或 BigInt*

unsigned long

Number,或 BigInt*

float

Number

double

Number

int64_t

BigInt**

uint64_t

BigInt**

std::string

ArrayBuffer、Uint8Array、Uint8ClampedArray、Int8Array 或 String

std::wstring

String(UTF-16 代码单元)

emscripten::val

任何东西

*使用 MEMORY64 时为 BigInt,否则为 Number。

**需要使用 -sWASM_BIGINT 标志启用 BigInt 支持。**

为了方便起见,embind 提供了工厂函数来注册 std::vector<T> (register_vector())、std::map<K, V> (register_map()) 和 std::optional<T> (register_optional()) 类型

EMSCRIPTEN_BINDINGS(stl_wrappers) {
    register_vector<int>("VectorInt");
    register_map<int,int>("MapIntInt");
    register_optional<std::string>();
}

下面是一个完整的示例

#include <emscripten/bind.h>
#include <string>
#include <vector>
#include <optional>

using namespace emscripten;

std::vector<int> returnVectorData () {
  std::vector<int> v(10, 1);
  return v;
}

std::map<int, std::string> returnMapData () {
  std::map<int, std::string> m;
  m.insert(std::pair<int, std::string>(10, "This is a string."));
  return m;
}

std::optional<std::string> returnOptionalData() {
  return "hello";
}

EMSCRIPTEN_BINDINGS(module) {
  function("returnVectorData", &returnVectorData);
  function("returnMapData", &returnMapData);
  function("returnOptionalData", &returnOptionalData);

  // register bindings for std::vector<int>, std::map<int, std::string>, and
  // std::optional<std::string>.
  register_vector<int>("vector<int>");
  register_map<int, std::string>("map<int, string>");
  register_optional<std::string>();
}

以下 JavaScript 可用于与上述 C++ 交互。

var retVector = Module['returnVectorData']();

// vector size
var vectorSize = retVector.size();

// reset vector value
retVector.set(vectorSize - 1, 11);

// push value into vector
retVector.push_back(12);

// retrieve value from the vector
for (var i = 0; i < retVector.size(); i++) {
    console.log("Vector Value: ", retVector.get(i));
}

// expand vector size
retVector.resize(20, 1);

var retMap = Module['returnMapData']();

// map size
var mapSize = retMap.size();

// retrieve value from map
console.log("Map Value: ", retMap.get(10));

// figure out which map keys are available
// NB! You must call `register_vector<key_type>`
// to make vectors available
var mapKeys = retMap.keys();
for (var i = 0; i < mapKeys.size(); i++) {
    var key = mapKeys.get(i);
    console.log("Map key/value: ", key, retMap.get(key));
}

// reset the value at the given index position
retMap.set(10, "OtherValue");

// Optional values will return undefined if there is no value.
var optional = Module['returnOptionalData']();
if (optional !== undefined) {
    console.log(optional);
}

TypeScript 定义

生成

Embind 支持从 EMSCRIPTEN_BINDINGS() 块中生成 TypeScript 定义文件。要生成 .d.ts 文件,请使用 embind-emit-tsd 选项调用 emcc

emcc -lembind quick_example.cpp --emit-tsd interface.d.ts

运行此命令将使用一个带工具的 embind 版本构建程序,然后在 node 中运行以生成定义文件。目前并非所有 embind 的功能都受支持,但其中许多常用功能都受支持。输入和输出示例可以在 embind_tsgen.cppembind_tsgen.d.ts 中找到。

自定义 val 定义

emscripten::val 类型默认情况下映射到 TypeScript 的 any 类型,这对于使用或生成 val 类型的 API 来说,无法提供太多有用的信息。为了提供更好的类型信息,可以使用 EMSCRIPTEN_DECLARE_VAL_TYPE()emscripten::register_type 结合使用,注册自定义 val 类型。下面是一个示例

EMSCRIPTEN_DECLARE_VAL_TYPE(CallbackType);

int function_with_callback_param(CallbackType ct) {
    ct(val("hello"));
    return 0;
}

EMSCRIPTEN_BINDINGS(custom_val) {
    function("function_with_callback_param", &function_with_callback_param);
    register_type<CallbackType>("(message: string) => void");
}

nonnull 指针

返回指针的 C++ 函数会生成带有 <SomeClass> | null 的 TS 定义,以默认允许 nullptr。如果 C++ 函数保证返回有效对象,则可以在函数绑定中添加 nonnull<ret_val>() 的策略参数,以从 TS 中省略 | null。这避免了在 TS 中处理 null 的情况。

性能

在撰写本文时,还没有进行过对 embind全面性能测试,无论是针对标准基准测试,还是与 WebIDL Binder 相比。

简单函数的调用开销已测得约为 200 ns。虽然有进一步优化的空间,但到目前为止,它在实际应用程序中的性能已被证明是完全可以接受的。