瞭解如何在 WebAssembly 程式庫中嵌入 JavaScript 程式碼,以便與外部世界通訊。
在 WebAssembly 與網頁整合時,您需要能夠呼叫外部 API (例如網頁 API 和第三方程式庫) 接著,您需要一種方法來儲存這些 API 傳回的值和物件例項,以及一種方法來稍後將這些已儲存的值傳遞至其他 API。對於非同步 API,您可能還需要使用 Asyncify 在同步 C/C++ 程式碼中等待承諾,並在作業完成後讀取結果。
Emscripten 提供多種工具,可用於這類互動:
emscripten::val
:用於在 C++ 中儲存及操作 JavaScript 值。EM_JS
:用於嵌入 JavaScript 程式碼片段,並將其綁定為 C/C++ 函式。EM_ASYNC_JS
與EM_JS
類似,但更容易嵌入非同步 JavaScript 程式碼片段。EM_ASM
:用於嵌入短片段並在內文中執行,不必宣告函式。--js-library
適用於您想將大量 JavaScript 函式一起宣告為單一程式庫的進階情境。
本文將說明如何將這些方法用於類似工作。
emscripten::val 類別
emcripten::val
類別是由 Embind 提供。它可以叫用全域 API、將 JavaScript 值繫結至 C++ 例項,以及在 C++ 和 JavaScript 類型之間轉換值。
以下說明如何搭配使用 Asyncify 的 .await()
擷取及剖析部分 JSON:
#include <emscripten/val.h>
using namespace emscripten;
val fetch_json(const char *url) {
// Get and cache a binding to the global `fetch` API in each thread.
thread_local const val fetch = val::global("fetch");
// Invoke fetch and await the returned `Promise<Response>`.
val response = fetch(url).await();
// Ask to read the response body as JSON and await the returned `Promise<any>`.
val json = response.call<val>("json").await();
// Return the JSON object.
return json;
}
// Example URL.
val example_json = fetch_json("https://httpbin.org/json");
// Now we can extract fields, e.g.
std::string author = json["slideshow"]["author"].as<std::string>();
這段程式碼運作良好,但會執行許多中間步驟。val
上的每個作業都需要執行下列步驟:
- 將做為引數傳遞的 C++ 值轉換為某種中繼格式。
- 前往 JavaScript,讀取並將引數轉換為 JavaScript 值。
- 執行函式
- 將結果從 JavaScript 轉換為中介格式。
- 將轉換後的結果傳回至 C++,最後由 C++ 讀取。
每個 await()
也必須暫停 C++ 端,方法是解開 WebAssembly 模組的整個呼叫堆疊、返回 JavaScript、等待,並在作業完成時還原 WebAssembly 堆疊。
這類程式碼不需要任何 C++ 程式碼,C++ 程式碼只會充當一系列 JavaScript 作業的驅動程式。如果您可以將 fetch_json
移至 JavaScript,同時減少中間步驟的負載,會如何呢?
EM_JS 巨集
EM_JS macro
可將 fetch_json
移至 JavaScript。Emscripten 中的 EM_JS
可讓您宣告由 JavaScript 程式碼片段實作的 C/C++ 函式。
與 WebAssembly 本身一樣,此方法也有限制,只支援數值引數和傳回值。如要傳遞任何其他值,您必須透過對應的 API 手動轉換這些值。以下是幾個例子。
傳遞數字時不需要進行任何轉換:
// Passing numbers, doesn't need any conversion.
EM_JS(int, add_one, (int x), {
return x + 1;
});
int x = add_one(41);
在將字串傳遞至 JavaScript 和從 JavaScript 傳遞時,您必須使用 preamble.js 中的對應轉換和分配函式:
EM_JS(void, log_string, (const char *msg), {
console.log(UTF8ToString(msg));
});
EM_JS(const char *, get_input, (), {
let str = document.getElementById('myinput').value;
// Returns heap-allocated string.
// C/C++ code is responsible for calling `free` once unused.
return allocate(intArrayFromString(str), 'i8', ALLOC_NORMAL);
});
最後,如果是更複雜的任意值類型,您可以使用前述 val
類別的 JavaScript API。您可以使用它將 JavaScript 值和 C++ 類別轉換為中介程式句柄,反之亦然:
EM_JS(void, log_value, (EM_VAL val_handle), {
let value = Emval.toValue(val_handle);
console.log(value);
});
EM_JS(EM_VAL, find_myinput, (), {
let input = document.getElementById('myinput');
return Emval.toHandle(input);
});
val obj = val::object();
obj.set("x", 1);
obj.set("y", 2);
log_value(obj.as_handle()); // logs { x: 1, y: 2 }
val myinput = val::take_ownership(find_input());
// Now you can store the `find_myinput` DOM element for as long as you like, and access it later like:
std::string value = input["value"].as<std::string>();
考量到這些 API,您可以重新編寫 fetch_json
範例,讓大部分工作在 JavaScript 中執行:
EM_JS(EM_VAL, fetch_json, (const char *url), {
return Asyncify.handleAsync(async () => {
url = UTF8ToString(url);
// Invoke fetch and await the returned `Promise<Response>`.
let response = await fetch(url);
// Ask to read the response body as JSON and await the returned `Promise<any>`.
let json = await response.json();
// Convert JSON into a handle and return it.
return Emval.toHandle(json);
});
});
// Example URL.
val example_json = val::take_ownership(fetch_json("https://httpbin.org/json"));
// Now we can extract fields, e.g.
std::string author = json["slideshow"]["author"].as<std::string>();
我們在函式的進入點和離開點仍有幾個明確的轉換,但其餘部分現在都是一般 JavaScript 程式碼。與 val
等同,現在 JavaScript 引擎可以對其進行最佳化,且只需要為所有非同步作業暫停 C++ 端一次。
EM_ASYNC_JS 巨集
唯一不太理想的部分是 Asyncify.handleAsync
包裝函式,其唯一目的是允許使用 Asyncify 執行 async
JavaScript 函式。事實上,這個用途非常常見,因此現在有專門的 EM_ASYNC_JS
巨集可將這兩者結合在一起。
以下說明如何使用此方法產生 fetch
範例的最終版本:
EM_ASYNC_JS(EM_VAL, fetch_json, (const char *url), {
url = UTF8ToString(url);
// Invoke fetch and await the returned `Promise<Response>`.
let response = await fetch(url);
// Ask to read the response body as JSON and await the returned `Promise<any>`.
let json = await response.json();
// Convert JSON into a handle and return it.
return Emval.toHandle(json);
});
// Example URL.
val example_json = val::take_ownership(fetch_json("https://httpbin.org/json"));
// Now we can extract fields, e.g.
std::string author = json["slideshow"]["author"].as<std::string>();
EM_ASM
EM_JS
是宣告 JavaScript 程式碼片段的建議方式。這項方法效率高,因為它會像任何其他 JavaScript 函式匯入一樣,直接繫結已宣告的程式碼片段。您也可以明確宣告所有參數類型和名稱,提供良好的人因設計。
不過,在某些情況下,您可能想為 console.log
呼叫、debugger;
陳述式或類似項目插入快速程式碼片段,而不想費心宣告整個獨立函式。在極少數情況下,EM_ASM macros family
(EM_ASM
、EM_ASM_INT
和 EM_ASM_DOUBLE
) 可能會是更簡單的選擇。這些巨集與 EM_JS
巨集類似,但會在插入的位置執行程式碼,而不是定義函式。
由於這些函式並未宣告函式原型,因此需要以不同的方式指定傳回類型和存取引數。
您必須使用正確的巨集名稱來選擇傳回類型。EM_ASM
區塊應可像 void
函式一樣運作,EM_ASM_INT
區塊可傳回整數值,而 EM_ASM_DOUBLE
區塊則會相應傳回浮點數。
所有傳遞的引數都會在 JavaScript 主體中以 $0
、$1
等名稱提供。與 EM_JS
或 WebAssembly 的一般情況一樣,引數僅限於數值,包括整數、浮點數、指標和句柄。
以下範例說明如何使用 EM_ASM
巨集,將任意 JS 值記錄到控制台:
val obj = val::object();
obj.set("x", 1);
obj.set("y", 2);
// executes inline immediately
EM_ASM({
// convert handle passed under $0 into a JavaScript value
let obj = Emval.fromHandle($0);
console.log(obj); // logs { x: 1, y: 2 }
}, obj.as_handle());
--js-library
最後,Emscripten 支援在自訂程式庫格式的個別檔案中宣告 JavaScript 程式碼:
mergeInto(LibraryManager.library, {
log_value: function (val_handle) {
let value = Emval.toValue(val_handle);
console.log(value);
}
});
接著,您需要在 C++ 端手動宣告對應的原型:
extern "C" void log_value(EM_VAL val_handle);
在兩側宣告後,JavaScript 程式庫即可透過 --js-library option
連結至主要程式碼,將原型與對應的 JavaScript 實作連結在一起。
不過,這個模組格式是非標準格式,需要仔細的依附元件註解。因此,這項功能大多用於進階情境。
結論
在本篇文章中,我們探討了在使用 WebAssembly 時,將 JavaScript 程式碼整合至 C++ 的各種方式。
加入這類程式碼片段,您就能以更簡潔、更有效率的方式表達長序列作業,並運用第三方程式庫、新的 JavaScript API,甚至是尚未透過 C++ 或 Embind 表達的 JavaScript 語法功能。