Embind

Embind は、C++ 関数とクラスを JavaScript にバインドするために使用され、コンパイルされたコードを「通常の」JavaScript で自然に使用できるようにします。Embind は、C++ から JavaScript クラスを呼び出すこともサポートしています。

Embind は、C++11 および C++14 で導入されたものを含む、ほとんどの C++ 構成要素のバインドをサポートしています。その唯一の重大な制限は、現在 複雑なライフタイムセマンティクスを持つ生のポインタをサポートしていないことです。

この記事では、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 ファイルが最初にロードされたとき (グローバルコンストラクタと同時に) に実行されます。関数 lerp() のパラメーター型と戻り値の型は、embind によって自動的に推論されます。

embind によって公開されるすべてのシンボルは、Emscripten Module オブジェクトで使用できます。

重要

上記のように、常に Module オブジェクト オブジェクトを介してオブジェクトにアクセスしてください。

オブジェクトはデフォルトでグローバル名前空間でも使用できますが、そうでない場合があります (たとえば、クロージャーコンパイラを使用してコードを最小化したり、グローバル名前空間を汚染しないようにコンパイルされたコードを関数でラップしたりする場合)。もちろん、新しい変数に割り当てることで、モジュールに好きな名前を使用できます。例: 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() (static)、property() を登録します。

このバインドブロックは、クラスとそのすべてのメソッドをバインドします。原則として、実際に必要な項目のみをバインドする必要があります。バインドするたびにコードサイズが増加するためです。たとえば、プライベートメソッドや内部メソッドをバインドすることはまれです。

次に、MyClass のインスタンスを作成し、以下に示すように JavaScript で使用できます。

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

クロージャーコンパイラは、Embind を介して JavaScript に公開されるシンボルの名前を認識しません。そのようなシンボルが、(たとえば、--pre-js または --post-js コンパイラフラグを使用して提供される) 独自のコードでクロージャーコンパイラによって名前が変更されないようにするには、それに応じてコードに注釈を付ける必要があります。このような注釈がないと、結果の JavaScript コードは、Embind コードで使用されているシンボル名と一致しなくなり、その結果、実行時エラーが発生します。

クロージャーコンパイラが上記のサンプルコードでシンボルの名前を変更しないようにするには、次のようにコードを書き換える必要があります。

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_ASM または EM_JS で、オプティマイザーによって認識されるコードに対してのみ必要であることに注意してください。クロージャーコンパイラによって最適化されていない他のコードでは、そのような変更を行う必要はありません。--closure 1 なしでビルドしてクロージャーコンパイラを有効にした場合も必要ありません。

メモリ管理

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 Edition 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 オブジェクトとの間で変換されます。

以下の例を検討してください

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 は、関数の戻り型に応じて異なる動作をする 3 つの戻り値ポリシーをサポートしています。ポリシーは次のように機能します。

  • default (引数なし) - 値および参照による返却の場合、オブジェクトのコピーコンストラクタを使用して新しいオブジェクトが割り当てられます。JS はオブジェクトを所有し、削除する責任があります。ポインタを返すことは、デフォルトでは許可されていません(以下の明示的なポリシーを使用してください)。

  • return_value_policy::take_ownership - 所有権は JS に転送されます。

  • return_value_policy::reference - 既存のオブジェクトを参照しますが、所有権は取得しません。JS でまだ使用中にオブジェクトを削除しないように注意する必要があります。

詳細を以下に示します

戻り型

コンストラクタ

クリーンアップ

default

値 (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_pointers または return_value_policy のいずれかでマークする必要があります。関数がポインタを返す場合は、一般的な allow_raw_pointers の代わりに return_value_policy を使用することをお勧めします。

例:

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 のような生のポインタポリシー を実装したいと考えています。

外部コンストラクタ

クラスのコンストラクタを指定する方法は 2 つあります。

ゼロ引数テンプレート形式 は、テンプレートで指定された引数を使用して、自然なコンストラクタを呼び出します。例:

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

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

2 番目の形式のコンストラクタ は、関数ポインタ引数を取り、ファクトリ関数を使用して自身を構築するクラスに使用されます。例:

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

2 つのコンストラクタは、JavaScript でオブジェクトを構築するためのまったく同じインターフェースを提供します。上記の例を続けます。

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

スマートポインタ

スマートポインタでオブジェクトのライフタイムを管理するには、embind にスマートポインタ型を通知する必要があります。

たとえば、クラス C のライフタイムを std::shared_ptr<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> が返されます。

別の方法は、smart_ptr()EMSCRIPTEN_BINDINGS() ブロックで使用することです。

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 バインディングに 2 つの特別なメソッド extendimplement を追加します。extend を使用すると、JavaScript は Backbone.js で例示されているスタイルでサブクラス化できます。implement は、ブラウザまたは他のライブラリによって提供された JavaScript オブジェクトがあり、それを使用して C++ インターフェイスを実装する場合に使用されます。

関数バインディングの pure_virtual アノテーションにより、JavaScript クラスが invoke() をオーバーライドしない場合に、JavaScript が役立つエラーをスローできます。それ以外の場合は、混乱を招くエラーが発生する可能性があります。

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

これで、interfaceObjectInterface ポインタまたは参照を受け取る任意の関数に渡せるようになりました。

非抽象仮想メソッド

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 の特別なラムダバインディングは、ラッパーと 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))
        ;
}

列挙型

Embind列挙型 サポート は、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++ の 定数() を 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 は、JavaScript コードを C++ に文字変換するために使用できる C++ クラス emscripten::val を提供します。val を使用すると、C++ から JavaScript オブジェクトを呼び出し、そのプロパティを読み書きしたり、boolint、または std::string などの C++ 値に強制変換したりできます。

以下の例は、val を使用して C++ から JavaScript Web Audio API を呼び出す方法を示しています。

この例は、優れた Web Audio チュートリアルである 正弦波、矩形波、のこぎり波、三角波の作成(stuartmemo.com)に基づいています。 emscripten::val ドキュメントにはさらに簡単な例があります。

まず、API の使い方を示す以下の JavaScript を考えてください。

// 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++ 関数は、デフォルトで nullptr を許可するために、<SomeClass> | null を使用して TS 定義を生成します。 C++ 関数が有効なオブジェクトを返すことが保証されている場合は、関数バインディングに nonnull<ret_val>() のポリシーパラメータを追加して、TS から | null を省略できます。 これにより、TS で null のケースを処理する必要がなくなります。

パフォーマンス

執筆時点では、標準ベンチマークに対するもの、または WebIDL Binder に対する相対的なものを含め、包括的な embind パフォーマンステストは行われていません。

単純な関数の呼び出しオーバーヘッドは約 200 ns と測定されています。 さらなる最適化の余地はありますが、これまでのところ、実際のアプリケーションでのパフォーマンスは十分に許容できることが証明されています。