在「什麼是 WebAssembly?它的起源為何?」中,剛剛談到我們今天遇到的 WebAssembly 是怎麼辦到的,在這篇文章中,我們會示範將現有 C 程式 mkbitmap
編譯為 WebAssembly 的方法。這個例子比「hello world」範例還要複雜,因為其中包括處理檔案、在 WebAssembly 和 JavaScript 之間進行通訊,以及繪圖到畫布上。不過,雖然程式碼還是能管理,不會讓您不知所措。
本文是為想要學習 WebAssembly 的網頁程式開發人員所撰寫,並逐步說明如何將類似 mkbitmap
編譯到 WebAssembly 以繼續。不過,雖然在首次執行時,無法取得某個應用程式或程式庫進行編譯,這是很正常的現象,導致以下某些步驟最終無法運作,所以我必須反向操作,再嘗試其他方法。這篇文章並未以魔法最終編譯指令的方式,呈現從天空中掉落的樣子,而是描述我實際進展的情況,其中有一些困擾。
關於「mkbitmap
」
mkbitmap
C 程式會讀取圖片,並依照下列順序套用以下一或多項作業:反轉、高過速篩選、縮放和閾值。每項作業都可以個別控制及開啟或關閉。mkbitmap
的主要用途是將顏色或灰階圖片轉換成適合其他程式的輸入格式,尤其是構成 SVGcode 基礎的追蹤程式 potrace
。做為預先處理工具,mkbitmap
特別適合將掃描的線形圖案 (例如卡通或手寫文字) 轉換成高解析度的雙層圖片。
您可以使用 mkbitmap
來傳遞多個選項和一或多個檔案名稱。詳情請參閱這項工具的手冊:
$ mkbitmap [options] [filename...]
取得程式碼
第一步是取得 mkbitmap
的原始碼。您可以在專案的網站中找到這組 ID。在撰寫本文時,potrace-1.16.tar.gz 是最新版本。
在本機編譯及安裝
下一步是在本機編譯並安裝這項工具,以感受工具的運作方式。INSTALL
檔案包含下列操作說明:
將
cd
設為包含套件原始碼和類型的目錄./configure
:設定系統套件。執行
configure
可能需要一些時間。執行期間 一些訊息,告訴您正在檢查哪些功能。輸入
make
編譯套件。視需要輸入
make check
來執行任何隨附的自我測試 套件,通常會使用剛建構的已解除安裝二進位檔。輸入
make install
來安裝這些程式和任何資料檔案,以及 說明文件。安裝至 root 擁有的前置字串時, 建議將套件設定成 ,且只有以 Root 權限執行的make install
階段 權限。
按照上述步驟操作後,您應該會看到 potrace
和 mkbitmap
這兩個執行檔,後者是本文的重點。您可以執行 mkbitmap --version
來確認程式碼是否正常運作。以下是我的機器全部四個步驟的輸出內容。為求簡潔,經過大幅調整:
步驟 1,./configure
:
$ ./configure
checking for a BSD-compatible install... /usr/bin/install -c
checking whether build environment is sane... yes
checking for a thread-safe mkdir -p... ./install-sh -c -d
checking for gawk... no
checking for mawk... no
checking for nawk... no
checking for awk... awk
checking whether make sets $(MAKE)... yes
[…]
config.status: executing libtool commands
步驟 2,make
:
$ make
/Applications/Xcode.app/Contents/Developer/usr/bin/make all-recursive
Making all in src
clang -DHAVE_CONFIG_H -I. -I.. -g -O2 -MT main.o -MD -MP -MF .deps/main.Tpo -c -o main.o main.c
mv -f .deps/main.Tpo .deps/main.Po
[…]
make[2]: Nothing to be done for `all-am'.
步驟 3,make check
:
$ make check
Making check in src
make[1]: Nothing to be done for `check'.
Making check in doc
make[1]: Nothing to be done for `check'.
[…]
============================================================================
Testsuite summary for potrace 1.16
============================================================================
# TOTAL: 8
# PASS: 8
# SKIP: 0
# XFAIL: 0
# FAIL: 0
# XPASS: 0
# ERROR: 0
============================================================================
make[1]: Nothing to be done for `check-am'.
步驟 4,sudo make install
:
$ sudo make install
Password:
Making install in src
.././install-sh -c -d '/usr/local/bin'
/bin/sh ../libtool --mode=install /usr/bin/install -c potrace mkbitmap '/usr/local/bin'
[…]
make[2]: Nothing to be done for `install-data-am'.
如要確認是否成功,請執行 mkbitmap --version
:
$ mkbitmap --version
mkbitmap 1.16. Copyright (C) 2001-2019 Peter Selinger.
如果您取得版本詳細資料,表示已成功編譯和安裝 mkbitmap
。接著,透過 WebAssembly 執行相同步驟。
將 mkbitmap
編譯為 WebAssembly
Emscripten 這項工具可用於將 C/C++ 程式編譯到 WebAssembly。Emscripten 的「Building Projects」(建構專案) 說明文件包含下列內容:
使用 Emscripten 輕鬆建構大型專案。Emscripten 提供兩個簡單的指令碼,可讓你將 makefile 設為使用
emcc
取代gcc
,在多數情況下,專案目前的建構系統保持不變。
說明文件包括 (為求簡潔,經過略做修改):
請設想您平常使用下列指令進行建構的情況:
./configure
make
如要使用 Emscripten 進行建構,請改使用下列指令:
emconfigure ./configure
emmake make
因此,./configure
會變為 emconfigure ./configure
,而 make
會變為 emmake make
。以下示範如何使用 mkbitmap
執行這項操作。
步驟 0,make clean
:
$ make clean
Making clean in src
rm -f potrace mkbitmap
test -z "" || rm -f
rm -rf .libs _libs
[…]
rm -f *.lo
步驟 1,emconfigure ./configure
:
$ emconfigure ./configure
configure: ./configure
checking for a BSD-compatible install... /usr/bin/install -c
checking whether build environment is sane... yes
checking for a thread-safe mkdir -p... ./install-sh -c -d
checking for gawk... no
checking for mawk... no
checking for nawk... no
checking for awk... awk
[…]
config.status: executing libtool commands
步驟 2,emmake make
:
$ emmake make
make: make
/Applications/Xcode.app/Contents/Developer/usr/bin/make all-recursive
Making all in src
/opt/homebrew/Cellar/emscripten/3.1.36/libexec/emcc -DHAVE_CONFIG_H -I. -I.. -g -O2 -MT main.o -MD -MP -MF .deps/main.Tpo -c -o main.o main.c
mv -f .deps/main.Tpo .deps/main.Po
[…]
make[2]: Nothing to be done for `all'.
如果一切順利,目錄中應該就會有 .wasm
檔案。您可以執行 find . -name "*.wasm"
來找出這些錯誤:
$ find . -name "*.wasm"
./a.wasm
./src/mkbitmap.wasm
./src/potrace.wasm
最後兩個指令看起來不錯,所以 cd
進入 src/
目錄。此外,現在還有兩個新的對應檔案:mkbitmap
和 potrace
。這篇文章只與 mkbitmap
相關。其實這些物件沒有 .js
擴充功能有點令人困惑,但其實這些是 JavaScript 檔案,可透過快速的 head
呼叫驗證:
$ cd src/
$ head -n 20 mkbitmap
// include: shell.js
// The Module object: Our interface to the outside world. We import
// and export values on it. There are various ways Module can be used:
// 1. Not defined. We create it here
// 2. A function parameter, function(Module) { ..generated code.. }
// 3. pre-run appended it, var Module = {}; ..generated code..
// 4. External script tag defines var Module.
// We need to check if Module already exists (e.g. case 3 above).
// Substitution will be replaced with actual code on later stage of the build,
// this way Closure Compiler will not mangle it (e.g. case 4. above).
// Note that if you want to run closure, and also to use Module
// after the generated code, you will need to define var Module = {};
// before the code. Then that object will be used in the code, and you
// can continue to use Module afterwards as well.
var Module = typeof Module != 'undefined' ? Module : {};
// --pre-jses are emitted after the Module integration code, so that they can
// refer to Module (if they choose; they can also define Module)
呼叫 mv mkbitmap mkbitmap.js
(視需要分別呼叫 mv potrace potrace.js
),將 JavaScript 檔案重新命名為 mkbitmap.js
。
現在要進行第一次測試,看看能否執行 node mkbitmap.js --version
,在指令列中使用 Node.js 執行檔案:
$ node mkbitmap.js --version
mkbitmap 1.16. Copyright (C) 2001-2019 Peter Selinger.
您已成功將 mkbitmap
編譯為 WebAssembly。現在,下一步是在瀏覽器中運作。
在瀏覽器中使用 WebAssembly 可享 mkbitmap
將 mkbitmap.js
和 mkbitmap.wasm
檔案複製到名為 mkbitmap
的新目錄,然後建立可載入 mkbitmap.js
JavaScript 檔案的 index.html
HTML 樣板檔案。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>mkbitmap</title>
</head>
<body>
<script src="mkbitmap.js"></script>
</body>
</html>
啟動提供 mkbitmap
目錄的本機伺服器,並在瀏覽器中開啟。系統會顯示提示,要求您輸入資料。這符合預期,因為根據工具的手冊頁面,「[i]沒有指定檔案名稱引數,mkbitmap 會做為篩選器,從標準輸入內容讀取資料」,也就是 Emscripten 預設為 prompt()
。
防止自動執行
如要停止 mkbitmap
立即執行,改為等待使用者輸入內容,您必須瞭解 Emscripten 的 Module
物件。Module
是全域 JavaScript 物件,具有屬性,讓 Emscripten 在執行程式碼的不同點呼叫程式碼。
您可以提供 Module
實作來控製程式碼的執行。
Emscripten 應用程式啟動後,會查看 Module
物件上的值並套用這些值。
若是 mkbitmap
,請將 Module.noInitialRun
設為 true
,以避免在初始執行作業中觸發提示。接著建立名為 script.js
的指令碼,並加到 index.html
的 <script src="mkbitmap.js"></script>
「之前」,然後將下列程式碼新增至 script.js
。現在重新載入應用程式後,提示應該不會再出現。
var Module = {
// Don't run main() at page load
noInitialRun: true,
};
使用更多建構旗標建立模組化建構作業
如要在應用程式中提供輸入內容,請在 Module.FS
中使用 Emscripten 的檔案系統支援。說明文件中的「加入檔案系統支援」一節:
Emscripten 會決定是否自動支援檔案系統。許多程式不需要檔案,而且檔案系統的大小有限,因此 Emscripten 會在沒有理由的情況下避免加入它。換句話說,如果您的 C/C++ 程式碼不會存取檔案,輸出內容中就不會包含
FS
物件和其他檔案系統 API。另一方面,如果您的 C/C++ 程式碼使用了檔案,那麼系統會自動納入檔案系統支援。
很抱歉,mkbitmap
是 Emscripten 不會自動納入檔案系統支援的情況之一,因此您必須明確指定系統支援。也就是說,您必須按照上述的 emconfigure
和 emmake
步驟操作,並透過 CFLAGS
引數設定其他幾個標記。下列標記或許也適用於其他專案。
- 設定
-sFILESYSTEM=1
以納入檔案系統支援。 - 設定
-sEXPORTED_RUNTIME_METHODS=FS,callMain
,讓系統匯出Module.FS
和Module.callMain
。 - 設定
-sMODULARIZE=1
和-sEXPORT_ES6
以產生新型 ES6 模組。 - 設定
-sINVOKE_RUN=0
,防止出現提示的初始執行作業。
此外,在本特定範例中,您必須將 --host
標記設為 wasm32
,告知 configure
指令碼您要編譯 WebAssembly。
最終的 emconfigure
指令如下所示:
$ emconfigure ./configure --host=wasm32 CFLAGS='-sFILESYSTEM=1 -sEXPORTED_RUNTIME_METHODS=FS,callMain -sMODULARIZE=1 -sEXPORT_ES6 -sINVOKE_RUN=0'
別忘了再次執行 emmake make
,並將剛建立的檔案複製到 mkbitmap
資料夾。
修改 index.html
,使其只載入 ES 模組 script.js
,再從該模組匯入 mkbitmap.js
模組。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>mkbitmap</title>
</head>
<body>
<!-- No longer load `mkbitmap.js` here -->
<script src="script.js" type="module"></script>
</body>
</html>
// This is `script.js`.
import loadWASM from './mkbitmap.js';
const run = async () => {
const Module = await loadWASM();
console.log(Module);
};
run();
在瀏覽器中開啟應用程式時,您應會看到 Module
物件已記錄到開發人員工具控制台,且提示會消失,因為系統不再呼叫 mkbitmap
的 main()
函式。
手動執行主要函式
下一步是執行 Module.callMain()
,手動呼叫 mkbitmap
的 main()
函式。callMain()
函式採用引數陣列,與您需在指令列傳遞的每一個陣列相符。如要在指令列執行 mkbitmap -v
,您必須在瀏覽器中呼叫 Module.callMain(['-v'])
。這會將 mkbitmap
版本號碼記錄到開發人員工具控制台。
// This is `script.js`.
import loadWASM from './mkbitmap.js';
const run = async () => {
const Module = await loadWASM();
Module.callMain(['-v']);
};
run();
重新導向標準輸出內容
根據預設,標準輸出內容 (stdout
) 為控制台。不過,您可以重新導向至其他內容,例如可將輸出內容儲存至變數的函式。這表示您可以設定 Module.print
屬性,將輸出內容新增至 HTML。
// This is `script.js`.
import loadWASM from './mkbitmap.js';
const run = async () => {
let consoleOutput = 'Powered by ';
const Module = await loadWASM({
print: (text) => (consoleOutput += text),
});
Module.callMain(['-v']);
document.body.textContent = consoleOutput;
};
run();
將輸入檔案取得至記憶體檔案系統
如要將輸入檔案傳入記憶體檔案系統,您需要在指令列中等同於 mkbitmap filename
的同等項目。為瞭解具體做法,請先瞭解 mkbitmap
預期其輸入內容和產生輸出內容的方式。
支援的 mkbitmap
輸入格式為 PNM (PBM、PGM、PPM) 和 BMP。輸出格式是點陣圖的 PBM,而灰色地圖的 PGM。如果提供 filename
引數,mkbitmap
預設會建立輸出檔案,並將其後置字串變更為 .pbm
,藉此從輸入檔案名稱取得其名稱。例如,若輸入檔案名稱 example.bmp
,輸出檔案名稱會是 example.pbm
。
Emscripten 提供模擬本機檔案系統的虛擬檔案系統,讓使用同步檔案 API 的原生程式碼只需少許變更或完全沒有變更即可編譯和執行。
為了讓 mkbitmap
讀取輸入檔案就像以 filename
指令列引數的形式傳遞,您必須使用 Emscripten 提供的 FS
物件。
FS
物件由記憶體內檔案系統 (通常稱為 MEMFS) 提供支援,並提供 writeFile()
函式,可用於將檔案寫入虛擬檔案系統。請如下列程式碼範例使用 writeFile()
。
如要驗證檔案寫入作業是否正常運作,請使用參數 '/'
執行 FS
物件的 readdir()
函式。您會看到 example.bmp
和一些系統一律自動建立的預設檔案。
請注意,先前對 Module.callMain(['-v'])
輸出版本號碼的呼叫已移除。這是因為 Module.callMain()
是一個一般預期只會執行一次的函式。
// This is `script.js`.
import loadWASM from './mkbitmap.js';
const run = async () => {
const Module = await loadWASM();
const buffer = await fetch('https://example.com/example.bmp').then((res) => res.arrayBuffer());
Module.FS.writeFile('example.bmp', new Uint8Array(buffer));
console.log(Module.FS.readdir('/'));
};
run();
首次實際執行
完成所有設定後,請執行 Module.callMain(['example.bmp'])
來執行 mkbitmap
。記錄「MEMFS」的內容'/'
資料夾,接著您應該會在 example.bmp
輸入檔案旁邊看到新建立的 example.pbm
輸出檔案。
// This is `script.js`.
import loadWASM from './mkbitmap.js';
const run = async () => {
const Module = await loadWASM();
const buffer = await fetch('https://example.com/example.bmp').then((res) => res.arrayBuffer());
Module.FS.writeFile('example.bmp', new Uint8Array(buffer));
Module.callMain(['example.bmp']);
console.log(Module.FS.readdir('/'));
};
run();
從記憶體檔案系統中取得輸出檔案
FS
物件的 readFile()
函式可讓您從記憶體檔案系統中,取得上一個步驟中建立的 example.pbm
。由於瀏覽器通常不支援透過瀏覽器直接檢視的 PBM 檔案,因此這個函式會傳回您轉換為 File
物件的 Uint8Array
。
(儲存檔案有更優雅的方式,但使用動態建立的 <a download>
是最廣泛支援的做法)。檔案儲存完成後,即可使用慣用的圖片檢視器開啟檔案。
// This is `script.js`.
import loadWASM from './mkbitmap.js';
const run = async () => {
const Module = await loadWASM();
const buffer = await fetch('https://example.com/example.bmp').then((res) => res.arrayBuffer());
Module.FS.writeFile('example.bmp', new Uint8Array(buffer));
Module.callMain(['example.bmp']);
const output = Module.FS.readFile('example.pbm', { encoding: 'binary' });
const file = new File([output], 'example.pbm', {
type: 'image/x-portable-bitmap',
});
const a = document.createElement('a');
a.href = URL.createObjectURL(file);
a.download = file.name;
a.click();
};
run();
新增互動式 UI
此時,輸入檔案將採用硬式編碼,mkbitmap
則會使用預設參數執行。最後一個步驟是讓使用者動態選取輸入檔案,調整 mkbitmap
參數,然後使用所選選項執行工具。
// Corresponds to `mkbitmap -o output.pbm input.bmp -s 8 -3 -f 4 -t 0.45`.
Module.callMain(['-o', 'output.pbm', 'input.bmp', '-s', '8', '-3', '-f', '4', '-t', '0.45']);
PBM 圖片格式並不是特別難以剖析,因此若使用部分 JavaScript 程式碼,您甚至可以顯示輸出圖片的預覽畫面。查看下方嵌入式示範的原始碼,瞭解執行方法。
結論
恭喜,您已成功將 mkbitmap
編譯為 WebAssembly,並在瀏覽器中運作了!過去有些人會死去,必須多次編譯工具才能正常運作,但如同我先前寫的內容,這是體驗的一部分。如果遇到問題,請一併記住 StackOverflow 的 webassembly
標記。祝您編譯愉快!
特別銘謝
本文評論者為 Sam Cleg 和 Rachel Andrew。