剛學完WebAssembly的入門課,賣弄一點入門知識。
首先我們知道wasm是目標語言,是一種新的V-ISA標準,所以編寫wasm應用,正常來説不會直接使用WAT可讀文本格式,更不會用wasm字節碼;而是使用其他高級語言編寫源代碼,經過編譯後得到wasm應用。課程中使用了C++來編寫源代碼,所以這裏我也用C++來編寫demo。
wasm的運行環境主要分為兩類,一類是Web瀏覽器,另一類就是out-of-web環境,運行於Web瀏覽器的wasm應用主要使用Emscripten來編譯得到,因為它會在編譯過程中,為所編譯代碼在Web平台的功能適配性進行一定的調整。
針對Web平台的編譯
對於功能適配性的調整,可以從下面這個例子中得到體現。
編碼
首先我們編寫一段功能簡單的C++源代碼:
#include <iostream>
extern "C" {
// 防止Name Mangling
int add(int x, int y) {
return x + y;
}
}
int main(int argc, char **argv) {
std::cout << add(10, 20) << std::endl;
return 0;
}
這段代碼裏,聲明瞭一個函數“add”,它的定義被放置在“extern "C" {}”結構中,以防止函數名被C++的Name Mangling機制更改,從而確保在宿主環境中調用該函數時,可以用與C++源碼中保持一致的函數名,來直接調用這個函數。
這段代碼中還定義了主函數main,其內部調用了add函數,並且通過std::cout 來將該函數的調用結果輸出到stdout。
編譯
現在我們可以用Emscripten這個工具集中最為重要的編譯器組件emcc,來編譯這段源代碼。命令如下所示:
emcc main.cc -s WASM=1 -O3 -o main.html
通過“-s”參數,為emcc指定了編譯時選項“WASM=1”,這樣emcc就會將輸入的源代碼編譯為wasm格式目標代碼,“-o”參數則指定了產出文件的格式為“.html”,這樣Emscripten就會生成一個可以直接在瀏覽器中使用的Web應用。
這個自動生成的應用中,包含了wasm模塊代碼、JavaScript代碼以及HTML代碼。
運行
現在我們可以嘗試在本地運行這個簡單的Web應用。首先自行準備一個簡單的Web服務器:
const http = require('http');
const url = require('url');
const fs = require('fs');
const path = require('path');
const PORT = 8888;
const mime = {
"html": "text/html;charset=UTF-8",
"wasm": "application/wasm" // 遇到".wasm"格式文件的請求時,返回特定的MIME
}
http.createServer((req, res) => {
let realPath = path.join(__dirname, `.${url.parse(req.url).pathname}`);
// 檢查所訪問文件是否存在並且可讀
fs.access(realPath, fs.constants.R_OK, err => {
if (err) {
res.writeHead(404, { 'Content-Type': 'text/plain' });
res.end();
} else {
fs.readFile(realPath, "binary", (err, file) => {
if (err) {
// 文件讀取失敗時返回500
res.writeHead(500, { 'Content-Type': 'text/plain' });
end();
} else {
// 根據請求的文件返回相應的文件內容
let ext = path.extname(realPath);
ext = ext ? ext.slice(1) : 'unknow';
let contentType = mime[ext] || 'text/plain';
res.writeHead(200, { 'Content-Type', contentType });
res.write(file, "binary");
res.end();
}
});
}
});
}).listen(PORT);
console.log("Server is running at port: " + PORT + ".");
這段代碼中最為重要的一個地方,就是對wasm格式文件請求的處理。
通過返回特殊的MIME類型“application/wasm”,我們明確告訴瀏覽器,這是一個wasm格式的文件,這樣瀏覽器就可以允許應用使用針對wasm文件的“流式編譯”方式,來加載和解析該文件。
現在我們通過8888端口來訪問剛剛編譯生成的main.html文件。
可以看到,Emscripten將C++源碼中使用std::cout將數據輸出到stdout,模擬為輸出到頁面上指定的textarea區域。這就是Emscripten針對Web平台的功能適配性調整。
再繼續看,Emscripten自動生成的完整wasm Web應用,不管是js文件還是html文件,體積都偏大,這是因為Emscripten自動生成的“膠水代碼”中,包含有通過JavaScript模擬出的POSIX運行時環境的完整代碼,而大多數情況下,我們不需要這些。
僅生成wasm模塊
那怎樣可以使得Emscripten僅生成wasm模塊,而js膠水代碼和Web API這兩部分的代碼由我們自己編寫呢?
答案就是調整編譯時的命令行參數。那麼我們要如何去編寫JS來調用wasm模塊導出的函數呢?
課程裏有個圖像處理的例子,這裏就來整個小例子。
首先編寫我們的HTML頁面:
<!-- index.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>DEMO</title>
</head>
<body>
<div>
<h1>Counter: </h1>
<span>0</span>
<button id="increaseButton">點我+1</button>
</div>
<script src="index.js"></script>
</body>
</html>
這裏想要實現一個功能,點擊按鈕後,span內的數字加1,當然這個功能JavaScript也能做,但現在作為練習,我們要通過調用wasm函數來實現。
然後就是重要的JavaScript代碼,如下:
// index.js
document.addEventListener('DOMContentLoaded', async () => {
let response = await fetch('./index.wasm');
let bytes = await response.arrayBuffer();
let {instance} = await WebAssembly.instantiate(bytes);
let {
increase
} = instance.exports;
const span = document.querySelector('span');
const button = document.querySelector('#increaseButton');
let count = 0;
button.addEventListener('click', () => {
count = increase(count);
span.innerText = count;
});
});
首先,通過fetch獲取wasm模塊,並獲取fetch方法返回的Response對象;
然後,調用response對象上的arrayBuffer()方法,將內容解析為ArrayBuffer的形式,這個ArrayBuffer將作為WebAssembly.instantiate方法的實際調用參數;這是一個用於實例化wasm模塊的方法。
接着,WebAssembly.instantiate將實例化對應的wasm模塊,我們就可以獲得模塊的實例對象,在instance變量中,可以獲得從wasm模塊導出的所有方法。
此時,我們就可以調用wasm模塊的方法了,假設instance上有個increase方法,就可以這樣調用。
現在,我們編寫對應的C++代碼並進行編譯。
// index.cc
#include <emscripten.h>
extern "C" {
EMSCRIPTEN_KEEPALIVE int increase(int x) {
return x+1;
}
}
此處我們需要引入<emscripten.h>,因為需要使用其中定義的宏EMSCRIPTEN_KEEPALIVE,因為這個文件中我們不聲明主函數main,也不在文件內部調用這個increase函數,為了防止在編譯過程中被DCE(Dead Code Elimination)處理掉,需要使用這個宏來標記函數。
現在我們來編譯這個文件。
$ emcc index.cc -s WASM=1 -O3 --no-entry -o index.wasm
僅生成wasm模塊文件的編譯方式,通常稱為”standalone模式”。
“-o”參數為我們指定了輸出的文件格式為“.wasm”,這就是告訴Emscripten以“standalone”的方式來編譯C++源碼。
“--no-entry”參數則告訴編譯器,這個wasm模塊沒有聲明“main”函數。
上述命令執行完畢後,就會得到一個名為“index.wasm”的二進制模塊文件。
此時我們就可以嘗試去運行這個Web應用,可以看到和期待的效果一致。
當然這個demo很簡單,目前要發揮wasm的優勢,更適合將其應用在計算密集的功能。
調試應用
當我們編寫完應用時,少不了要調試。那麼如何針對wasm應用進行調試呢,Emscripten也提供了一些方式。
編譯階段
首先是針對編譯階段,當使用emcc編譯項目時,可以通過為命令添加“EMCC_DEBUG”環境變量的方式,來讓emcc以“調試模式”來編譯項目。
$ EMCC_DEBUG=1 emcc index.cc \
> -s WASM=1 \
> -O3 \
> --no-entry -o index.wasm
可以看到編譯時輸出了很多的信息,這是因為我們將EMCC_DEBUG這個環境變量的值設置為1,EMCC_DEBUG的值可以設置為3個值,分別是0、1、2。
0表示關閉調試模式,這和不加這個環境變量是一樣的效果;1表示輸出編譯時的調試性信息,同時生成包含有編譯器各個階段運行信息的中間文件;可用於編譯流程的調試。
可以通過ls命令查看生成了哪些文件;調試性信息中包含了各個編譯階段所實際調用的命令行信息,通過對這些信息分析,能夠輔助開發者查找編譯失敗的原因。
當EMCC_DEBUG的值設置為2時,可以得到更多的調試性信息。
運行階段
當我們成功地編譯了wasm應用,但在實際運行時發生了錯誤,就需要在運行時進行調試。Emscripten也提供了一定的支持,我們可以在編譯時設定參數“-g“以保留與調試相關的信息。
當設置為”-gsource-map“時,emcc會生成可用於在Web瀏覽器中進行“源碼級”調試的特殊DWARF信息;通過這些特殊格式的信息,使我們可以直接在瀏覽器中對wasm模塊編譯之前的源代碼進行諸如“設置斷點”、“單步跟蹤”等調試手段。
這裏我們嘗試調試之前編寫的index.cc。
$ emcc index.cc -gsource-map -s WASM=1 -O3 --no-entry -o index.wasm
此時重新加載Web應用並打開“開發者面板”的“sources”Tab,就可以通過“操作”C++源代碼的方式,來為應用所使用的wasm模塊設置斷點。(wasm模塊的加載方式需要改為“流式編譯”)。
通過這種方式,開發者就可以方便地在wasm Web應用的運行過程中,調試發生在wasm模塊內部的“源碼級”錯誤。
WebAssembly作為一種相對較新的技術,可以先保持一點了解。