將新的 C/C++ 模組編譯為 WebAssembly
當您用 C/C++ 等語言編寫新的程式碼模組時,可以使用 Emscripten 等工具將其編譯為 WebAssembly。讓我們來看看它是如何工作的。
Emscripten 環境設定
首先,讓我們設定必要的開發環境。
先決條件
按照以下說明獲取 Emscripten SDK:https://emscripten.org/docs/getting_started/downloads.html
編譯示例
環境設定完畢後,讓我們看看如何使用它將 C 示例編譯為 Wasm。使用 Emscripten 編譯時,有多種選項可用,但我們將介紹的兩種主要情況是
- 編譯為 Wasm 並建立 HTML 來執行我們的程式碼,以及執行 Wasm 在 Web 環境中所需的所有 JavaScript“粘合”程式碼。
- 編譯為 Wasm 並僅建立 JavaScript。
我們將在下面介紹這兩種方法。
建立 HTML 和 JavaScript
這是我們將介紹的最簡單的情況,您可以讓 emscripten 生成執行程式碼所需的一切,包括 WebAssembly 在瀏覽器中的執行方式。
- 首先,我們需要一個示例來編譯。複製以下簡單的 C 示例,並將其儲存在本地驅動器上新目錄中的名為
hello.c的檔案中cpp#include <stdio.h> int main() { printf("Hello World\n"); return 0; } - 現在,使用您用來進入 Emscripten 編譯器環境的終端視窗,導航到與您的
hello.c檔案相同的目錄,並執行以下命令bashemcc hello.c -o hello.html
我們透過命令傳入的選項如下
-o hello.html— 指定我們希望 Emscripten 生成一個 HTML 頁面來執行我們的程式碼(以及要使用的檔名),以及 Wasm 模組和 JavaScript“粘合”程式碼來編譯和例項化 Wasm,以便它可以在 Web 環境中使用。
此時,您的源目錄中應該有
- 二進位制 Wasm 模組程式碼 (
hello.wasm) - 一個 JavaScript 檔案,其中包含粘合程式碼,用於在原生 C 函式與 JavaScript/Wasm 之間進行轉換 (
hello.js) - 一個 HTML 檔案,用於載入、編譯和例項化您的 Wasm 程式碼,並在瀏覽器中顯示其輸出 (
hello.html)
執行您的示例
現在,剩下的就是將生成的 hello.html 載入到支援 WebAssembly 的瀏覽器中。從 Firefox 52、Chrome 57、Edge 57、Opera 44 開始,它預設啟用。
注意:如果您嘗試直接從本地硬碟驅動器開啟生成的 HTML 檔案 (hello.html)(例如 file://your_path/hello.html),您將收到類似於 both async and sync fetching of the wasm failed 的錯誤訊息。您需要透過 HTTP 伺服器 (http://) 執行 HTML 檔案 — 有關更多資訊,請參閱 如何設定本地測試伺服器?。
如果一切按計劃進行,您應該在網頁上和瀏覽器的 JavaScript 控制檯中看到 Emscripten 控制檯中顯示的“Hello world”輸出。恭喜,您剛剛將 C 編譯為 WebAssembly 並將其在瀏覽器中執行! 
使用自定義 HTML 模板
有時您會想要使用自定義 HTML 模板。讓我們看看如何做到這一點。
- 首先,將以下 C 程式碼儲存在名為
hello2.c的檔案中,並將該檔案儲存在新目錄中cpp#include <stdio.h> int main() { printf("Hello World\n"); return 0; } - 在您的 emsdk 儲存庫中搜索
shell_minimal.html檔案。將其複製到您先前的新目錄中的名為html_template的子目錄中。 - 現在,導航到您的新目錄(同樣,在 Emscripten 編譯器環境終端視窗中),並執行以下命令我們這次傳入的選項略有不同bash
emcc -o hello2.html hello2.c -O3 --shell-file html_template/shell_minimal.html- 我們指定了
-o hello2.html,這意味著編譯器仍然會輸出 JavaScript 粘合程式碼和.html。 - 我們指定了
-O3,用於最佳化程式碼。Emcc 具有與其他 C 編譯器相同的最佳化級別,包括:-O0(無最佳化)、-O1、-O2、-Os、-Oz、-Og和-O3。-O3是發行版構建的良好設定。 - 我們還指定了
--shell-file html_template/shell_minimal.html— 這提供了要用於建立將執行示例的 HTML 的 HTML 模板的路徑。
- 我們指定了
- 現在讓我們執行這個示例。上面的命令將生成
hello2.html,它將具有與模板相同的內容,並添加了一些粘合程式碼來載入生成的 Wasm、執行它等等。在瀏覽器中開啟它,您將看到與上一個示例相同的輸出。
注意:您可以透過在 -o 標誌中指定 .js 檔案而不是 HTML 檔案來指定僅輸出 JavaScript“粘合”檔案*,而不是完整的 HTML,例如 emcc -o hello2.js hello2.c -O3。然後,您可以從頭開始構建自己的自定義 HTML,儘管這是一種高階方法;使用提供的 HTML 模板通常更容易。
- Emscripten 需要大量 JavaScript“粘合”程式碼來處理記憶體分配、記憶體洩漏以及許多其他問題
呼叫在 C 中定義的自定義函式
如果您想從 JavaScript 呼叫在 C 程式碼中定義的函式,可以使用 Emscripten ccall() 函式和 EMSCRIPTEN_KEEPALIVE 宣告,它將您的函式新增到匯出的函式列表中(請參閱 為什麼當我編譯到 JavaScript 時,我的 C/C++ 原始碼中的函式會消失,或者我收到“沒有函式可處理”?)。讓我們看看它是如何工作的。
- 首先,將以下程式碼儲存在名為
hello3.c的檔案中,並將該檔案儲存在新目錄中預設情況下,Emscripten 生成的程式碼始終只調用cpp#include <stdio.h> #include <emscripten/emscripten.h> int main() { printf("Hello World\n"); return 0; } #ifdef __cplusplus #define EXTERN extern "C" #else #define EXTERN #endif EXTERN EMSCRIPTEN_KEEPALIVE void myFunction(int argc, char ** argv) { printf("MyFunction Called\n"); }main()函式,其他函式將被視為死程式碼並被刪除。在函式名前面加上EMSCRIPTEN_KEEPALIVE可以阻止這種情況發生。您還需要匯入emscripten.h庫才能使用EMSCRIPTEN_KEEPALIVE。注意:我們包括了
#ifdef塊,以便如果您嘗試將其包含在 C++ 程式碼中,該示例仍將有效。由於 C 與 C++ 的名稱修飾規則,否則這將導致錯誤,但在這裡我們將其設定為,如果使用 C++,則將其視為外部 C 函式。 - 現在將包含
{{{ SCRIPT }}}作為內容的html_template/shell_minimal.html也新增到這個新目錄中,只是為了方便(在實際的開發環境中,您顯然會將它放在一箇中心位置)。 - 現在讓我們再次執行編譯步驟。從最新的目錄(以及在 Emscripten 編譯器環境終端視窗中)內部,使用以下命令編譯您的 C 程式碼。請注意,我們需要使用
NO_EXIT_RUNTIME編譯:否則,當main()退出時,執行時將被關閉,呼叫已編譯的程式碼將無效。這對於正確的 C 模擬是必要的:例如,確保atexit()函式被呼叫。bashemcc -o hello3.html hello3.c --shell-file html_template/shell_minimal.html -s NO_EXIT_RUNTIME=1 -s "EXPORTED_RUNTIME_METHODS=['ccall']" - 如果您再次在瀏覽器中載入示例,您將看到與之前相同的輸出!
- 現在,我們需要從 JavaScript 執行新的
myFunction()函式。首先,在文字編輯器中開啟您的 hello3.html 檔案。 - 新增一個
<button>元素,如下所示,位於第一個開啟的<script type='text/javascript'>標籤之上。html<button id="mybutton">Run myFunction</button> - 現在,將以下程式碼新增到第一個
<script>元素的末尾jsdocument.getElementById("mybutton").addEventListener("click", () => { alert("check console"); const result = Module.ccall( "myFunction", // name of C function null, // return type null, // argument types null, // arguments ); });
這說明了如何使用 ccall() 呼叫匯出的函式。
另請參閱
- emscripten.org — 瞭解有關 Emscripten 及其眾多選項的更多資訊。
- 使用 ccall/cwrap 從 JavaScript 呼叫已編譯的 C 函式
- 為什麼當我編譯到 JavaScript 時,我的 C/C++ 原始碼中的函式會消失,或者我收到“沒有函式可處理”?
- 將現有的 C 模組編譯為 WebAssembly