WebIDLバインダー

WebIDLバインダーは、C++のバインディングを簡素化し軽量化するためのアプローチを提供します。これにより、コンパイルされたコードを通常のJavaScriptライブラリのようにJavaScriptから呼び出すことができます。

WebIDLバインダーは、バインディングの定義にWebIDLを使用します。これは、C++とJavaScriptを連携させるために特化したインターフェース言語です。バインディングとして自然な選択であるだけでなく、低レベルであるため、最適化が比較的容易です。

このバインダーは、WebIDLで表現できるC++型のサブセットをサポートしています。このサブセットは、ほとんどのユースケースで十分です。バインダーを使用して移植されたプロジェクトの例としては、Box2DBulletの物理エンジンなどがあります。

このトピックでは、IDLを使用してC++クラス、関数、その他の型をバインドして使用する方法を示します。

注記

WebIDLバインダーの代替手段として、Embindを使用できます。詳細については、C++とJavaScriptのバインディング - WebIDLバインダーとEmbindを参照してください。

簡単な例

WebIDLバインダーを使用したバインディングは、3段階のプロセスです。

  • C++インターフェースを記述するWebIDLファイルを作成します。

  • バインダーを使用して、C++とJavaScriptの「Glue」コードを生成します。

  • このGlueコードをEmscriptenプロジェクトでコンパイルします。

WebIDLファイルの定義

最初のステップは、バインドするC++型を記述するWebIDLファイルを作成することです。このファイルは、C++ヘッダーファイルの一部情報を、明示的に解析しやすい形式で、コード項目を表す形式で複製します。

たとえば、次のC++クラスを考えてみましょう。

class Foo {
public:
  int getVal();
  void setVal(int v);
};

class Bar {
public:
  Bar(long val);
  void doSomething();
};

これらを記述するには、次のIDLファイルを使用できます。

interface Foo {
  void Foo();
  long getVal();
  void setVal(long v);
};

interface Bar {
  void Bar(long val);
  void doSomething();
};

IDL定義とC++間のマッピングは、かなり明白です。主な注意点は次のとおりです。

  • IDLクラス定義には、インターフェースと同じ名前のvoidを返す関数が含まれています。このコンストラクタを使用すると、JavaScriptからオブジェクトを作成できます。C++でデフォルトコンストラクタを使用する場合でも、IDLで定義する必要があります(上記のFooを参照)。

  • WebIDLの型名は、C++の型名と同一ではありません(たとえば、intは上記のlongにマップされます)。マッピングの詳細については、WebIDL型を参照してください。

注記

structsは、上記のクラスと同じ方法で、interfaceキーワードを使用して定義されます。

バインディングGlueコードの生成

バインディングジェネレーター(tools/webidl_binder.py)は、Web IDLファイル名と出力ファイル名をインプットとして受け取り、C++とJavaScriptのGlueコードファイルを作成します。

たとえば、IDLファイルmy_classes.idlに対してGlueコードファイルglue.cppglue.jsを作成するには、次のコマンドを使用します。

tools/webidl_binder my_classes.idl glue

プロジェクトのコンパイル(バインディングGlueコードの使用)

プロジェクトでGlueコードファイル(glue.cppglue.js)を使用するには、

  1. 最終的なemccコマンドに--post-js glue.jsを追加します。post-jsオプションは、コンパイル済み出力の最後にGlueコードを追加します。

  2. バインドするクラスのヘッダーとglue.cpp#includeするmy_glue_wrapper.cppという名前のファイルを作成します。内容は次のようになります。

#include <...> // Where "..." represents the headers for the classes we are binding.
#include <glue.cpp>

注記

バインディングジェネレーターによって出力されるC++ Glueコードには、バインドするクラスのヘッダーは含まれていません。これは、Web IDLファイルに存在しないためです。上記のステップで、Glueコードでこれらを使用できるようにします。別の方法としては、glue.cppの先頭にヘッダーを含めることもできますが、その場合、IDLファイルが再コンパイルされるたびに上書きされます。

  1. 最終的なemccコマンドにmy_glue_wrapper.cppを追加します。

最終的なemccコマンドには、C++とJavaScriptのGlueコードの両方が含まれており、これらは連携してビルドされます。

emcc my_classes.cpp my_glue_wrapper.cpp --post-js glue.js -o output.js

出力には、JavaScriptを介してC++クラスを使用するために必要なものがすべて含まれるようになりました。

モジュール式出力

WebIDLバインダーを使用する場合、多くの場合、ライブラリを作成しています。その場合、`MODULARIZE`オプションを使用するのが理にかなっています。これは、JavaScript出力を関数でラップし、初期化されたModuleインスタンスに解決されるPromiseを返します。

var instance;
Module().then(module => {
  instance = module;
});

Promiseは、コンパイル済みコードを実行できるようになった時点で解決されます。つまり、ダウンロードとインスタンス化が完了した後です。Promiseは`onRuntimeInitialized`コールバックが呼び出されると同時に解決されるため、`MODULARIZE`を使用する場合は`onRuntimeInitialized`を使用する必要はありません。

`EXPORT_NAME`オプションを使用して`Module`を別の名前に変更できます。これはライブラリには良い習慣です。グローバルスコープに不要なものを含めなくなるため、場合によっては複数作成したいこともあります。

JavaScriptでのC++クラスの使用

バインディングが完了すると、C++オブジェクトは、通常のJavaScriptオブジェクトのようにJavaScriptで作成および使用できます。たとえば、上記の例を続けると、FooBarオブジェクトを作成し、それらのメソッドを呼び出すことができます。

var f = new Module.Foo();
f.setVal(200);
alert(f.getVal());

var b = new Module.Bar(123);
b.doSomething();

重要

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

オブジェクトはデフォルトでグローバル名前空間にも存在しますが、存在しない場合もあります(たとえば、Closureコンパイラを使用してコードを縮小する場合、またはグローバル名前空間の汚染を避けるためにコンパイル済みコードを関数でラップする場合)。もちろん、新しい変数に割り当てることで、モジュールには任意の名前を使用できます。`var MyModuleName = Module;`。

重要

このコードは、コンパイル済みコードを呼び出すのが安全な場合にのみ使用できます。そのFAQエントリで詳細を参照してください。

JavaScriptは、参照がなくなったときにラップされたC++オブジェクトを自動的にガベージコレクションします。C++オブジェクトに特定のクリーンアップが必要ない場合(つまり、デストラクタがない場合)、他にアクションを実行する必要はありません。

C++オブジェクトにクリーンアップが必要な場合は、明示的にModule.destroy(obj)を呼び出してデストラクタを呼び出す必要があります。その後、オブジェクトへのすべての参照を削除して、ガベージコレクションできるようにします。たとえば、Barがクリーンアップが必要なメモリを割り当てる場合

var b = new Module.Bar(123);
b.doSomething();
Module.destroy(b); // If the C++ object requires clean up

注記

C++オブジェクトがJavaScriptで作成されると、C++コンストラクタは透過的に呼び出されます。ただし、JavaScriptオブジェクトがガベージコレクションされる直前であるかどうかを判断する方法はないため、バインダーGlueコードはデストラクタを自動的に呼び出すことができません。

通常は作成したオブジェクトを破棄する必要がありますが、これは移植されているライブラリによって異なります。

属性

オブジェクト属性は、attributeキーワードを使用してIDLで定義されます。これらは、get_foo()/set_foo()アクセサメソッドを使用するか、オブジェクトのプロパティとして直接、JavaScriptでアクセスできます。

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

読み取り専用の属性については、Constを参照してください。

ポインタ、参照、値型(RefとValue)

C++の引数と戻り値の型は、ポインタ、参照、または値型(スタック上に割り当てられる)にすることができます。IDLファイルは、これらの各ケースを表すために異なる装飾を使用します。

IDLのカスタム型の装飾されていない引数と戻り値は、C++ではポインタと見なされます。

// C++
MyClass* process(MyClass* input);
// WebIDL
MyClass process(MyClass input);

この仮定は、void、int、bool、DOMStringなど、基本型には当てはまりません。

参照は[Ref]を使用して装飾する必要があります。

// C++
MyClass& process(MyClass& input);
// WebIDL
[Ref] MyClass process([Ref] MyClass input);

注記

参照で[Ref]を省略した場合、生成されたGlue C++はコンパイルされません(ポインタと見なしている参照をオブジェクトに変換しようとすると失敗します)。

C++がオブジェクトを返す場合(参照またはポインタではなく)、戻り値の型は[Value]を使用して装飾する必要があります。これにより、そのクラスの静的(シングルトン)インスタンスが割り当てられ、返されます。すぐに使用し、使用後は参照を削除する必要があります。

// C++
MyClass process(MyClass& input);
// WebIDL
[Value] MyClass process([Ref] MyClass input);

Const

constを使用するC++の引数または戻り値の型は、[Const]を使用してIDLで指定できます。

例えば、以下のコード断片は、定数ポインタオブジェクトを返す関数のC++とIDLを示しています。

//C++
const myObject* getAsConst();
// WebIDL
[Const] myObject getAsConst();

constデータメンバに対応する属性は、[Const]ではなく、readonlyキーワードで指定する必要があります。例えば

//C++
const int numericalConstant;
// WebIDL
readonly attribute long numericalConstant;

これにより、バインディングにget_numericalConstant()メソッドが生成されますが、対応するセッターは生成されません。この属性はJavaScriptでも読み取り専用として定義されるため、値を設定しようとすると、値には影響がなく、厳格モードではエラーが発生します。

ヒント

戻り値の型に複数の指定子を持つことができます。例えば、定数参照を返すメソッドは、IDLでは[Ref, Const]を使用してマークアップされます。

削除不可能なクラス (NoDelete)

クラスが削除できない場合(デストラクタがprivateの場合)、IDLファイルに[NoDelete]を指定します。

[NoDelete]
interface Foo {
...
};

内部クラスと名前空間内のクラスの定義 (Prefix)

名前空間(または別のクラス)内で宣言されたC++クラスは、IDLファイルのPrefixキーワードを使用してスコープを指定する必要があります。このプレフィックスは、C++ glueコードでクラスを参照する際に使用されます。

例えば、以下のIDL定義は、InnerクラスがMyNameSpace::Innerとして参照されるようにします。

[Prefix="MyNameSpace::"]
interface Inner {
..
};

演算子

[Operator=]を使用して、C++演算子にバインドできます。

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

注記

  • 演算子名は任意です(addは単なる例です)。

  • 現在サポートされているのは、以下の2項演算子のみです。+-*/%^&|=<>+=-=*=/=%=^=&=|=<<>>>>=<<===!=<=>=<=>&&||、および配列インデックス演算子[]です。

列挙型

列挙型は、C++とIDLで非常に似たように宣言されます。

// C++
enum AnEnum {
  enum_value1,
  enum_value2
};

// WebIDL
enum AnEnum {
  "enum_value1",
  "enum_value2"
};

名前空間内で宣言された列挙型の構文は、少し複雑です。

// C++
namespace EnumNamespace {
  enum EnumInNamespace {
  e_namespace_val = 78
  };
};

// WebIDL
enum EnumNamespace_EnumInNamespace {
  "EnumNamespace::e_namespace_val"
};

列挙型がクラス内で定義されている場合、列挙型とクラスインターフェースのIDL定義は別々になります。

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

JavaScriptでのC++基本クラスのサブクラス化 (JSImplementation)

WebIDL Binderを使用すると、C++基本クラスをJavaScriptでサブクラス化できます。以下のIDL断片では、JSImplementation="Base"は、関連付けられたインターフェース(ImplJS)がC++クラスBaseのJavaScript実装になることを意味します。

[JSImplementation="Base"]
interface ImplJS {
  void ImplJS();
  void virtualFunc();
  void virtualFunc2();
};

バインディングジェネレータを実行してコンパイルした後、次のようにJavaScriptでインターフェースを実装できます。

var c = new ImplJS();
c.virtualFunc = function() { .. };

C++コードがBaseインスタンスへのポインタを持ち、virtualFunc()を呼び出すと、その呼び出しは上記で定義されたJavaScriptコードに到達します。

注記

  • JSImplementationクラス(ImplJS)のIDLで記述したすべてのメソッドを実装する必要があります。そうでない場合、コンパイルはエラーで失敗します。

  • IDLファイルにBaseクラスのインターフェース定義も提供する必要があります。

ポインタと比較

すべてのバインディング関数は、生のポインタではなく、ラッパーオブジェクト(生のポインタを含む)を受け取ることが期待されます。通常、生のポインタを扱う必要はありません(これらは単なるメモリアドレス/整数です)。扱う必要がある場合は、コンパイルされたコードの以下の関数が役立ちます。

  • wrapPointer(ptr, Class) — 生のポインタ(整数)を指定して、ラップされたオブジェクトを返します。

    注記

    Classを渡さない場合、ルートクラスとみなされます。これはおそらく意図したものではありません。

  • getPointer(object) — 生のポインタを返します。

  • castObject(object, Class) — 同じポインタの別のクラスへのラッピングを返します。

  • compare(object1, object2) — 2つのオブジェクトのポインタを比較します。

注記

特定のクラスへの特定のポインタに対しては、常に1つのラップされたオブジェクトがあります。これにより、そのオブジェクトにデータを追加し、通常のJavaScript構文(object.attribute = someDataなど)を使用して他の場所でそれを使用できます。

あるクラスが別のクラスのサブクラスである場合、同じポインタを持つ異なるラップされたオブジェクトを持つ可能性があるため、直接的なポインタ比較の代わりにcompare()を使用する必要があります。

NULL

ポインタ、参照、またはオブジェクトを返すすべてのバインディング関数は、ラップされたポインタを返します。常にラッパーを返すことによって、その関数が引数の型をチェックする必要なく、出力を取得して別のバインディング関数に渡すことができます。

NULLポインタを返す場合に混乱が生じる可能性があります。バインディングを使用する場合、返されるポインタはNULL(ラップされたポインタ0を持つグローバルシングルトン)ではなく、null(JavaScript組み込みオブジェクト)または0になります。

void*

void*型は、IDLファイルで使用できるVoidPtr型を介してサポートされています。any型も使用できます。

それらの違いは、VoidPtrはラッパーオブジェクトを取得するポインタ型のように動作するのに対し、anyは32ビット整数(Emscriptenコンパイルコードでの生のポインタ)のように動作することです。

WebIDL型

WebIDLの型名は、C++の型名と同一ではありません。このセクションでは、よく遭遇する型のマッピングを示します。

C++

IDL

bool

boolean

float

float

double

double

char

byte

char*

DOMString(JavaScript文字列を表します)

unsigned char

octet

int

long

long

long

unsigned short

unsigned short

unsigned long

unsigned long

long long

long long

void

void

void*

anyまたはVoidPtrvoid*を参照)

注記

WebIDL型については、このW3C仕様で詳しく説明されています。

テストとサンプルコード

完全な動作例については、test_webidlテストスイート内)を参照してください。テストスイートコードは動作が保証されており、この記事だけではカバーできない多くのケースを含んでいます。

もう1つの優れた例として、ammo.jsがあります。これは、WebIDL Binderを使用してBullet PhysicsエンジンをWebに移植しています。