從 C/C++ 編譯 WebAssembly
當你使用 C/C++ 之類的語言編寫模組時,你可以使用 Emscripten 等工具將它編譯成 WebAssembly。讓我們來看看它是如何做到的。
Emscripten 環境配置
首先,讓我們來配置所需要的開發環境。
所需條件
利用這些說明,取得 Emscripten SDK:https://emscripten.org/docs/getting_started/downloads.html
編譯範例
環境配置完畢了,讓我們看看如何使用它將 C 的程式編譯成 Wasm。使用 Emscripten 來編譯的時候,有很多種不同的作法。其中,主要有 2 種:
- 編譯到 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
——指定這個選項將會生成 HTML 頁面來運行我們的程式,並且會產生 Wasm 模組,編譯和實例化 Wasm 模組所需要的 JavaScript 膠水程式碼,讓程式能直接在 web 環境使用。
這個時候在你的原始碼資料夾應該有下列文件:
hello.wasm
二進制的 Wasm 模組程式碼hello.js
一個包含了用來在原生 C 函數和 JavaScript/Wasm 之間轉換的膠水程式碼的 JavaScript 檔案hello.html
一個用來加載、編譯、實例化你的 Wasm 程式,並將它輸出在瀏覽器顯示上的 HTML 檔案
運行你的範例
現在只要使用一個支援 WebAssembly 的瀏覽器,加載產生的 hello.html
即可。自從 Firefox 52、Chrome 57、Edge 57 和 Opera 44 開始,已經預設啟用了 WebAssembly。
備註:如果你試圖直接從本機硬碟打開產生的 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
中:cpp#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 頁面模板。
- 我們使用了
-
下面讓我們來運行這個例子。上面的指令已經產生了
hello2.html
,內容和我們使用的模板非常相像,只不過多加了一些 JavaScript 膠水程式碼和加載 Wasm 程式的程式碼。在瀏覽器中打開它,你會看到與上一個範例相同的輸出。
備註:你可以在 -o
選項中指定 .js 檔案而不是 HTML 檔案,例如 emcc -o hello2.js hello2.c -O3
,從而只輸出 JavaScript 膠水程式碼,而不是完整的 HTML 檔案。這樣,你就可以完全從頭開始建立自訂 HTML,儘管這是一種進階用法;使用提供的 HTML 模板通常會更簡單。
- Emscripten 需要大量的 JavaScript 膠水程式碼來處理記憶體分配、記憶體洩漏等一系列問題
調用一個定義在 C 中的自訂方法
如果要調用一個在 C 語言自訂的函式,你可以使用 Emscripten 中的 ccall()
函式,以及 EMSCRIPTEN_KEEPALIVE
宣告,從而將你的函式添加到導出函式列表中(詳見為什麼當我要將 C/C++ 編譯成 JavaScript 時,原始碼中的函式會消失且/或沒有函式可以處理?)。
讓我們看看這是怎麼實現的。
-
首先,將以下程式碼在新資料夾中儲存為
hello3.c
: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"); }
預設情況下,Emscripten 生成的程式只會調用
main()
函式,其他的函式將被無視。在函式名稱之前添加EMSCRIPTEN_KEEPALIVE
能夠防止這樣的事情發生。你需要導入emscripten.h
來使用EMSCRIPTEN_KEEPALIVE
。備註:我們使用了
#ifdef
,來使得你在引用這段 C++ 程式時,這個範例一樣會運作。由於 C 與 C++ 中名字修飾規則的差異,這有可能發生問題,但目前我們這樣設定能保證你使用 C++ 時,這些代碼會被視為外部 C 語言函式。 -
為了方便起見,現在將
html_template/shell_minimal.html
也添加到這一資料夾(但在實際開發環境中你肯定需要將其放到某一特定位置)。 -
運行以下指令編譯。請注意,我們需要使用
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']"
-
如果你在瀏覽器中在此加載實例,你將看到和之前相同的結果。
-
現在我們需要運行新的
myFunction()
JavaScript 函數。 -
首先,在
<script type='text/javascript'>
開頭標籤之前,依照以下範例添加一個<button>
。html<button id="mybutton">Run myFunction</button>
-
在第一個
<script>
元素(位於</script>
關閉標籤前)中添加以下程式碼:jsdocument.getElementById("mybutton").addEventListener("click", () => { alert("check console"); const result = Module.ccall( "myFunction", // C 函式的名稱 null, // 回傳值的類型 null, // 引數的類型 null, // 引數 ); });
以上就是如何使用 ccall()
調用來導出的函數的方式。