WebIDL 繫結器 提供了一種簡單且輕量的方法來繫結 C++,以便可以像正常的 JavaScript 程式庫一樣從 JavaScript 呼叫編譯後的程式碼。
WebIDL 繫結器 使用 WebIDL 來定義繫結,這是一種專門為將 C++ 和 JavaScript 黏合在一起而設計的介面語言。這不僅是繫結的自然選擇,而且由於它是低階的,因此相對容易進行最佳化。
繫結器支援可以在 WebIDL 中表達的 C++ 類型子集。此子集足以滿足大多數使用案例 — 使用繫結器移植的專案範例包括 Box2D 和 Bullet 物理引擎。
本主題示範如何使用 IDL 繫結和使用 C++ 類別、函式和其他類型。
注意
WebIDL 繫結器 的替代方案是使用 Embind。如需更多資訊,請參閱 繫結 C++ 和 JavaScript — WebIDL 繫結器和 Embind。
使用 WebIDL 繫結器 進行繫結是一個分為三個階段的過程
建立一個 WebIDL 檔案,描述 C++ 介面。
使用繫結器來產生 C++ 和 JavaScript「黏合」程式碼。
使用 Emscripten 專案編譯此黏合程式碼。
第一步是建立一個 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.cpp 和 glue.js,您可以使用下列指令
tools/webidl_binder my_classes.idl glue
若要使用專案中的黏合程式碼檔案(glue.cpp
和 glue.js
)
在最終的 emcc 指令中加入 --post-js glue.js
。post-js 選項會在編譯後的輸出結尾加入黏合程式碼。
建立一個名為 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 檔案時被覆寫。
將 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 物件一樣。例如,繼續上面的範例,您可以建立 Foo
和 Bar
物件,並呼叫它們的方法。
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。
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);
可以使用 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]
標記。
如果無法刪除類別(因為解構子是私有的),請在 IDL 檔案中指定 [NoDelete]
。
[NoDelete]
interface Foo {
...
};
在命名空間(或另一個類別)中宣告的 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();
};
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
(具有包裝的指標 0 的全域單例),而不是 null
(JavaScript 內建物件)或 0。
透過您可以在 IDL 檔案中使用的 VoidPtr
型別,支援 void*
型別。您也可以使用 any
型別。
它們之間的區別在於 VoidPtr
的行為類似於指標型別,您會取得包裝器物件,而 any
的行為類似於 32 位元整數(這是在 Emscripten 編譯程式碼中原始指標的本質)。
WebIDL 中的型別名稱與 C++ 中的型別名稱不完全相同。本節顯示您會遇到的較常見型別的對應。
C++ |
IDL |
---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
注意
WebIDL 類型已完整記錄在 此 W3C 規範中。
如需完整可運作的範例,請參閱 test_webidl,位於 測試套件中。測試套件中的程式碼保證可以運作,並且涵蓋比本文更多的案例。
另一個好範例是 ammo.js,它使用 WebIDL Binder 將 Bullet Physics 引擎移植到 Web 上。