這會將 JS 繫結至 WASM!
在上一篇 wasm 文章中,我說明瞭如何將 C 程式庫編譯為 wasm,以便在網路上使用。我 (和許多讀者) 發現,您必須手動宣告要使用的 wasm 模組函式,這種方式相當粗糙,而且有點不自然。為喚起您的記憶,以下是我所指的程式碼片段:
const api = {
version: Module.cwrap('version', 'number', []),
create_buffer: Module.cwrap('create_buffer', 'number', ['number', 'number']),
destroy_buffer: Module.cwrap('destroy_buffer', '', ['number']),
};
在這裡,我們宣告以 EMSCRIPTEN_KEEPALIVE 標記的函式名稱、傳回類型,以及引數類型。之後,我們可以使用 api 物件的方法來叫用這些函式。不過,以這種方式使用 wasm 不支援字串,且需要手動移動記憶體區塊,因此許多程式庫 API 的使用方式非常繁瑣。有沒有更好的方法?當然有,否則這篇文章要寫什麼?
C++ 名稱修飾
雖然開發人員體驗已足以成為建構工具的理由,但其實還有更迫切的原因:編譯 C 或 C++ 程式碼時,每個檔案都會分別編譯。然後,連結器會負責將所有這些所謂的物件檔案合併在一起,並轉換成 WASM 檔案。使用 C 時,函式名稱仍可在物件檔案中供連結器使用。如要呼叫 C 函式,您只需要名稱,
我們將名稱以字串形式提供給 cwrap()。
另一方面,C++ 支援函式多載,也就是說,只要簽章不同 (例如參數類型不同),您就可以多次實作相同函式。在編譯器層級,add 等適當的名稱會遭到破壞,變成在函式名稱中為連結器編碼簽章的內容。因此,我們無法再依名稱查詢函式。
輸入 embind
embind 是 Emscripten 工具鍊的一部分,提供大量 C++ 巨集,可讓您註解 C++ 程式碼。您可以從 JavaScript 宣告要使用的函式、列舉、類別或值型別。我們先從一些簡單的純函式開始:
#include <emscripten/bind.h>
using namespace emscripten;
double add(double a, double b) {
return a + b;
}
std::string exclaim(std::string message) {
return message + "!";
}
EMSCRIPTEN_BINDINGS(my_module) {
function("add", &add);
function("exclaim", &exclaim);
}
與先前的文章相比,我們不再包含 emscripten.h,因為我們不必再使用 EMSCRIPTEN_KEEPALIVE 註解函式。我們在 EMSCRIPTEN_BINDINGS 區段中列出名稱,這些名稱會將函式公開給 JavaScript。
如要編譯這個檔案,可以使用與前一篇文章相同的設定 (或 Docker 映像檔)。如要使用 embind,請新增 --bind 旗標:
$ emcc --bind -O3 add.cpp
現在只要製作 HTML 檔案,載入我們新建立的 wasm 模組即可:
<script src="/a.out.js"></script>
<script>
Module.onRuntimeInitialized = _ => {
console.log(Module.add(1, 2.3));
console.log(Module.exclaim("hello world"));
};
</script>
如您所見,我們不再使用 cwrap()。這項功能可直接使用,但更重要的是,我們不必擔心手動複製記憶體區塊,讓字串正常運作!embind 會免費提供這項功能,以及型別檢查:
這相當實用,因為我們可以在早期發現一些錯誤,而不必處理偶爾相當笨拙的 WASM 錯誤。
物件
許多 JavaScript 建構函式和函式都會使用選項物件。這是 JavaScript 中很棒的模式,但手動在 wasm 中實現非常繁瑣。embind 也能派上用場!
舉例來說,我設計出這個非常實用的 C++ 函式,可處理字串,而且我急需在網路上使用這個函式。方法如下:
#include <emscripten/bind.h>
#include <algorithm>
using namespace emscripten;
struct ProcessMessageOpts {
bool reverse;
bool exclaim;
int repeat;
};
std::string processMessage(std::string message, ProcessMessageOpts opts) {
std::string copy = std::string(message);
if(opts.reverse) {
std::reverse(copy.begin(), copy.end());
}
if(opts.exclaim) {
copy += "!";
}
std::string acc = std::string("");
for(int i = 0; i < opts.repeat; i++) {
acc += copy;
}
return acc;
}
EMSCRIPTEN_BINDINGS(my_module) {
value_object<ProcessMessageOpts>("ProcessMessageOpts")
.field("reverse", &ProcessMessageOpts::reverse)
.field("exclaim", &ProcessMessageOpts::exclaim)
.field("repeat", &ProcessMessageOpts::repeat);
function("processMessage", &processMessage);
}
我要為 processMessage() 函式的選項定義結構體。在 EMSCRIPTEN_BINDINGS 區塊中,我可以使用 value_object,讓 JavaScript 將這個 C++ 值視為物件。如果我偏好將這個 C++ 值做為陣列使用,也可以使用 value_array。我也繫結了 processMessage() 函式,其餘部分則是 embind magic。現在,我可以在 JavaScript 中呼叫 processMessage() 函式,不必使用任何樣板程式碼:
console.log(Module.processMessage(
"hello world",
{
reverse: false,
exclaim: true,
repeat