與程式碼互動

Emscripten 提供了許多方法來連結和互動 JavaScript 與編譯過的 C 或 C++

本文說明上述列出的每個方法,並提供更多詳細資訊的連結。

注意

如需編譯程式碼如何與瀏覽器環境互動的資訊,請參閱Emscripten 執行階段環境。如需檔案系統相關事項,請參閱檔案系統概觀

注意

在您可以呼叫程式碼之前,執行階段環境可能需要載入記憶體初始化檔案、預先載入檔案,或執行其他非同步作業,具體取決於最佳化和建置設定。請參閱 FAQ 中的我如何知道頁面已完全載入,並且可以安全地呼叫編譯的函式?

使用 ccall/cwrap 從 JavaScript 呼叫編譯的 C 函式

從 JavaScript 呼叫編譯的 C 函式最簡單的方法是使用ccall()cwrap()

ccall() 會以指定的參數呼叫已編譯的 C 函式並傳回結果,而 cwrap() 則會「包裝」已編譯的 C 函式並傳回您可以正常呼叫的 JavaScript 函式。因此,如果您計畫多次呼叫已編譯的函式,則 cwrap() 更有用。

請考慮下面顯示的 test/hello_function.cpp 檔案。要編譯的 int_sqrt() 函式會包裝在 extern "C" 中,以防止 C++ 名稱修飾。

// Copyright 2012 The Emscripten Authors.  All rights reserved.
// Emscripten is available under two separate licenses, the MIT license and the
// University of Illinois/NCSA Open Source License.  Both these licenses can be
// found in the LICENSE file.

#include <math.h>

extern "C" {

int int_sqrt(int x) {
  return sqrt(x);
}

}

若要編譯此程式碼,請在 Emscripten 主目錄中執行下列命令

emcc test/hello_function.cpp -o function.html -sEXPORTED_FUNCTIONS=_int_sqrt -sEXPORTED_RUNTIME_METHODS=ccall,cwrap

EXPORTED_FUNCTIONS 會告訴編譯器我們希望從編譯程式碼中存取哪些內容 (如果未使用,則可能會移除所有其他內容),而 EXPORTED_RUNTIME_METHODS 會告訴編譯器我們想要使用執行階段函式 ccallcwrap (否則,它不會包含它們)。

注意

EXPORTED_FUNCTIONS 會影響 JavaScript 編譯。如果您先編譯為物件檔,然後將物件編譯為 JavaScript,則您需要在第二個命令上使用該選項。如果您像這裡的範例一樣 (原始碼直接到 JavaScript),則一切都會正常運作。

編譯後,您可以使用下列 JavaScript,使用 cwrap() 呼叫此函式

int_sqrt = Module.cwrap('int_sqrt', 'number', ['number'])
int_sqrt(12)
int_sqrt(28)

第一個參數是要包裝的函式名稱,第二個參數是函式的傳回類型 (如果沒有,則為 JavaScript null 值),第三個參數是參數類型陣列 (如果沒有參數,則可以省略)。這些類型是「number」(對應於 C 整數、浮點數或一般指標的 JavaScript 數字)、「string」(對應於 C char* 表示字串的 JavaScript 字串) 或「array」(對應於 C 陣列的 JavaScript 陣列或類型陣列;對於類型陣列,它必須是 Uint8Array 或 Int8Array)。

您可以先在網頁瀏覽器中開啟產生的頁面 function.html (由於沒有 main(),因此頁面載入時不會發生任何事情) 來自行執行。開啟 JavaScript 環境 (在 Firefox 上按 Control-Shift-K,在 Chrome 上按 Control-Shift-J),並輸入上述命令作為三個單獨的命令,在每個命令後按 Enter。您應該會得到結果 35,這是使用 C++ 整數數學的這些輸入的預期輸出。

ccall() 類似,但會接收另一個參數,其中包含要傳遞給函式的參數

// Call C from JavaScript
var result = Module.ccall('int_sqrt', // name of C function
  'number', // return type
  ['number'], // argument types
  [28]); // arguments

// result is 5

注意

此範例說明了其他幾點,您在使用 ccall()cwrap() 時應該記住這些點

  • 這些方法可用於已編譯的 C 函式,名稱修飾的 C++ 函式無法運作。

  • 我們強烈建議您匯出要從 JavaScript 呼叫的函式

    • 匯出是在編譯時完成的。例如:-sEXPORTED_FUNCTIONS=_main,_other_function 會匯出 main()other_function()

    • 請注意,您需要在 EXPORTED_FUNCTIONS 清單中的函式名稱開頭加上 _

    • 請注意,該清單中提到了 _main。如果您沒有將其放在那裡,編譯器會將其作為無效程式碼刪除。匯出的函式清單是將保持運作的完整清單 (除非其他程式碼以其他方式保持運作)。

    • Emscripten 會執行無效程式碼刪除,以盡可能減少程式碼大小,匯出可確保不會移除您需要的函式。

    • 在較高的最佳化層級 (-O2 及更高版本) 中,程式碼會縮小,包括函式名稱。匯出函式可讓您繼續使用全域 Module 物件使用原始名稱來存取它們。

    • 如果您想要匯出 JS 程式庫函式 (例如,來自 src/library*.js 檔案的內容),則除了 EXPORTED_FUNCTIONS 之外,您還需要將其新增至 DEFAULT_LIBRARY_FUNCS_TO_INCLUDE,因為後者會強制將該方法實際包含在建置中。

  • 編譯器會移除它認為未使用的程式碼,以改善程式碼大小。如果在編譯器可見之處使用 ccall,例如在 --pre-js--post-js 中的程式碼,它會正常運作。如果在編譯器看不到的地方使用它,例如在 HTML 中的另一個 script 標籤或像本教學一樣在 JS 主控台中使用,那麼由於最佳化和縮小化,您應該使用 EXPORTED_RUNTIME_METHODS 從執行時環境匯出 ccall,例如使用 -sEXPORTED_RUNTIME_METHODS=ccall,cwrap,並在 Module 上呼叫它 (其中包含所有匯出的內容,以一種安全的方式,不受縮小化或最佳化的影響)。

從 NodeJS 與以 C/C++ 編寫的 API 互動

假設您有一個 C 函式庫公開了一些程序

//api_example.c
#include <stdio.h>
#include <emscripten.h>

EMSCRIPTEN_KEEPALIVE
void sayHi() {
  printf("Hi!\n");
}

EMSCRIPTEN_KEEPALIVE
int daysInWeek() {
  return 7;
}

使用 emcc 編譯函式庫

emcc api_example.c -o api_example.js -sMODULARIZE -sEXPORTED_RUNTIME_METHODS=ccall

要求函式庫並從 node 呼叫其程序

var factory = require('./api_example.js');

factory().then((instance) => {
  instance._sayHi(); // direct calling works
  instance.ccall("sayHi"); // using ccall etc. also work
  console.log(instance._daysInWeek()); // values can be returned, etc.
});

MODULARIZE 選項使 emcc 以模組化格式發出程式碼,該格式易於使用 require() 匯入和使用:模組的 require() 會傳回一個工廠函數,該函數可以實例化編譯後的程式碼,傳回一個 Promise 來告知我們何時準備就緒,並以參數形式提供模組的實例給我們。

(請注意,這裡我們使用 ccall,因此我們需要像之前一樣將其新增到匯出的執行時方法中。)

從 JavaScript「直接」呼叫編譯後的 C/C++ 程式碼

原始碼中的函數會變成 JavaScript 函數,因此如果您自己進行類型轉換,則可以直接呼叫它們 — 這會比使用 ccall()cwrap() 快,但會稍微複雜一些。

若要直接呼叫方法,您需要使用產生程式碼中顯示的完整名稱。這將與原始 C 函數相同,但帶有前導 _

注意

如果您使用 ccall()cwrap(),則不需要在函數呼叫前加上 _ — 只需使用 C 名稱即可。

您傳遞給函數和從函數接收的參數需要是原始值

  • 整數和浮點數可以按原樣傳遞。

  • 指標也可以按原樣傳遞,因為它們在產生的程式碼中只是整數。

  • 可以使用 ptr = stringToNewUTF8(someString) 將 JavaScript 字串 someString 轉換為 char *

    注意

    轉換為指標會配置記憶體,之後需要透過呼叫 free(ptr) (_free 在 JavaScript 端) 來釋放記憶體 -

  • 可以使用 UTF8ToString() 將從 C/C++ 接收的 char * 轉換為 JavaScript 字串。

    preamble.js 中,還有其他用於轉換字串和編碼的便利函數。

  • 其他值可以透過 emscripten::val 傳遞。請查看關於 as_handle 和 take_ownership 方法 的範例。

從 C/C++ 呼叫 JavaScript

Emscripten 提供了兩種主要方法,可從 C/C++ 呼叫 JavaScript:使用 emscripten_run_script() 執行腳本,或編寫「內嵌 JavaScript」。

最直接但稍慢的方式是使用 emscripten_run_script()。這實際上是使用 eval() 從 C/C++ 執行指定的 JavaScript 程式碼。例如,若要使用文字「hi」呼叫瀏覽器的 alert() 函數,您會呼叫下列 JavaScript

emscripten_run_script("alert('hi')");

注意

函數 alert 出現在瀏覽器中,但不會出現在 *node* 或其他 JavaScript shell 中。更通用的替代方法是呼叫 console.log

從 C 呼叫 JavaScript 的更快方法是使用 EM_JS()EM_ASM() (和相關巨集) 編寫「內嵌 JavaScript」。

EM_JS 用於從 C 檔案內宣告 JavaScript 函數。可以使用 EM_JS 撰寫「alert」範例,如下所示

#include <emscripten.h>

EM_JS(void, call_alert, (), {
  alert('hello world!');
  throw 'all done';
});

int main() {
  call_alert();
  return 0;
}

EM_JS 的實作本質上是 實作 JavaScript 函式庫 的簡寫。

EM_ASM 的使用方式與內嵌組合語言程式碼類似。可以使用內嵌 JavaScript 撰寫「alert」範例,如下所示

#include <emscripten.h>

int main() {
  EM_ASM(
    alert('hello world!');
    throw 'all done';
  );
  return 0;
}

在編譯和執行時,Emscripten 會執行兩行 JavaScript,就像它們直接出現在產生的程式碼中一樣。結果將是一個警示,然後是一個例外。(但是請注意,在幕後,即使在這種情況下,Emscripten 仍然會進行函數呼叫,這會有一些額外的負荷。)

您也可以將值從 C 傳送到 EM_ASM 中的 JavaScript,例如

EM_ASM({
  console.log('I received: ' + $0);
}, 100);

這會顯示 I received: 100

您也可以接收傳回值,例如,以下程式碼會先印出 I received: 100,然後印出 101

int x = EM_ASM_INT({
  console.log('I received: ' + $0);
  return $0 + 1;
}, 100);
printf("%d\n", x);

請參閱 emscripten.h 文件 以了解更多詳細資訊。

注意

  • 您需要使用適當的巨集 EM_ASM_INTEM_ASM_DOUBLEEM_ASM_PTR 指定傳回值是 intdouble 或指標類型。(除非使用 MEMORY64,否則 EM_ASM_PTREM_ASM_INT 相同,因此主要用於想要與 MEMORY64 相容的程式碼中。)

  • 輸入值顯示為 $0$1 等等。

  • return 用於提供從 JavaScript 傳回 C 的值。

  • 請參閱此處如何使用 {} 來括住程式碼。這是必要的,以區分程式碼和稍後傳遞的引數 (輸入值)(這是 C 巨集的工作方式)。

  • 當使用 EM_ASM 巨集時,請確保僅使用單引號 (`)。雙引號 (") 會導致編譯器無法偵測到的語法錯誤,並且僅在執行錯誤的程式碼時查看 JavaScript 主控台時才會顯示。

  • clang-format 可能會損壞 Javascript 結構,例如將 => 變成 = >。若要避免這種情況,請將下列程式碼新增至您的 .clang-formatWhitespaceSensitiveMacros: ['EM_ASM', 'EM_JS', 'EM_ASM_INT', 'EM_ASM_DOUBLE', 'EM_ASM_PTR', 'MAIN_THREAD_EM_ASM', 'MAIN_THREAD_EM_ASM_INT', 'MAIN_THREAD_EM_ASM_DOUBLE', 'MAIN_THREAD_EM_ASM_DOUBLE', 'MAIN_THREAD_ASYNC_EM_ASM']。或者,在 EM_ASM 區段之前撰寫 // clang-format off,並在其之後撰寫 // clang-format on,以關閉 clang-format

在 JavaScript 中實作 C API

可以在 JavaScript 中實作 C API!這是 Emscripten 的許多函式庫 (如 SDL1 和 OpenGL) 中使用的方法。

您可以使用它來撰寫自己的 API 以從 C/C++ 呼叫。若要執行此操作,您可以定義介面,使用 extern 裝飾,以將 API 中的方法標記為外部符號。然後,您只需將其定義新增至 library.js (預設) 即可在 JavaScript 中實作這些符號。在編譯 C 程式碼時,編譯器會在 JavaScript 函式庫中尋找相關的外部符號。

預設情況下,實作會被加入到 library.js 中(你可以在這裡找到 Emscripten 的 libc 部分)。你可以將 JavaScript 實作放在你自己的函式庫檔案中,並使用 emcc 選項 --js-library 來加入。請參閱 test/test_other.py 中的 test_js_libraries,其中包含完整可運作的範例,包括你應該在 JavaScript 函式庫檔案中使用的語法。

舉一個簡單的例子,考慮以下 C 程式碼的情況:

extern void my_js(void);

int main() {
  my_js();
  return 1;
}

注意

當使用 C++ 時,你應該將 extern void my_js(); 包在 extern "C" {} 區塊中,以防止 C++ 名稱修飾。

extern "C" {
  extern void my_js();
}

然後,你可以透過簡單地將實作加入到 library.js(或你自己的檔案)中,來在 JavaScript 中實作 my_js。就像我們其他從 C 呼叫 JavaScript 的例子一樣,下面的範例僅使用簡單的 alert() 函數建立一個對話框。

my_js: function() {
  alert('hi');
},

如果你將其加入到你自己的檔案中,你應該寫入如下內容:

addToLibrary({
  my_js: function() {
    alert('hi');
  },
});

addToLibrary 會將輸入物件的屬性複製到 LibraryManager.library(所有 JavaScript 函式庫程式碼所在的全域物件)。在這個例子中,它會在這個物件上加入一個名為 my_js 的函式。

函式庫檔案中的 JavaScript 限制

如果你不熟悉 JavaScript,例如你是一位 C/C++ 程式設計師且只是使用 emscripten,那麼以下問題可能不會出現。但如果你是一位有經驗的 JavaScript 程式設計師,你需要注意某些常見的 JavaScript 實務無法在 emscripten 函式庫檔案中以某些方式使用。

為了節省空間,預設情況下,emscripten 只會包含 C/C++ 參考的函式庫屬性。它透過對連結的 JavaScript 函式庫中每個使用的屬性呼叫 toString 來完成此操作。這表示你不能直接使用閉包,例如,因為 toString 與之不相容 - 就像使用字串建立 Web Worker 時,你也無法傳遞閉包一樣。(請注意,此限制僅適用於傳遞給 JS 函式庫中 addToLibrary 的物件的鍵的值,也就是說,頂層的鍵值對是特殊的。函數內部的程式碼當然可以有任意的 JS)。

為了避免 JS 函式庫的此限制,你可以使用 --pre-js--post-js 選項將程式碼放在另一個檔案中,這允許使用任意的標準 JS,並且它會與輸出的其餘部分一起包含和優化。這是大多數情況下建議的方法。另一個選項是另一個 <script> 標籤。

或者,如果你喜歡使用 JS 函式庫檔案,你可以讓一個函數取代自身,並在初始化期間呼叫它。

addToLibrary({

  // Solution for bind or referencing other functions directly
  good_02__postset: '_good_02();',
  good_02: function() {
    _good_02 = document.querySelector.bind(document);
  },

  // Solution for closures
  good_03__postset: '_good_03();',
  good_03: function() {
    var callCount = 0;
    _good_03 = function() {
      console.log("times called: ", ++callCount);
    };
  },

  // Solution for curry/transform
  good_05__postset: '_good_05();',
  good_05: function() {
    _good_05 = curry(scrollTo, 0);
 },

});

__postset 是一個編譯器將直接發送到輸出檔案的字串。對於上面的範例,將會發出此程式碼。

 function _good_02() {
   _good_o2 = document.querySelector.bind(document);
 }

 function _good_03() {
   var callCount = 0;
   _good_03 = function() {
     console.log("times called: ", ++callCount);
   };
 }

 function _good_05() {
   _good_05 = curry(scrollTo, 0);
};

// Call each function once so it will replace itself
_good_02();
_good_03();
_good_05();

你也可以將大部分程式碼放在 xxx__postset 字串中。下面的範例中,每個方法都宣告對 $method_support 的依賴,否則都是虛設函數。$method_support 本身具有一個對應的 __postset 屬性,其中包含將各種方法設定為我們實際想要的函數的所有程式碼。

addToLibrary({
  $method_support: {},
  $method_support__postset: [
    '(function() {                                  ',
    '  var SomeLib = function() {                   ',
    '    this.callCount = 0;                        ',
    '  };                                           ',
    '                                               ',
    '  SomeLib.prototype.getCallCount = function() {',
    '    return this.callCount;                     ',
    '  };                                           ',
    '                                               ',
    '  SomeLib.prototype.process = function() {     ',
    '    ++this.callCount;                          ',
    '  };                                           ',
    '                                               ',
    '  SomeLib.prototype.reset = function() {       ',
    '    this.callCount = 0;                        ',
    '  };                                           ',
    '                                               ',
    '  var inst = new SomeLib();                    ',
    '  _method_01 = inst.getCallCount.bind(inst);   ',
    '  _method_02 = inst.process.bind(inst);        ',
    '  _method_03 = inst.reset.bind(inst);          ',
    '}());                                          ',
  ].join('\n'),
  method_01: function() {},
  method_01__deps: ['$method_support'],
  method_02: function() {},
  method_01__deps: ['$method_support'],
  method_03: function() {},
  method_01__deps: ['$method_support'],
 });

注意:如果你使用 node 4.1 或更新版本,你可以使用多行字串。它們僅在編譯時使用,而非執行時,因此輸出仍然會在基於 ES5 的環境中執行。

另一個選項是將大部分程式碼放在一個物件中,而不是一個函數。

addToLibrary({
  $method_support__postset: 'method_support();',
  $method_support: function() {
    var SomeLib = function() {
      this.callCount = 0;
    };

    SomeLib.prototype.getCallCount = function() {
      return this.callCount;
    };

    SomeLib.prototype.process = function() {
      ++this.callCount;
    };

    SomeLib.prototype.reset = function() {
      this.callCount = 0;
    };

    var inst = new SomeLib();
    _method_01 = inst.getCallCount.bind(inst);
    _method_02 = inst.process.bind(inst);
    _method_03 = inst.reset.bind(inst);
  },
  method_01: function() {},
  method_01__deps: ['$method_support'],
  method_02: function() {},
  method_01__deps: ['$method_support'],
  method_03: function() {},
  method_01__deps: ['$method_support'],
 });

請參閱 library_*.js 檔案以獲取其他範例。

注意

  • JavaScript 函式庫可以宣告相依性(__deps),但是這些相依性僅適用於其他 JavaScript 函式庫。請參閱 /src 中名稱格式為 library_*.js 的範例

  • 你可以使用 autoAddDeps(myLibrary, name) 為所有方法新增相依性,其中 myLibrary 是包含所有方法的物件,而 name 是它們都依賴的項目。當所有已實作的方法都使用包含 Helper 方法的 JavaScript 單例時,這非常有用。請參閱 library_webgl.js 以獲取範例。

  • 傳遞給 addToLibrary 的鍵會產生以 _ 開頭的函數。換句話說,my_func: function() {}, 會變成 function _my_func() {},因為 emscripten 中的所有 C 方法都有 _ 前綴。以 $ 開頭的鍵會移除 $,並且不會新增底線。

從 C 呼叫 JavaScript 函數作為函數指標

你可以使用 addFunction 來傳回代表函數指標的整數值。然後將該整數傳遞給 C 程式碼,可以將該值作為函數指標呼叫,並且將呼叫你傳遞給 addFunction 的 JavaScript 函數。

請參閱 test_add_function in test/test_core.py 以獲取範例。

你應該使用 -sALLOW_TABLE_GROWTH 進行建置,以允許將新函數加入到表格中。否則,預設情況下,表格具有固定大小。

當使用帶有 JavaScript 函數的 addFunction 時,你需要提供一個額外的第二個引數,即 Wasm 函數簽名字串,如下所述。請參閱 test/interop/test_add_function_post.js 以獲取範例。

函數簽名

當使用 addFunction 和在 JavaScript 函式庫中使用時,LLVM Wasm 後端需要 Wasm 函數簽名字串。簽名字串中的每個字元都代表一個類型。第一個字元代表函數的傳回類型,其餘字元則代表參數類型。

  • 'v':void 類型

  • 'i':32 位元整數類型

  • 'j':64 位元整數類型(請參閱下方註解)

  • 'f':32 位元浮點數類型

  • 'd':64 位元浮點數類型

  • 'p':32 位元或 64 位元指標 (MEMORY64)

例如,如果你加入一個採用整數且不傳回任何內容的函數,則簽名為 'vi'

當使用 'j' 時,將會有多種方式將參數值傳遞給 JavaScript。預設情況下,該值將會被當作單一 BigInt 或一對 JavaScript 數字 (double) 傳遞,具體取決於是否啟用 WASM_BIGINT 設定。此外,如果你只需要 53 位元的精確度,你可以新增 __i53abi 修飾符,這將會忽略高位元,並且該值將會以單一 JavaScript 數字 (double) 接收。它不能與 addFunction 一起使用。以下是一個函式庫函數的範例,該函數使用以 53 位元 (double) 傳遞的 64 位元值設定檔案大小,並傳回整數錯誤碼

extern "C" int _set_file_size(int handle, uint64_t size);
_set_file_size__i53abi: true,  // Handle 64-bit
_set_file_size__sig: 'iij',    // Function signature
_set_file_size: function(handle, size) { ... return error; }

在連結時使用 -sWASM_BIGINT 是在函式庫中處理 64 位元類型的另一種方法。可能需要在 JavaScript 端使用 `Number()` 來將其轉換為可用的值。請參閱 設定參考

從 JavaScript 存取記憶體

你可以使用 getValue(ptr, type)setValue(ptr, value, type) 來存取記憶體。第一個引數是指標(表示記憶體位址的數字)。type 必須是 LLVM IR 類型,即 i8i16i32i64floatdouble 或指標類型,例如 i8*(或只是 *)。

在測試中使用了這些函數的範例 — 請參閱 test/core/test_utf.intest/test_core.py

注意

這是比 ccall()cwrap() 更低階的操作 — 我們需要注意正在使用的特定類型(例如整數)。

您也可以透過操作代表記憶體的陣列「直接」存取記憶體。除非您確定知道自己在做什麼,並且需要比 getValue()setValue() 更快的速度,否則不建議這樣做。

一個可能需要這樣做的情況是,如果您想從 JavaScript 導入大量資料,以供編譯後的程式碼處理。例如,以下程式碼會配置一個緩衝區、複製一些資料進去、呼叫一個 C 函數來處理資料,最後釋放該緩衝區。

var buf = Module._malloc(myTypedArray.length*myTypedArray.BYTES_PER_ELEMENT);
Module.HEAPU8.set(myTypedArray, buf);
Module.ccall('my_function', 'number', ['number'], [buf]);
Module._free(buf);

這裡的 my_function 是一個 C 函數,它接收一個整數參數(或者一個指標,對我們來說它們都只是 32 位元的整數)並回傳一個整數。這可能是像 int my_function(char *buf) 這樣的函數。

當基於 Wasm 的記憶體允許透過使用 -sALLOW_MEMORY_GROWTH 編譯來 **擴增** 時,將配置的記憶體匯出到 JavaScript 的相反情況可能會很棘手。增加記憶體大小會變更到新的緩衝區,而現有的陣列視圖基本上會失效,因此您不能簡單地這樣做

function func() {
  let someView = HEAPU8.subarray(x, y);
  compute(someView);

  // This may grow memory, which would invalidate all views.
  maybeGrow();

  // If we grew, this use of an invalidated view will fail. Failure in this
  // case will return undefined, the same as reading out of bounds from a
  // typed array. If the operation were someView.subarray(), however, then it
  // would throw an error.
  return someView[z];
}

Emscripten 會為您刷新像 HEAPU8 這樣的標準視圖,您可以使用這些視圖來刷新您自己的視圖

function func() {
  let someView = HEAPU8.subarray(x, y);
  compute(someView);

  // This may grow memory, which would invalidate all views.
  maybeGrow();

  // Create a new, fresh view after the possible growth.
  someView = HEAPU8.subarray(x, y);
  return someView[z];
}

另一個避免此類問題的方法是在適當的情況下複製資料。

影響執行行為

Module 是一個全域 JavaScript 物件,其屬性會在 Emscripten 產生的程式碼執行時的各個點被呼叫。

開發人員提供 Module 的實作,以控制如何顯示來自 Emscripten 的通知、在主迴圈執行前載入哪些檔案,以及其他一些行為。有關更多資訊,請參閱 Module 物件

環境變數

有時,編譯後的程式碼需要存取環境變數(例如,在 C 語言中,透過呼叫 getenv() 函數)。Emscripten 產生的 JavaScript 無法直接存取電腦的環境變數,因此提供了一個「虛擬化」的環境。

JavaScript 物件 ENV 包含虛擬化的環境變數,您可以透過修改它來將變數傳遞給編譯後的程式碼。必須注意確保 ENV 變數已由 Emscripten 初始化後再進行修改 — 使用 Module.preRun 是一種方便的方式。

例如,若要將環境變數 MY_FILE_ROOT 設定為 "/usr/lib/test/",您可以將以下 JavaScript 新增到您的 Module 設定程式碼

Module.preRun = () => {ENV.MY_FILE_ROOT = "/usr/lib/test"};

請注意,如果您沒有設定自己的值,Emscripten 會在您設定 ENV 後,為某些環境變數(例如 LANG)設定預設值。如果您希望這些變數保持未設定狀態,您可以將它們的值明確設定為 undefined。例如

Module.preRun = () => {ENV.LANG = undefined};

綁定 C++ 和 JavaScript — WebIDL Binder 和 Embind

用於呼叫編譯後的 C 函數的 JavaScript 方法效率很高,但不能與名稱修飾的 C++ 函數一起使用。

WebIDL BinderEmbind 會在 C++ 和 JavaScript 之間建立綁定,允許從 JavaScript 以自然的方式使用 C++ 程式碼實體。Embind 還支援從 C++ 程式碼呼叫 JavaScript 程式碼。

Embind 可以綁定幾乎任何 C++ 程式碼,包括複雜的 C++ 結構(例如 shared_ptrunique_ptr)。WebIDL Binder 支援可以在 WebIDL 中表達的 C++ 型別。雖然此子集比 Embind 支援的範圍小,但對於大多數用例來說已綽綽有餘 — 使用 binder 移植的專案範例包括 Box2DBullet 物理引擎。

這兩種工具都允許以類似的方式從 JavaScript 使用對應的項目。但是,它們在不同的層級運作,並使用非常不同的方法來定義綁定

  • Embind 在 C/C++ 檔案中宣告綁定。

  • WebIDL-Binder 在單獨的檔案中宣告綁定。此檔案會透過 binder 工具執行,以建立「膠合」程式碼,然後與專案一起編譯。

注意

沒有強有力的證據表明一種工具在效能方面比另一種「更好」(不存在比較基準),並且這兩種工具都已在許多專案中成功使用。選擇其中一種工具通常基於哪一種最適合專案及其建置系統。

綁定 C/C++ 和 JavaScript - Node-API

Emnapi 是一個非官方的 Node-API 實作,可用於 Emscripten。如果您想將現有的 Node-API 附加元件移植到 WebAssembly,或將相同的綁定程式碼編譯為 Node.js 原生附加元件和 WebAssembly,您可以試試看。有關更多詳細資訊,請參閱 Emnapi 文件