C/C++與JavaScript的WebAssembly編程(一)
1. JavaScript與C/C++混合編程的技術
JavaScript與C++混合編程可以實現兩種語言的優勢結合,C++的程序性能很高且支持強大的系統調用能力,JavaScript則生態豐富且開發效率高。
JavaScript與C++混合編程常見的技術手段主要有以下幾種:
- Node.js的C++擴展: 常用于Node.js實現的后端服務代碼。在WebAssembly出現之前,Node.js的服務器代碼通常以這種方式調用C/C++的模塊。
- JsAPI和Native API: 常應用于包含WebView的客戶端。將Native端的代碼封裝成Web接口(JsAPI)供前端調用,將Web端的代碼封裝成Native接口供Native調用。像Electron、CEF技術棧的客戶端均采用了此種方式。
- WebAssembly: 主要用于瀏覽器上運行的前端頁面,Node.js從8.0開始也支持WebAssembly,因此也可用于服務端開發。
本章所講的內容是基于WebAssembly的混合編程技術。
2. 什么是WebAssembly?
WebAssembly是一種新的編碼方式,是一種為web設計的高效、低級字節碼格式。我們可以將C/C++、Rust等低級語言編寫的代碼編譯成WebAssembly字節碼,現代的Web瀏覽器可以加載WebAssembly,并與JavaScript協同運行。從而使得WebAssembly成為JavaScript與C/C++混合編程并在Web上運行的最有效機制。C/C++編譯成的WebAssembly能夠以接近原生語言的效率在瀏覽器上運行。
2.1. 支持WebAssembly的瀏覽器
支持WebAssembly的常用瀏覽器及版本:
- Chrome 57及以上版本。
- Firefox 52及以上版本。
- Edge 16(基于Chromium的版本)及以上。
- Safari 11及以上版本。
- Opera 44及以上版本。
參考信息: https://caniuse.com/wasm
此外,Node.js從8.0版本也開始支持WebAssembly,WebAssembly目前已經成了W3C的Web標準之一。
2.2. WebAssembly的編譯器
除了C/C++外,WebAssembly還支持多種其他計算機語言編譯成.wasm,常見的語言和編譯器如下:
- C/C++: emscripten編譯工具鏈可以將 C/C++ 編譯成 WebAssembly。
- Rust: wasm-pack:工具可以將 Rust 編譯成 WebAssembly。
- Go: Go語言的官方工具就鏈支持將 Go 編譯成 WebAssembly。
3. 開發環境搭建
-
emscripten官方文檔: https://emscripten.org/docs/getting_started/downloads.html
-
依賴的環境準備
- git
- Python3.6或更新版本(Windows)
-
安裝步驟
# 1. 從Github上克隆emsdk倉庫 # emsdk即Emscripten SDK,是將C/C++編譯成WebAssembly的工具 git clone https://github.com/emscripten-core/emsdk.git # 2. 進入emsdk目錄 cd emsdk # 3. 下載和安裝最新的SDK tools(包括node.js、emscripten等) # Linux/macOS: ./emsdk install latest # Windows: ./emsdk.bat install latest # (安裝大概需要十幾分鐘的時間,可以去喝杯茶休息一下了) # 會將相關的工具安裝在以下三個目錄 # emsdk/node # emsdk/upstream # emsdk/python (Windows才有,會安裝nuget) # 4. 為當前用戶設置latest版本為當前激活的工具 # Linux/macOS: ./emsdk activate latest # Windows: ./emsdk.bat activate latest # 5. 為當前命令終端設置環境變量 # Linux/macOS: source ./emsdk_env.sh # Windows: ./emsdk_env.bat # 6. 驗證是否安裝成功 emcc -v # (如果有顯示正常的版本信息,則說明安裝成功)以上示例基于3.1.72版本的emscripten。
4. Hello World程序
我們從一個Hello World程序開始,了解WebAssembly程序的開發、編譯、運行的大致流程。
-
新建一個測試目錄
hello_world和源碼文件hello.cpp。// hello_world/hello.cpp #include <iostream> int main() { std::cout << "Hello World from C++" << std::endl; return 0; } -
執行以下命令編譯為WebAssembly
emcc hello.cpp -o hello.html編譯后會生成如下三個文件:
./hello_world ├── hello.html # emscripten的測試頁面,用來展示輸出內容的HTML頁面。 ├── hello.js # 是Emscripten生成的膠水代碼,其中包含了Emscripten的運行環境和.wasm文件的封裝。 └── hello.wasm # 二進制的字節碼文件 -
在當前Demo目錄下啟動一個http-server服務,可以用python或node.js工具。
# 進入目錄 cd hello_world # 在當前目錄啟動http-server服務 # Python3的用法 python -m http.server # Python2的用法 python -m SimpleHTTPServer -
在支持WebAssembly的瀏覽器中打開
http://localhost:8000/hello.html頁面,正常情況下就可以看到輸出內容(Hello World from C++)了。![file]()
5. emscripten常見用法
5.1. emscripten編譯流程
Emscripten的誕生早于WebAssembly。WebAssembly出現之前,Emscripten的編譯目標時asm.js,即Emscripten的主要功能是將C/C++代碼編譯成JavaScript代碼。Emscripten在1.37.3開始正式支持WebAssembly,可以根據編譯選項設置編譯目標為asm.js或WebAssembly。
Emscripten的編譯流程如下:

- C/C++代碼先通過Clang編譯為LLVM的字節碼,然后再根據設置的不同目標編譯為asm.js或WebAssembly(
.wasm)。 - 可以通過
-s WASM=1或-s WASM=0來設置編譯目標,Emscripten自v1.38.1開始,默認的缺省編譯選項為WASM=1,之前的版本默認為WASM=0。 - 相比于asm.js,.wasm具有體積小、執行效率高的優勢,因此一般會優先選擇.wasm作為編譯目標。當然,在實際的項目中,為了增加程序的兼容性,可能會同時構建兩個編譯目標,在支持WebAssembly的瀏覽器中加載.wasm,不支持WebAssembly的老版本瀏覽器中降級為asm.js。
5.2. emsdk常見用法
emsdk是emscripten工具鏈最核心的部分,emsdk是將C/C++編譯成WebAssembly的編譯工具,其用法與Clang/GCC有點相似。
1. 最簡單用法。
編譯指令:emcc ./hello.cpp結果文件:a.out.wasm: 為C/C++源文件編譯后形成的WebAssembly匯編文件,是一個二進制的字節碼文件。a.out.js: 是Emscripten生成的膠水代碼,其中包含了Emscripten的運行環境和.wasm文件的封裝,導入a.out.js即可自動完成.wasm文件的載入、編譯、實例化、運行時初始化等繁雜的工作。
2. -o選項
-o選項可以指定輸出的文件名和文件類型。
【demo1】
編譯指令:emcc ./hello.cpp -o hello.js結果文件:hello.wasm: 與a.out.wasm文件相同。hello.js: 與a.out.js文件相同。
【demo2】
編譯指令:emcc ./hello.cpp -o hello2.html結果文件:hello2.wasm: 與a.out.wasm文件相同。hello2.js: 與a.out.js文件相同。hello2.html: emscripten的測試頁面,用來展示輸出內容的HTML頁面。
3. -s選項。
-s選項是一個用于設置編譯目標和編譯屬性的重要選項:
-s WASM=1: 指定輸出為WebAssembly式,這能提升執行性能,WASM=1是默認的缺省參數,此選項會生成XXX.wasm和XXX.js文件。-s WASM=0: 指定輸出為asm.js格式,此選項只會生成XXX.js文件,不會生成XXX.wasm文件。-s EXPORTED_RUNTIME_METHODS=['ccall','cwrap']: 指定導出運行時方法ccall和cwrap,ccall/cwrap輔助函數默認沒有導出,在編譯時需要通過此選項顯示導出。-s MODULARIZE=1:使輸出的 JavaScript 代碼成為一個模塊化的形式,便于在不同的環境下使用。-s EXPORT_NAME='myModule':自定義導出的模塊名稱。-s ALLOW_MEMORY_GROWTH=1:允許動態擴展內存,適用于需要可變內存的應用場景。
4. --bind選項
--bind選項表示使用embind模塊。embind模塊可以將C++類和函數綁定到JavaScript環境中,后文將講解此部分內容。
5. --js-library選項
--js-library選項可以指定一個JavaScript文件作為JS庫,參與C/C++的編譯過程。后文將進一步講解此相關內容。
5.3. 導出一個函數
Hello World程序中,我們在HTML頁面中加載并調用了C++的main函數。main函數是C/C++程序的入口函數,實際項目中,底層的C/C++模塊通常希望通過接口來提供特定功能,而不是直接調用main函數作為單一入口。
emscripten中C/C++要導出一個接口,有兩個關鍵的點:
- 用
extern "C"以C的方式導出接口,避免C++的函數在編譯后會對函數名稱進行重整,在《導出接口的定義》一章中已介紹過相關內容。 - 用
EMSCRIPTEN_KEEPALIVE宏告知編譯器后續函數在優化時必須保留,并且該函數將被導出至JavaScript環境。EMSCRIPTEN_KEEPALIVE是emscripten編譯器內置的預編譯宏,在<emscripten.h>頭文件中定義了該宏。
函數定義:
extern "C" EMSCRIPTEN_KEEPALIVE int32_t add(int32_t a, int32_t b)
{
return a + b;
}
C++代碼:
為了代碼編寫方便,我們可以定義一個宏來簡化代碼,如下代碼(export_function.cpp)。
#include <cstdint>
#include <emscripten.h>
#define DECL_API(rettype) extern "C" EMSCRIPTEN_KEEPALIVE rettype
DECL_API(int32_t) add(int32_t a, int32_t b)
{
return a + b;
}
DECL_API(int32_t) sub(int32_t a, int32_t b)
{
return a - b;
}
編譯指令:
通過以下指令編譯代碼。
emcc ./export_function.cpp -o ./export_function.js
HTML代碼:
編寫html測試頁面(test.html)如下。
<html>
<head>
<meta charset="utf-8" />
<title>Emscripten</title>
</head>
<body>
<h2>Emscripten:你好,世界!</h2>
<script>
Module = {};
Module.onRuntimeInitialized = function () {
let r1 = Module._add(3, 2);
console.log("add(3, 2) = " + r1);
let r2 = Module._sub(3, 2);
console.log("sub(3, 2) = " + r2);
};
</script>
<script src="export_function.js"></script>
</body>
</html>
運行結果:
瀏覽器打開該頁面,可以看到控制臺輸出了add(3, 2) = 5和sub(3, 2) = 1:

代碼說明:
WebAssembly模塊是異步加載的,這意味著JS加載完成后emscripten的運行時環境可能并未準備好,我們要等待emscripten的運行時環境準備就緒后再調用WebAssembly模塊的代碼。而onRuntimeInitialized()就是emscripten的運行時環境準備就緒后的一個回調函數,因此可在該函數內安全的調用WebAssembly模塊相關的代碼。在無特殊說明(不產生歧義)的情況下,后續文章的測試代碼將不再列出該回調函數的完整代碼。
Module.onRuntimeInitialized = function () {
<!-- TODO -->
};
歷史文章推薦:
大家好,我是陌塵。
IT從業10年+, 北漂過也深漂過,目前暫定居于杭州,未來不知還會飄向何方。
搞了8年C++,也干過2年前端;用Python寫過書,也玩過一點PHP,未來還會折騰更多東西,不死不休。
感謝大家的關注,期待與你一起成長。


浙公網安備 33010602011771號