使用 Emscripten 以 C++ 嵌入 JavaScript 程式碼片段

瞭解如何在 WebAssembly 程式庫中嵌入 JavaScript 程式碼,以便與外部世界通訊。

Ingvar Stepanyan
Ingvar Stepanyan

在 WebAssembly 與網頁整合時,您需要能夠呼叫外部 API (例如網頁 API 和第三方程式庫) 接著,您需要一種方法來儲存這些 API 傳回的值和物件例項,以及一種方法來稍後將這些已儲存的值傳遞至其他 API。對於非同步 API,您可能還需要使用 Asyncify 在同步 C/C++ 程式碼中等待承諾,並在作業完成後讀取結果。

Emscripten 提供多種工具,可用於這類互動:

  • emscripten::val:用於在 C++ 中儲存及操作 JavaScript 值。
  • EM_JS:用於嵌入 JavaScript 程式碼片段,並將其綁定為 C/C++ 函式。
  • EM_ASYNC_JSEM_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 上的每個作業都需要執行下列步驟:

  1. 將做為引數傳遞的 C++ 值轉換為某種中繼格式。
  2. 前往 JavaScript,讀取並將引數轉換為 JavaScript 值。
  3. 執行函式
  4. 將結果從 JavaScript 轉換為中介格式。
  5. 將轉換後的結果傳回至 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_ASMEM_ASM_INTEM_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 語法功能。