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 檔案可以載入為 node 模組或透過 <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 的輸出。

當 JavaScript 檔案最初載入時(與全域建構函式同時),會執行 EMSCRIPTEN_BINDINGS() 區塊中的程式碼。函式 lerp() 的參數類型和傳回類型會由 embind 自動推斷。

embind 公開的所有符號都可以在 Emscripten 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++ 方法傳回的物件都必須明確刪除,除非使用 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++ 物件產生多個句柄。只有當所有句柄都被刪除時,該物件才會被銷毀。

clone() JavaScript 方法會回傳新的句柄。最終也必須使用 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 提供對值類型的支援。 陣列會轉換為 JavaScript 陣列並從 JavaScript 陣列轉換而來,而 物件會轉換為 JavaScript 物件並從 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*)

不適用

指標必須明確使用回傳策略。

take_ownership

值 (T)

移動

JS 必須刪除移動的物件。

參考 (T&)

移動

JS 必須刪除移動的物件。

指標 (T*)

JS 必須刪除物件。

reference

值 (T)

不適用

不允許參考值。

參考 (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 的原始指標策略,以管理引數的物件所有權。

外部建構函式

有兩種方法可以指定類別的建構函式。

零引數範本形式會使用範本中指定的引數叫用自然建構函式。例如

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

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

第二種建構函式形式會採用函式指標引數,並用於使用工廠函式建構自身的類別。例如

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++ 介面時。

注意

函式繫結上的 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 必須了解完全衍生的類型,才能使自動向下轉型正常運作。

注意

除非啟用 RTTI,否則 Embind 不支援此功能。

多載函式

建構函式和函式可以根據引數數量進行多載,但是 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 支援 可與 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 教學課程:產生正弦波、方波、鋸齒波和三角波 (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_() 來建立上下文,從這個上下文,我們可以建立一個 oscillatorset() 其屬性(再次使用 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 奈秒。雖然還有進一步最佳化的空間,但到目前為止,它在實際應用中的效能已被證明是可以接受的。