將新的 C/C++ 模組編譯為 WebAssembly
當您使用 C/C++ 等語言編寫了一個新的程式碼模組後,可以使用 Emscripten 等工具將其編譯為 WebAssembly。讓我們看看它是如何工作的。
Emscripten 環境設定
首先,讓我們設定必要的開發環境。
預備知識
按照以下說明獲取 Emscripten SDK:https://emscripten.org/docs/getting_started/downloads.html
編譯示例
環境設定完成後,讓我們看看如何使用它將 C 示例編譯為 Wasm。Emscripten 編譯時有許多可用選項,但我們將介紹的兩個主要場景是:
- 編譯為 Wasm 並建立 HTML 來執行我們的程式碼,以及執行 Wasm 所需的所有 JavaScript “膠水”程式碼。
- 編譯為 Wasm 並僅建立 JavaScript。
我們將在下面介紹這兩種情況。
建立 HTML 和 JavaScript
這是我們將介紹的最簡單的情況,即讓 emscripten 生成您在瀏覽器中以 WebAssembly 形式執行程式碼所需的一切。
-
首先,我們需要一個示例來編譯。複製以下簡單的 C 示例,並將其儲存在本地驅動器上的新目錄中的一個名為
hello.c的檔案中。c#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 “膠水”程式碼,以便在 Web 環境中編譯和例項化 Wasm 以便使用。
此時,在您的源目錄中應該有:
- 二進位制 Wasm 模組程式碼 (
hello.wasm) - 一個 JavaScript 檔案,包含用於在原生 C 函式、JavaScript/Wasm 之間進行轉換的膠水程式碼 (
hello.js) - 一個 HTML 檔案,用於載入、編譯、例項化您的 Wasm 程式碼,並在瀏覽器中顯示其輸出 (
hello.html)
執行您的示例
現在,您只需在支援 WebAssembly 的瀏覽器中載入生成的 hello.html 即可。從 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 檔案 — 有關更多資訊,請參閱如何設定本地測試伺服器?。
如果一切按計劃進行,您應該會在網頁上出現的 Emscripten 控制檯以及瀏覽器的 JavaScript 控制檯中看到“Hello world”的輸出。恭喜,您剛剛將 C 編譯為 WebAssembly 並在瀏覽器中運行了它! 
使用自定義 HTML 模板
有時您會想使用自定義 HTML 模板。讓我們看看如何做到這一點。
-
首先,將以下 C 程式碼儲存在一個名為
hello2.c的檔案中,放在一個新的目錄中。c#include <stdio.h> int main() { printf("Hello World\n"); return 0; } -
在您的 emsdk 倉庫中搜索
shell_minimal.html檔案。將其複製到您的新目錄中的一個名為html_template的子目錄中。 -
現在導航到您的新目錄(同樣,在您的 Emscripten 編譯器環境終端視窗中),然後執行以下命令:
bashemcc -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 模板的路徑,您將透過該 HTML 執行您的示例。
- 我們指定了
-
現在讓我們執行這個示例。上面的命令將生成
hello2.html,它將與模板具有大致相同的內容,並添加了一些膠水程式碼來載入生成的 Wasm,執行它等。在瀏覽器中開啟它,您將看到與上一個示例大致相同的輸出。
編譯為 JavaScript 模組
您可以指定僅輸出 JavaScript “膠水”檔案(Emscripten 需要大量 JavaScript “膠水”程式碼來處理記憶體分配、記憶體洩漏以及許多其他問題),而不是完整的 HTML,只需在 -o 標誌中指定一個 .js 檔案而不是 HTML 檔案,如下所示:
emcc -o hello.js hello.c -O3
然後,您可以將此 JavaScript 檔案合併到您的程式中,這在使用打包器並且不直接處理 HTML 時特別有用。例如,您可以匯入生成的 JavaScript 膠水檔案,使其作為副作用執行。在您的應用程式的入口模組中,新增:
import "./hello.js";
或者,您可以生成一個工廠模組,它允許您建立模組的多個例項(預設情況下,膠水程式碼會全域性載入模組,導致多個例項發生衝突)。
emcc -o hello.mjs hello.c -O3 -sMODULARIZE
注意: 如果您的輸出副檔名是 .js 而不是 .mjs,那麼您必須新增 -sEXPORT_ES6 設定來輸出 JavaScript 模組。
然後,在您的程式碼中匯入工廠並呼叫它:
import createModule from "./hello.mjs";
createModule().then((Module) => {
console.log("Wasm ready", Module);
});
呼叫 C 中定義的自定義函式
如果您想從 JavaScript 呼叫 C 程式碼中定義的函式,您可以使用 Emscripten 的 ccall() 函式和 EMSCRIPTEN_KEEPALIVE 宣告,它會將您的函式新增到已匯出函式的列表中(請參閱為什麼我 C/C++ 原始碼中的函式在編譯到 JavaScript 時會消失,以及/或者我收到“沒有要處理的函式”?)。讓我們看看它是如何工作的。
-
首先,將以下程式碼儲存為
hello3.c並放在一個新的目錄中:c#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"); }預設情況下,Emscripten 生成的程式碼總是隻呼叫
main()函式,其他函式會被視為死程式碼而被刪除。在函式名前面加上EMSCRIPTEN_KEEPALIVE可以阻止這種情況發生。您還需要包含emscripten.h庫才能使用EMSCRIPTEN_KEEPALIVE。注意: 我們包含了
#ifdef塊,以便如果您嘗試將其包含在 C++ 程式碼中,示例仍然可以工作。由於 C 和 C++ 的名稱修飾規則,這通常會導致失敗,但在這裡我們將其設定為當使用 C++ 時,它被視為外部 C 函式。 -
現在,為了方便起見,也將
html_template/shell_minimal.html與{{{ SCRIPT }}}內容一起新增到這個新目錄中(在您的真實開發環境中,您顯然會將其放在一箇中心位置)。 -
現在讓我們再次執行編譯步驟。從您的最後一個目錄中(同時在您的 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="my-button">Run myFunction</button> -
現在,在第一個
<script>元素的末尾新增以下程式碼:jsdocument.getElementById("my-button").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 函式
- 為什麼我 C/C++ 原始碼中的函式在編譯到 JavaScript 時會消失,以及/或者我收到“沒有要處理的函式”?
- 將現有的 C 模組編譯為 WebAssembly