WebIDL 繫結器

WebIDL 繫結器 提供了一種簡單且輕量的方法來繫結 C++,以便可以像正常的 JavaScript 程式庫一樣從 JavaScript 呼叫編譯後的程式碼。

WebIDL 繫結器 使用 WebIDL 來定義繫結,這是一種專門為將 C++ 和 JavaScript 黏合在一起而設計的介面語言。這不僅是繫結的自然選擇,而且由於它是低階的,因此相對容易進行最佳化。

繫結器支援可以在 WebIDL 中表達的 C++ 類型子集。此子集足以滿足大多數使用案例 — 使用繫結器移植的專案範例包括 Box2DBullet 物理引擎。

本主題示範如何使用 IDL 繫結和使用 C++ 類別、函式和其他類型。

注意

WebIDL 繫結器 的替代方案是使用 Embind。如需更多資訊,請參閱 繫結 C++ 和 JavaScript — WebIDL 繫結器和 Embind

快速範例

使用 WebIDL 繫結器 進行繫結是一個分為三個階段的過程

  • 建立一個 WebIDL 檔案,描述 C++ 介面。

  • 使用繫結器來產生 C++ 和 JavaScript「黏合」程式碼。

  • 使用 Emscripten 專案編譯此黏合程式碼。

定義 WebIDL 檔案

第一步是建立一個 WebIDL 檔案,描述您要繫結的 C++ 類型。此檔案將重複 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 關鍵字。

產生繫結黏合程式碼

繫結產生器 (tools/webidl_binder.py) 以 Web IDL 檔案名稱和輸出檔案名稱作為輸入,並建立 C++ 和 JavaScript 黏合程式碼檔案。

例如,若要為 IDL 檔案 my_classes.idl 建立黏合程式碼檔案 glue.cppglue.js,您可以使用下列指令

tools/webidl_binder my_classes.idl glue

編譯專案(使用繫結黏合程式碼)

若要使用專案中的黏合程式碼檔案(glue.cppglue.js

  1. 在最終的 emcc 指令中加入 --post-js glue.jspost-js 選項會在編譯後的輸出結尾加入黏合程式碼。

  2. 建立一個名為 my_glue_wrapper.cpp 之類的檔案,以 #include 您要繫結的類別的標頭和 glue.cpp。這可能具有以下內容

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

注意

繫結產生器 發出的 C++ 黏合程式碼不包含它繫結的類別的標頭,因為它們不存在於 Web IDL 檔案中。上述步驟使這些可供黏合程式碼使用。另一種替代方法是在 glue.cpp 的頂部包含標頭,但這樣的話,它們會在每次重新編譯 IDL 檔案時被覆寫。

  1. my_glue_wrapper.cpp 加入最終的 emcc 指令。

最終的 emcc 指令包含 C++ 和 JavaScript 黏合程式碼,這些程式碼是建置為一起運作

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

輸出現在包含透過 JavaScript 使用 C++ 類別所需的一切。

模組化輸出

使用 WebIDL 繫結器時,您通常要做的是建立一個程式庫。在這種情況下,使用 MODULARIZE 選項是合理的。它會將整個 JavaScript 輸出包裝在函式中,並傳回一個 Promise,該 Promise 會解析為已初始化的 Module 執行個體。

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

當執行編譯後的程式碼安全時(即,在下載和執行個體化之後),就會解析 promise。promise 會在叫用 onRuntimeInitialized 回呼時同時解析,因此在使用 MODULARIZE 時不需要使用 onRuntimeInitialized

您可以使用 EXPORT_NAME 選項將 Module 變更為其他名稱。這對程式庫來說是一個很好的做法,因為它們不會在全域範圍內包含不必要的東西,而且在某些情況下,您想要建立多個程式庫。

在 JavaScript 中使用 C++ 類別

繫結完成後,可以在 JavaScript 中建立和使用 C++ 物件,就像它們是正常的 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

注意

在 JavaScript 中建立 C++ 物件時,會透明地呼叫 C++ 建構函式。但是,沒有辦法判斷 JavaScript 物件是否即將進行垃圾收集,因此繫結器黏合程式碼無法自動呼叫解構函式。

您通常需要銷毀您建立的物件,但這取決於所移植的程式庫。

屬性

物件屬性在 IDL 中使用 attribute 關鍵字定義。然後可以在 JavaScript 中使用 get_foo()/ set_foo() 存取器方法,或直接作為物件的屬性來存取這些屬性。

// 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],則產生的膠合 C++ 將無法編譯(當它嘗試將參考(它認為是指標)轉換為物件時會失敗)。

如果 C++ 傳回物件(而不是參考或指標),則應使用 [Value] 修飾傳回型別。這將會配置該類別的靜態(單例)實例並將其傳回。您應該立即使用它,並在使用後丟棄對它的任何參考。

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

Const

可以使用 IDL 中的 [Const] 來指定使用 const 的 C++ 引數或傳回型別。

例如,以下程式碼片段顯示了傳回常數指標物件的函式的 C++ 和 IDL。

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

對應於 const 資料成員的屬性必須使用 readonly 關鍵字指定,而不是使用 [Const]。例如

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

這會在繫結中產生 get_numericalConstant() 方法,但不會產生對應的設定器。該屬性也會在 JavaScript 中定義為唯讀,這表示嘗試設定它對值沒有任何影響,並且會在嚴格模式下拋出錯誤。

提示

傳回型別可以有多個規範。例如,傳回常數參考的方法將在 IDL 中使用 [Ref, Const] 標記。

不可刪除的類別 (NoDelete)

如果無法刪除類別(因為解構子是私有的),請在 IDL 檔案中指定 [NoDelete]

[NoDelete]
interface Foo {
...
};

定義內部類別和命名空間內的類別 (Prefix)

在命名空間(或另一個類別)中宣告的 C++ 類別必須使用 IDL 檔案的 Prefix 關鍵字來指定範圍。然後,每當在 C++ 膠合程式碼中引用類別時,都會使用前置詞。

例如,以下 IDL 定義確保將 Inner 類別稱為 MyNameSpace::Inner

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

運算子

您可以使用 [Operator=] 來繫結到 C++ 運算子

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

注意

  • 運算子名稱可以是任何內容(add 只是一個範例)。

  • 目前僅支援以下二元運算子:+-*/%^&|=<>+=-=*=/=%=^=&=|=<<>>>>=<<===!=<=>=<=>&&||,以及陣列索引運算子 []

列舉

列舉在 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 允許在 JavaScript 中對 C++ 基底類別進行子類別化。在下面的 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) — 比較兩個物件的指標。

注意

對於特定類別的特定指標,始終只有單個包裝的物件。這讓您可以將資料新增到該物件上,並使用正常的 JavaScript 語法 (object.attribute = someData 等) 在其他地方使用。

應使用 compare() 而不是直接指標比較,因為如果一個類別是另一個類別的子類別,則有可能擁有具有相同指標的不同包裝物件。

NULL

所有傳回指標、參考或物件的繫結函式都會傳回包裝的指標。原因是透過始終傳回包裝器,您可以取得輸出並將其傳遞給另一個繫結函式,而無需該函式檢查引數的型別。

當傳回 NULL 指標時,這種情況可能會令人困惑。使用繫結時,傳回的指標將會是 NULL(具有包裝的指標 0 的全域單例),而不是 null(JavaScript 內建物件)或 0。

void*

透過您可以在 IDL 檔案中使用的 VoidPtr 型別,支援 void* 型別。您也可以使用 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*

anyVoidPtr (請參閱 void*)

注意

WebIDL 類型已完整記錄在 此 W3C 規範中。

測試與範例程式碼

如需完整可運作的範例,請參閱 test_webidl,位於 測試套件中。測試套件中的程式碼保證可以運作,並且涵蓋比本文更多的案例。

另一個好範例是 ammo.js,它使用 WebIDL BinderBullet Physics 引擎移植到 Web 上。