為什么說(shuō) WebAssembly 是 Web 的未來(lái)?
大廠技術(shù)??堅(jiān)持周更??精選好文
這篇文章打算講什么?
了解 WebAssembly 的前世今生,這一致力于讓 Web 更廣泛使用的偉大創(chuàng)造是如何在整個(gè) Web/Node.js 的生命周期起作用的。
在整篇文章的講解過(guò)程中,你可以了解到 WebAssembly 原生、AssemblyScript、Emscripten 編譯器、以及如何在瀏覽器調(diào)試 WebAssembly 程序的。
最后還對(duì) WebAssembly 的未來(lái)進(jìn)行了展望,列舉了一些令人興奮的技術(shù)的發(fā)展方向。
本文旨在對(duì)那些有興趣了解 WebAssembly,但是一直沒(méi)有時(shí)間深入探究它的邊界的同學(xué)提供一個(gè)快速入門且具有一定深度的分享,希望本文能為你在學(xué)習(xí) WebAssembly 的路上一個(gè)比較有意思的指引。
同時(shí)本文還試圖回答之前分享文章的一些問(wèn)題:WebAssembly 入門:如何和有 C 項(xiàng)目結(jié)合使用[1]
- 如何將復(fù)雜的 CMake 項(xiàng)目編譯到 WebAssembly?
- 在編譯復(fù)雜的 CMake 項(xiàng)目到 WebAssembly 時(shí)如何探索一套通用的最佳實(shí)踐?
- 如何和 CMake 項(xiàng)目結(jié)合起來(lái)進(jìn)行 Debug?
動(dòng)態(tài)語(yǔ)言之踵
首先先來(lái)看一下 JS 代碼的執(zhí)行過(guò)程:

上述是 Microsoft Edge 之前的 ChakraCore 引擎結(jié)構(gòu),目前 Microsoft Edge 的 JS 引擎已經(jīng)切換為 V8 。
整體的流程就是:
- 拿到了 JS 源代碼,交給 Parser,生成 AST
- ByteCode Compiler 將 AST 編譯為字節(jié)碼(ByteCode)
- ByteCode 進(jìn)入翻譯器,翻譯器將字節(jié)碼一行一行翻譯(Interpreter)為機(jī)器碼(Machine Code),然后執(zhí)行
但其實(shí)我們平時(shí)寫的代碼有很多可以優(yōu)化的地方,如多次執(zhí)行同一個(gè)函數(shù),那么可以將這個(gè)函數(shù)生成的 Machine Code 標(biāo)記可優(yōu)化,然后打包送到 JIT Compiler(Just-In-Time),下次再執(zhí)行這個(gè)函數(shù)的時(shí)候,就不需要經(jīng)過(guò) Parser-Compiler-Interpreter 這個(gè)過(guò)程,可以直接執(zhí)行這份準(zhǔn)備好的 Machine Code,大大提高的代碼的執(zhí)行效率。
但是上述的 JIT 優(yōu)化只能針對(duì)靜態(tài)類型的變量,如我們要優(yōu)化的函數(shù),它只有兩個(gè)參數(shù),每個(gè)參數(shù)的類型是確定的,而 JavaScript 卻是一門動(dòng)態(tài)類型的語(yǔ)言,這也意味著,函數(shù)在執(zhí)行過(guò)程中,可能類型會(huì)動(dòng)態(tài)變化,參數(shù)可能變成三個(gè),第一個(gè)參數(shù)的類型可能從對(duì)象變?yōu)閿?shù)組,這就會(huì)導(dǎo)致 JIT 失效,需要重新進(jìn)行 Parser-Compiler-Interpreter-Execuation,而 Parser-Compiler 這兩步是整個(gè)代碼執(zhí)行過(guò)程中最耗費(fèi)時(shí)間的兩步,這也是為什么 JavaScript 語(yǔ)言背景下,Web 無(wú)法執(zhí)行一些高性能應(yīng)用,如大型游戲、視頻剪輯等。
靜態(tài)語(yǔ)言優(yōu)化
通過(guò)上面的說(shuō)明了解到,其實(shí) JS 執(zhí)行慢的一個(gè)主要原因是因?yàn)槠鋭?dòng)態(tài)語(yǔ)言的特性,導(dǎo)致 JIT 失效,所以如果我們能夠?yàn)?JS 引入靜態(tài)特性,那么可以保持有效的 JIT,勢(shì)必會(huì)加快 JS 的執(zhí)行速度,這個(gè)時(shí)候 asm.js 出現(xiàn)了。
asm.js 只提供兩種數(shù)據(jù)類型:
- 32 位帶符號(hào)整數(shù)
- 64 位帶符號(hào)浮點(diǎn)數(shù)
其他類似如字符串、布爾值或?qū)ο蠖际且詳?shù)值的形式保存在內(nèi)存中,通過(guò) TypedArray 調(diào)用。整數(shù)和浮點(diǎn)數(shù)表示如下:
ArrayBuffer對(duì)象、TypedArray視圖和DataView視圖是 JavaScript 操作二進(jìn)制數(shù)據(jù)的一個(gè)接口,以數(shù)組的語(yǔ)法處理二進(jìn)制數(shù)據(jù),統(tǒng)稱為二進(jìn)制數(shù)組。參考 ArrayBuffer[2] 。
var?a?=?1;
var?x?=?a?|?0;??//?x?是32位整數(shù)
var?y?=?+a;??//?y?是64位浮點(diǎn)數(shù)
而函數(shù)的寫法如下:
function?add(x,?y)?{
??x?=?x?|?0;
??y?=?y?|?0;
??return?(x?+?y)?|?0;
}
上述的函數(shù)參數(shù)及返回值都需要聲明類型,這里都是 32 位整數(shù)。
而且 asm.js 也不提供垃圾回收機(jī)制,內(nèi)存操作都是由開發(fā)者自己控制,通過(guò) TypedArray 直接讀寫內(nèi)存:
var?buffer?=?new?ArrayBuffer(32768);?//?申請(qǐng)?32?MB?內(nèi)存
var?HEAP8?=?new?Int8Array(buffer);?//?每次讀?1?個(gè)字節(jié)的視圖?HEAP8
function?compiledCode(ptr)?{
??HEAP[ptr]?=?12;
??return?HEAP[ptr?+?4];
}??
從上可見,asm.js 是一個(gè)嚴(yán)格的 JavaScript 子集要求變量的類型在運(yùn)行時(shí)確定且不可改變,且去除了 JavaScript 擁有的垃圾回收機(jī)制,需要開發(fā)者手動(dòng)管理內(nèi)存。這樣 JS 引擎就可以基于 asm.js 的代碼進(jìn)行大量的 JIT 優(yōu)化,據(jù)統(tǒng)計(jì) asm.js 在瀏覽器里面的運(yùn)行速度,大約是原生代碼(機(jī)器碼)的 50% 左右。
推陳出新
但是不管 asm.js 再怎么靜態(tài)化,干掉一些需要耗時(shí)的上層抽象(垃圾收集等),也還是屬于 JavaScript 的范疇,代碼執(zhí)行也需要 Parser-Compiler 這兩個(gè)過(guò)程,而這兩個(gè)過(guò)程也是代碼執(zhí)行中最耗時(shí)的。
為了極致的性能,Web 的前沿開發(fā)者們拋棄 JavaScript,創(chuàng)造了一門可以直接和 Machine Code 打交道的匯編語(yǔ)言 WebAssembly,直接干掉 Parser-Compiler,同時(shí) WebAssembly 是一門強(qiáng)類型的靜態(tài)語(yǔ)言,能夠進(jìn)行最大限度的 JIT 優(yōu)化,使得 WebAssembly 的速度能夠無(wú)限逼近 C/C++ 等原生代碼。
相當(dāng)于下面的過(guò)程:
WebAssembly 初探我們可以通過(guò)一張圖來(lái)直觀了解 WebAssembly 在 Web 中的位置:

WebAssembly(也稱為 WASM),是一種可在 Web 中運(yùn)行的全新語(yǔ)言格式,同時(shí)兼具體積小、性能高、可移植性強(qiáng)等特點(diǎn),在底層上類似 Web 中的 JavaScript,同時(shí)也是 W3C 承認(rèn)的 Web 中的第 4 門語(yǔ)言。
為什么說(shuō)在底層上類似 JavaScript,主要有以下幾個(gè)理由:
- 和 JavaScript 在同一個(gè)層次執(zhí)行:JS Engine,如 Chrome 的 V8
- 和 JavaScript 一樣可以操作各種 Web API
同時(shí) WASM 也可以運(yùn)行在 Node.js 或其他 WASM Runtime 中。
WebAssembly 文本格式
實(shí)際上 WASM 是一堆可以直接執(zhí)行二進(jìn)制格式,但是為了易于在文本編輯器或開發(fā)者工具里面展示,WASM 也設(shè)計(jì)了一種 “中間態(tài)” 的文本格式[3],以 .wat 或 .wast 為擴(kuò)展命名,然后通過(guò) wabt[4] 等工具,將文本格式下的 WASM 轉(zhuǎn)為二進(jìn)制格式的可執(zhí)行代碼,以 .wasm 為擴(kuò)展的格式。
來(lái)看一段 WASM 文本格式下的模塊代碼:
(module
??(func?$i?(import?"imports"?"imported_func")?(param?i32))
??(func?(export?"exported_func")
????i32.const?42
????call?$i
??)
)
上述代碼邏輯如下:
- 首先定義了一個(gè) WASM 模塊,然后從一個(gè)
importsJS 模塊導(dǎo)入了一個(gè)函數(shù)imported_func,將其命名為$i,接收參數(shù)i32 - 然后導(dǎo)出一個(gè)名為
exported_func的函數(shù),可以從 Web App,如 JS 中導(dǎo)入這個(gè)函數(shù)使用 - 接著為參數(shù)
i32傳入 42,然后調(diào)用函數(shù)$i
我們通過(guò) wabt 將上述文本格式轉(zhuǎn)為二進(jìn)制代碼:
- 將上述代碼復(fù)制到一個(gè)新建的,名為
simple.wat的文件中保存 - 使用 wabt[5] 進(jìn)行編譯轉(zhuǎn)換
當(dāng)你安裝好 wabt 之后,運(yùn)行如下命令進(jìn)行編譯:
wat2wasm?simple.wat?-o?simple.wasm
雖然轉(zhuǎn)換成了二進(jìn)制,但是無(wú)法在文本編輯器中查看其內(nèi)容,為了查看二進(jìn)制的內(nèi)容,我們可以在編譯時(shí)加上 -v 選項(xiàng),讓內(nèi)容在命令行輸出:
wat2wasm?simple.wat?-v
輸出結(jié)果如下:

可以看到,WebAssembly 其實(shí)是二進(jìn)制格式的代碼,即使其提供了稍為易讀的文本格式,也很難真正用于實(shí)際的編碼,更別提開發(fā)效率了。
將 WebAssembly 作為編程語(yǔ)言的一種嘗試
因?yàn)樯鲜龅亩M(jìn)制和文本格式都不適合編碼,所以不適合將 WASM 作為一門可正常開發(fā)的語(yǔ)言。
為了突破這個(gè)限制,AssemblyScript[6] 走到臺(tái)前,AssemblyScript 是 TypeScript 的一種變體,為 JavaScript 添加了 WebAssembly 類型[7] , 可以使用 Binaryen[8] 將其編譯成 WebAssembly。
WebAssembly 類型大致如下:
i32、u32、i64、v128 等
小整數(shù)類型:i8、u8 等
變量整數(shù)類型:isize、usize 等
Binaryen 會(huì)前置將 AssemblyScript 靜態(tài)編譯成強(qiáng)類型的 WebAssembly 二進(jìn)制,然后才會(huì)交給 JS 引擎去執(zhí)行,所以說(shuō)雖然 AssemblyScript 帶來(lái)了一層抽象,但是實(shí)際用于生產(chǎn)的代碼依然是 WebAssembly,保有 WebAssembly 的性能優(yōu)勢(shì)。AssemblyScript 被設(shè)計(jì)的和 TypeScript 非常相似,提供了一組內(nèi)建的函數(shù)可以直接操作 WebAssembly 以及編譯器的特性.
內(nèi)建函數(shù):
靜態(tài)類型檢查:
function isInteger<T>(value?: T): bool等
實(shí)用函數(shù):
function sizeof<T>(): usize等
操作 WebAssembly:
function select<T>(ifTrue: T, ifFalse: T, condition: bool): T等function load<T>(ptr: usize, immOffset?: usize): T等function clz<T>(value: T): T等數(shù)學(xué)操作
內(nèi)存操作
控制流
SIMD
Atomics
Inline instructions
然后基于這套內(nèi)建的函數(shù)向上構(gòu)建一套標(biāo)準(zhǔn)庫(kù)。
標(biāo)準(zhǔn)庫(kù):
Globals
Array
ArrayBuffer
DataView
Date
Error
Map
Math
Number
Set
String
Symbol
TypedArray
如一個(gè)典型的 Array 的使用如下:
var?arr?=?new?Array<string>(10)
//?arr[0];?//?會(huì)出錯(cuò)???
//?進(jìn)行初始化
for?(let?i?=?0;?i?<?arr.length;?++i)?{
??arr[i]?=?""
}
arr[0];?//?可以正確工作???
可以看到 AssemblyScript 在為 JavaScript 添加類似 TypeScript 那樣的語(yǔ)法,然后在使用上需要保持和 C/C++ 等靜態(tài)強(qiáng)類型的要求,如不初始化,進(jìn)行內(nèi)存分配就訪問(wèn)就會(huì)報(bào)錯(cuò)。
還有一些擴(kuò)展庫(kù),如 Node.js 的 process、crypto 等,JS 的 console,還有一些和內(nèi)存相關(guān)的 StaticArray、heap 等。
可以看到通過(guò)上面基礎(chǔ)的類型、內(nèi)建庫(kù)、標(biāo)準(zhǔn)庫(kù)和擴(kuò)展庫(kù),AssemblyScript 基本上構(gòu)造了 JavaScript 所擁有的的全部特性,同時(shí) AssemblyScript 提供了類似 TypeScript 的語(yǔ)法,在寫法上嚴(yán)格遵循強(qiáng)類型靜態(tài)語(yǔ)言的規(guī)范。
值得一提的是,因?yàn)楫?dāng)前 WebAssembly 的 ES 模塊規(guī)范依然在草案中,AssemblyScript 自行進(jìn)行了模塊的實(shí)現(xiàn),例如導(dǎo)出一個(gè)模塊:
//?env.ts
export?declare?function?doSomething(foo:?i32):?void?{?/*?...?函數(shù)體?*/?}
導(dǎo)入一個(gè)模塊:
import?{?doSomething?}?from?"./env";
一個(gè)大段代碼、使用類的例子:
class?Animal<T>?{
??static?ONE:?i32?=?1;
??static?add(a:?i32,?b:?i32):?i32?{?return?a?+?b?+?Animal.ONE;?}
??two:?i16?=?2;?//?6
??instanceSub<T>(a:?T,?b:?T):?T?{?return?a?-?b?+?<T>Animal.ONE;?}?//?tsc?does?not?allow?this
}
export?function?staticOne():?i32?{
??return?Animal.ONE;
}
export?function?staticAdd(a:?i32,?b:?i32):?i32?{
??return?Animal.add(a,?b);
}
export?function?instanceTwo():?i32?{
??let?animal?=?new?Animal<i32>();
??return?animal.two;
}
export?function?instanceSub(a:?f32,?b:?f32):?f32?{
??let?animal?=?new?Animal<f32>();
??return?animal.instanceSub<f32>(a,?b);
}
AssemblyScript 為我們打開了一扇新的大門,可以以 TS 形式的語(yǔ)法,遵循靜態(tài)強(qiáng)類型的規(guī)范進(jìn)行高效編碼,同時(shí)又能夠便捷的操作 WebAssembly/編譯器相關(guān)的 API,代碼寫完之后,通過(guò) Binaryen 編譯器將其編譯為 WASM 二進(jìn)制,然后獲取到 WASM 的執(zhí)行性能。
得益于 AssemblyScript 兼具靈活性與性能,目前使用 AssemblyScript 構(gòu)建的應(yīng)用生態(tài)已經(jīng)初具繁榮,目前在區(qū)塊鏈、構(gòu)建工具、編輯器、模擬器、游戲、圖形編輯工具、庫(kù)、IoT、測(cè)試工具等方面都有大量使用 AssemblyScript 構(gòu)建的產(chǎn)物:https://www.assemblyscript.org/built-with-assemblyscript.html#games
上面是使用 AssemblyScript 構(gòu)建的一個(gè)五子棋游戲。
一種鬼才哲學(xué):將 C/C++ 代碼跑在瀏覽器
雖然 AssemblyScript 的出現(xiàn)極大的改善了 WebAssembly 在高效率編碼方面的缺陷,但是作為一門新的編程語(yǔ)言,其最大的劣勢(shì)就是生態(tài)、開發(fā)者與積累。
WebAssembly 的設(shè)計(jì)者顯然在設(shè)計(jì)上同時(shí)考慮到了各種完善的情況,既然 WebAssembly 是一種二進(jìn)制格式,那么其就可以作為其他語(yǔ)言的編譯目標(biāo),如果能夠構(gòu)建一種編譯器,能夠?qū)⒁延械摹⒊墒斓摹⑶壹婢吆A康拈_發(fā)者和強(qiáng)大的生態(tài)的語(yǔ)言編譯到 WebAssembly 使用,那么相當(dāng)于可以直接復(fù)用這個(gè)語(yǔ)言多年的積累,并用它們來(lái)完善 WebAssembly 生態(tài),將它們運(yùn)行在 Web、Node.js 中。
幸運(yùn)的是,針對(duì) C/C++ 已經(jīng)有 Emscripten[9] 這樣優(yōu)秀的編譯器存在了。

可以通過(guò)下面這張圖直觀的闡述 Emscripten 在開發(fā)鏈路中的地位:

即將 C/C++ 的代碼(或者 Rust/Go 等)編譯成 WASM,然后通過(guò) JS 膠水代碼將 WASM 跑在瀏覽器中(或 Node.js)的 runtime,如 ffmpeg 這個(gè)使用 C 編寫音視頻轉(zhuǎn)碼工具,通過(guò) Emscripten 編譯器編譯到 Web 中使用,可直接在瀏覽器前端轉(zhuǎn)碼音視頻。
上述的 JS “Gule” 代碼是必須的,因?yàn)槿绻枰獙?C/C++ 編譯到 WASM,還能在瀏覽器中執(zhí)行,就得實(shí)現(xiàn)映射到 C/C++ 相關(guān)操作的 Web API,這樣才能保證執(zhí)行有效,這些膠水代碼目前包含一些比較流行的 C/C++ 庫(kù),如 SDL[10]、OpenGL[11]、OpenAL[12]、以及 POSIX[13] 的一部分 API。
目前使用 WebAssembly 最大的場(chǎng)景也是這種將 C/C++ 模塊編譯到 WASM 的方式,比較有名的例子有 Unreal Engine 4[14]、Unity[15] 之類的大型庫(kù)或應(yīng)用。
WebAssembly 會(huì)取代 JavaScript 嗎?
答案是不會(huì)。
根據(jù)上面的層層闡述,實(shí)際上 WASM 的設(shè)計(jì)初衷就可以梳理為以下幾點(diǎn):
- 最大程度的復(fù)用現(xiàn)有的底層語(yǔ)言生態(tài),如 C/C++ 在游戲開發(fā)、編譯器設(shè)計(jì)等方面的積淀
- 在 Web、Node.js 或其他 WASM runtime 獲得近乎于原生的性能,也就是可以讓瀏覽器也能跑大型游戲、圖像剪輯等應(yīng)用
- 還有最大程度的兼容 Web、保證安全
- 同時(shí)在開發(fā)上(如果需要開發(fā))易于讀寫和可調(diào)試,這一點(diǎn) AssemblyScript 走得更遠(yuǎn)
所以從初衷出發(fā),WebAssembly 的作用更適合下面這張圖:

WASM 橋接各種系統(tǒng)編程語(yǔ)言的生態(tài),進(jìn)一步補(bǔ)齊了 Web 開發(fā)生態(tài)之外,還為 JS 提供性能的補(bǔ)充,正是 Web 發(fā)展至今所缺失的重要的一塊版圖。
深入探索 EmscriptenRust Web Framework:https://github.com/yewstack/yew
地址:https://github.com/emscripten-core/emscripten
下面所有的 demo 都可以在倉(cāng)庫(kù):https://code.byted.org/huangwei.fps/webassembly-demos/tree/master 找到
Star:21.4K
維護(hù):活躍
Emscripten 是一個(gè)開源的,跨平臺(tái)的,用于將 C/C++ 編譯為 WebAssembly 的編譯器工具鏈,由 LLVM、Binaryen、Closure Compiler 和其他工具等組成。
Emscripten 的核心工具為 Emscripten Compiler Frontend(emcc),emcc 是用于替代一些原生的編譯器如 gcc 或 clang,對(duì) C/C++ 代碼進(jìn)行編譯。
實(shí)際上為了能讓幾乎所有的可移植的 C/C++ 代碼庫(kù)能夠編譯為 WebAssembly,并在 Web 或 Node.js 執(zhí)行,Emscripten Runtime 其實(shí)還提供了兼容 C/C++ 標(biāo)準(zhǔn)庫(kù)、相關(guān) API 到 Web/Node.js API 的映射,這份映射存在于編譯之后的 JS 膠水代碼中。
再看下面這張圖,紅色部分為 Emscripten 編譯后的產(chǎn)物,綠色部分為 Emscripten 為保證 C/C++ 代碼能夠運(yùn)行的一些 runtime 支持:

簡(jiǎn)單體驗(yàn)一下 “Hello World”
值得一提的是,WebAssembly 相關(guān)工具鏈的安裝幾乎都是以源碼的形式提供,這可能和 C/C++ 生態(tài)的習(xí)慣不無(wú)關(guān)系。
為了完成簡(jiǎn)單的 C/C++ 程序運(yùn)行在 Web,我們首先需要安裝 Emscripten 的 SDK:
#?Clone?代碼倉(cāng)庫(kù)
git?clone?https:?//?github?.?com?/?emscripten-core?/?emsdk?.?git
#?進(jìn)入倉(cāng)庫(kù)
cd?emsdk
#?獲取最新代碼,如果是新?clone?的這一步可以不需要
git?pull
#?安裝?SDK?工具,我們安裝?1.39.18,方便測(cè)試
./emsdk?install?1.39.18
#?激活?SDK
./emsdk?activate?1.39.18
#?將相應(yīng)的環(huán)境變量加入到系統(tǒng)?PATH
source?./emsdk_env.sh
#?運(yùn)行命令測(cè)試是否安裝成功
emcc?-v?#?
如果安裝成功,上述的命令運(yùn)行之后會(huì)輸出如下結(jié)果:
emcc?(Emscripten?gcc/clang-like?replacement?+?linker?emulating?GNU?ld)?1.39.18
clang?version?11.0.0?(/b/s/w/ir/cache/git/chromium.googlesource.com-external-github.com-llvm-llvm--project?613c4a87ba9bb39d1927402f4dd4c1ef1f9a02f7)
Target:?x86_64-apple-darwin21.1.0
Thread?model:?posix
讓我們準(zhǔn)備初始代碼:
mkdir?-r?webassembly/hello_world
cd?webassembly/hello_world?&&?touch?main.c
在 main.c 中加入如下代碼:
?#include?<stdio.h>
int?main()?{
??printf("hello,?world!\n");
??return?0;
}
然后使用 emcc 來(lái)編譯這段 C 代碼,在命令行切換到 webassembly/hello_world 目錄,運(yùn)行:
emcc?main.c
上述命令會(huì)輸出兩個(gè)文件:a.out.js 和 a.out.wasm ,后者為編譯之后的 wasm 代碼,前者為 JS 膠水代碼,提供了 WASM 運(yùn)行的 runtime。
可以使用 Node.js 進(jìn)行快速測(cè)試:
node?a.out.js
會(huì)輸出 "hello, world!" ,我們成功將 C/C++ 代碼運(yùn)行在了 Node.js 環(huán)境。

接下來(lái)我們嘗試一下將代碼運(yùn)行在 Web 環(huán)境,修改編譯代碼如下:
emcc?main.c?-o?main.html
上述命令會(huì)生成三個(gè)文件:
main.js膠水代碼main.wasmWASM 代碼main.html加載膠水代碼,執(zhí)行 WASM 的一些邏輯
Emscripten 生成代碼有一定的規(guī)則,具體可以參考:https://emscripten.org/docs/compiling/Building-Projects.html#emscripten-linker-output-files
如果要在瀏覽器打開這個(gè) HTML,需要在本地起一個(gè)服務(wù)器,因?yàn)閱渭兊拇蜷_通過(guò) file:// 協(xié)議訪問(wèn)時(shí),主流瀏覽器不支持 XHR 請(qǐng)求,只有在 HTTP 服務(wù)器下,才能進(jìn)行 XHR 請(qǐng)求,所以我們運(yùn)行如下命令來(lái)打開網(wǎng)站:
npx?serve?.
打開網(wǎng)頁(yè),訪問(wèn) localhost:3000/main.html,可以看到如下結(jié)果:

同時(shí)開發(fā)者工具里面也會(huì)有相應(yīng)的打印輸出:

嘗試在 JS 中調(diào)用 C/C++ 函數(shù)
上一小節(jié)我們初步體驗(yàn)了一下如何在 Web 和 Node.js 中運(yùn)行 C 程序,但其實(shí)如果我們想要讓復(fù)雜的 C/C++ 應(yīng)用,如 Unity 運(yùn)行在 Web,那我們還有很長(zhǎng)的路要走,其中一條,就是能夠在 JS 中操作 C/C++ 函數(shù)。
讓我們?cè)谀夸浵滦陆?function.c 文件,添加如下代碼:
?#include?<stdio.h>
?#include?<emscripten/emscripten.h>
int?main()?{
????printf("Hello?World\n");
}
EMSCRIPTEN_KEEPALIVE?void?myFunction(int?argc,?char?**?argv)?{
????printf("MyFunction?Called\n");
}
值得注意的是 Emscripten 默認(rèn)編譯的代碼只會(huì)調(diào)用 main 函數(shù),其他的代碼會(huì)作為 “死代碼” 在編譯時(shí)被刪掉,所以為了使用我們?cè)谏厦娑x的 myFunction ,我們需要在其定義之前加上 EMSCRIPTEN_KEEPALIVE 聲明,確保在編譯時(shí)不會(huì)刪掉 myFunction 函數(shù)相關(guān)的代碼。
我們需要導(dǎo)入
emscripten/emscripten.h頭文件,才能使用EMSCRIPTEN_KEEPALIVE聲明。
同時(shí)我們還需要對(duì)編譯命令做一下改進(jìn)如下:
emcc?function.c?-o?function.html?-s?NO_EXIT_RUNTIME=1?-s?"EXTRA_EXPORTED_RUNTIME_METHODS=['ccall']"
上述額外增加了兩個(gè)參數(shù):
-s NO_EXIT_RUNTIME=1表示在main函數(shù)運(yùn)行完之后,程序不退出,依然保持可執(zhí)行狀態(tài),方便后續(xù)可調(diào)用myFunction函數(shù)-s "EXTRA_EXPORTED_RUNTIME_METHODS=['ccall']"則表示導(dǎo)出一個(gè)運(yùn)行時(shí)的函數(shù)ccall,這個(gè)函數(shù)可以在 JS 中調(diào)用 C 程序的函數(shù)
進(jìn)行編譯之后,我們還需要修改生成的 function.html 文件,加入我們的函數(shù)調(diào)用邏輯如下:
<html>
??<body>
????<!--?其它?HTML?內(nèi)容?-->
????<button?class="mybutton">Run?myFunction</button>
??</body>
??<!--?其它?JS?引入?-->
??<script>
??????document
????????.querySelector(".mybutton")
????????.addEventListener("click",?function?()?{
??????????alert("check?console");
??????????var?result?=?Module.ccall(
????????????"myFunction",?//?需要調(diào)用的?C?函數(shù)名
????????????null,?//?函數(shù)返回類型
????????????null,?//?函數(shù)參數(shù)類型,默認(rèn)是數(shù)組
????????????null?//?函數(shù)需要傳入的參數(shù),默認(rèn)是數(shù)組
??????????);
????????});
????</script>
</html>
可以看到我們?cè)黾恿艘粋€(gè) Button,然后增加了一段腳本,為這個(gè) Button 注冊(cè)了 click 事件,在回調(diào)函數(shù)里,我們調(diào)用了 myFunction 函數(shù)。
在命令行中運(yùn)行 npx serve . 打開瀏覽器訪問(wèn) http://localhost:3000/function.html,查看結(jié)果如下:
只執(zhí)行 main 函數(shù):

嘗試點(diǎn)擊按鈕執(zhí)行 myFunction 函數(shù):


可以看到首先進(jìn)行 alert 彈框展示,然后打開控制臺(tái),可以看到 myFunction 的調(diào)用結(jié)果,打印 "MyFunction Called" 。
初嘗 Emscripten 文件系統(tǒng)
我們可以在 C/C++ 程序中使用 libc stdio API 如 fopen 、fclose 來(lái)訪問(wèn)你文件系統(tǒng),但是 JS 是運(yùn)行在瀏覽器提供的沙盒環(huán)境里,無(wú)法直接訪問(wèn)到本地文件系統(tǒng)。所以為了兼容 C/C++ 程序訪問(wèn)文件系統(tǒng),編譯為 WASM 之后依然能夠正常運(yùn)行,Emscripten 會(huì)在其 JS 膠水代碼里面模擬一個(gè)文件系統(tǒng),并提供和 libc stdio 一致的 API。
讓我們重新創(chuàng)建一個(gè)名為 file.c 的程序,添加如下代碼:
#include?<stdio.h>
int?main()?{
??FILE?*file?=?fopen("file.txt",?"rb");
??if?(!file)?{
????printf("cannot?open?file\n");
????return?1;
??}
??while?(!feof(file))?{
????char?c?=?fgetc(file);
????if?(c?!=?EOF)?{
??????putchar(c);
????}
??}
??fclose?(file);
??return?0;
}
上述代碼我們首先使用 fopen 訪問(wèn) file.txt ,然后一行一行的讀取文件內(nèi)容,如果程序執(zhí)行過(guò)程中有任何的出錯(cuò),就會(huì)打印錯(cuò)誤。
我們?cè)谀夸浵滦陆?file.txt 文件,并加入如下內(nèi)容:
==
This?data?has?been?read?from?a?file.
The?file?is?readable?as?if?it?were?at?the?same?location?in?the?filesystem,?including?directories,?as?in?the?local?filesystem?where?you?compiled?the?source.
==
如果我們要編譯這個(gè)程序,并確保能夠在 JS 中正常運(yùn)行,還需要在編譯時(shí)加上 preload 參數(shù),提前將文件內(nèi)容加載進(jìn) Emscripten runtime,因?yàn)樵?C/C++ 等程序上訪問(wèn)文件都是同步操作,而 JS 是基于事件模型的異步操作,且在 Web 中只能通過(guò) XHR 的形式去訪問(wèn)文件(Web Worker、Node.js 可同步訪問(wèn)文件),所以需要提前將文件加載好,確保在代碼編譯之前,文件已經(jīng)準(zhǔn)備好了,這樣 C/C++ 代碼可以直接訪問(wèn)到文件。
運(yùn)行如下命令進(jìn)行代碼編譯:
emcc?file.c?-o?file.html?-s?EXIT_RUNTIME=1?--preload-file?file.txt
上述添加了
-s EXIT_RUNTIME=1,依然是確保main邏輯執(zhí)行完之后,程序不會(huì)退出。
然后運(yùn)行我們的本地服務(wù)器,訪問(wèn) http://localhost:3000/file.html,可以查看結(jié)果:

嘗試編譯已存在的 WebP 模塊并使用
通過(guò)上面三個(gè)例子,我們已經(jīng)了解了基礎(chǔ)的 C/C++ 如打印、函數(shù)調(diào)用、文件系統(tǒng)相關(guān)的內(nèi)容如何編譯為 WASM,并在 JS 中運(yùn)行,這里的 JS 特指 Web 和 Node.js 環(huán)境,通過(guò)上面的例子基本上絕大部分自己寫的 C/C++ 程序都可以自行編譯到 WASM 使用了。
而之前我們也提到過(guò),其實(shí)當(dāng)前 WebAssembly 最大的一個(gè)應(yīng)用場(chǎng)景,就是最大程度的復(fù)用當(dāng)前已有語(yǔ)言的生態(tài),如 C/C++ 生態(tài)的庫(kù),這些庫(kù)通常都依賴 C 標(biāo)準(zhǔn)庫(kù)、操作系統(tǒng)、文件系統(tǒng)或其他依賴,而 Emscripten 最厲害的一點(diǎn)就在于能夠兼容絕大部分這些依賴的特性,盡管還存在一些限制,但是已經(jīng)足夠可用。
簡(jiǎn)單的測(cè)試
接下來(lái)我們來(lái)了解一下如何將一個(gè)現(xiàn)存的、比較復(fù)雜且廣泛使用的 C 模塊:libwebp,將其編譯到 WASM 并允許到 Web。libwebp 的源碼是用 C 實(shí)現(xiàn)的,能夠在 Github[16] 上找到它,同時(shí)可以了解到它的一些 API 文檔[17]。
首先準(zhǔn)備代碼,在我們的目錄下運(yùn)行如下命令:
git?clone?https://github.com/webmproject/libwebp
為了快速測(cè)試是否正確的接入了 libwebp 進(jìn)行使用,我們可以編寫一個(gè)簡(jiǎn)單的 C 函數(shù),然后在里面調(diào)用 libwebp 獲取版本的函數(shù),測(cè)試版本是否可以正確獲取。
我們?cè)谀夸浵聞?chuàng)建 webp.c 文件,添加如下內(nèi)容:
#include?"emscripten.h"
#include?"src/webp/encode.h"
EMSCRIPTEN_KEEPALIVE?int?version()?{
??return?WebPGetEncoderVersion();
}
上述的 WebPGetEncoderVersion 就是 libwebp 里面獲取當(dāng)前版本的函數(shù),而我們是通過(guò)導(dǎo)入 src/webp/encode.h 頭文件來(lái)獲取這個(gè)函數(shù)的,為了讓編譯器在編譯時(shí)能夠找到這個(gè)頭文件,我們需要在編譯的時(shí)候?qū)?libwebp 庫(kù)的頭文件地址告訴編譯器,并將編譯器需要的所有 libwebp 庫(kù)下的 C 文件傳給編譯器。
讓我們運(yùn)行如下編譯命令:
emcc?-O3?-s?WASM=1?-s?EXTRA_EXPORTED_RUNTIME_METHODS='["cwrap"]'?\
?-I?libwebp?\
?webp.c?\
?libwebp/src/{dec,dsp,demux,enc,mux,utils}/*.c
上述命令中主要做了如下工作:
-I libwebp將 libwebp 庫(kù)的頭文件地址告訴編譯器libwebp/src/{dec,dsp,demux,enc,mux,utils}/*.c將編譯器所需的 C 文件傳給編譯器,這里將dec,dsp,demux,enc,mux,utils等目錄下的所有 C 文件都傳遞給了編譯器,避免了一個(gè)個(gè)列出所需文件的繁瑣,然后讓編譯器去自動(dòng)識(shí)別那些沒(méi)有使用的文件,并將其過(guò)濾掉webp.c是我們編寫的 C 函數(shù),用于調(diào)用WebPGetEncoderVersion獲取庫(kù)版本-O3代表在編譯時(shí)進(jìn)行等級(jí)為 3 的優(yōu)化,包含內(nèi)聯(lián)函數(shù)、去除無(wú)用代碼、對(duì)代碼進(jìn)行各種壓縮優(yōu)化等- 而
-s WASM=1其實(shí)是默認(rèn)的,就是在編譯時(shí)輸出xx.out.wasm,這里之所以會(huì)設(shè)置這個(gè)選項(xiàng)主要是針對(duì)那些不支持 WASM 的 runtime,可以設(shè)置-s WASM=0,輸出等價(jià)的 JS 代碼替代 WASM EXTRA_EXPORTED_RUNTIME_METHODS= '["cwrap"]'則是輸出 runtime 的函數(shù)cwrap,類似ccall可以在 JS 中調(diào)用 C 函數(shù)
上述的編譯輸出只有 a.out.js 和 a.out.wasm ,我們還需要建一份 HTML 文檔來(lái)使用輸出的腳本代碼,新建 webp.html ,添加如下內(nèi)容:
<html>
??<head></head>
??<body></body>
??<script?src="./a.out.js"></script>
????<script>
??????Module.onRuntimeInitialized?=?async?_?=>?{
????????const?api?=?{
??????????version:?Module.cwrap('version',?'number',?[]),
????????};
????????console.log(api.version());
??????};
????</script>
</html>
值得注意的是,我們通常在
Module.onRuntimeInitialized的回調(diào)里面去執(zhí)行我們 WASM 相關(guān)的操作,因?yàn)?WASM 相關(guān)的代碼從加載到可用是需要一段時(shí)間的,而onRuntimeInitialized的回調(diào)則是確保 WASM 相關(guān)的代碼已經(jīng)加載完成,達(dá)到可用狀態(tài)。
接著我們可以運(yùn)行 npx serve . ,然后訪問(wèn) http://localhost:3000/webp.html,查看結(jié)果:

可以看到控制臺(tái)打印了 66049 版本號(hào)。
libwebp 通過(guò)十六進(jìn)制的
0xabc的 abc 來(lái)表示當(dāng)前版本a.b.c,例如 v0.6.1,則會(huì)被編碼成十六進(jìn)制0x000601,對(duì)應(yīng)的十進(jìn)制為 1537。而這里為十進(jìn)制 66049,轉(zhuǎn)成 16 進(jìn)制則為0x010201,表示當(dāng)前版本為 v1.2.1。
在 JavaScript 中獲取圖片并放入 wasm 中運(yùn)行
剛剛通過(guò)調(diào)用編碼器的 WebPGetEncoderVersion 方法來(lái)獲取版本號(hào)來(lái)證實(shí)了已經(jīng)成功編譯了 libwebp 庫(kù)到 wasm,然后可以在 JavaScript 使用它,接下來(lái)我們將了解更加復(fù)雜的操作,如何使用 libwebp 的編碼 API 來(lái)轉(zhuǎn)換圖片格式。
libwebp 的 encoding API 需要接收一個(gè)關(guān)于 RGB、RGBA、BGR 或 BGRA 的字節(jié)數(shù)組,幸運(yùn)的是,Canvas API 有一個(gè) CanvasRenderingContext2D.getImageData 方法,能夠返回一個(gè) Uint8ClampedArray ,這個(gè)數(shù)組包含 RGBA 格式的圖片數(shù)據(jù)。
首先我們需要在 JavaScript 中編寫加載圖片的函數(shù),將其寫到上一步創(chuàng)建的 HTML 文件里:
<script?src="./a.out.js"></script>
<script>
??Module.onRuntimeInitialized?=?async?_?=>?{
????const?api?=?{
??????version:?Module.cwrap('version',?'number',?[]),
????};
????console.log(api.version());
??};
??
???async?function?loadImage(src)?{
?????//?加載圖片
??????const?imgBlob?=?await?fetch(src).then(resp?=>?resp.blob());
??????const?img?=?await?createImageBitmap(imgBlob);
??????
??????//?設(shè)置?canvas?畫布的大小與圖片一致
??????const?canvas?=?document.createElement('canvas');
??????canvas.width?=?img.width;
??????canvas.height?=?img.height;
??????
??????//?將圖片繪制到?canvas?上
??????const?ctx?=?canvas.getContext('2d');
??????ctx.drawImage(img,?0,?0);
??????return?ctx.getImageData(0,?0,?img.width,?img.height);
????}
</script>
現(xiàn)在剩下的操作則是如何將圖片數(shù)據(jù)從 JavaScript 復(fù)制到 wasm,為了達(dá)成這個(gè)目的,需要在先前的 webp.c 函數(shù)里面暴露額外的方法:
- 一個(gè)為 wasm 里面的圖片分配內(nèi)存的方法
- 一個(gè)釋放內(nèi)存的方法
修改 webp.c 如下:
#include?<stdlib.h>?//?此頭文件導(dǎo)入用于分配內(nèi)存的?malloc?方法和釋放內(nèi)存的?free?方法
EMSCRIPTEN_KEEPALIVE
uint8_t*?create_buffer(int?width,?int?height)?{
??return?malloc(width?*?height?*?4?*?sizeof(uint8_t));
}
EMSCRIPTEN_KEEPALIVE
void?destroy_buffer(uint8_t*?p)?{
??free(p);
}
create_buffer 為 RGBA 的圖片分配內(nèi)存,RGBA 圖片一個(gè)像素包含 4 個(gè)字節(jié),所以代碼中需要添加 4 * sizeof(uint8_t) ,malloc 函數(shù)返回的指針指向所分配內(nèi)存的第一塊內(nèi)存單元地址,當(dāng)這個(gè)指針?lè)祷亟o JavaScript 使用時(shí),會(huì)被當(dāng)做一個(gè)簡(jiǎn)單的數(shù)字處理。當(dāng)通過(guò) cwrap 函數(shù)獲取暴露給 JavaScript 的對(duì)應(yīng) C 函數(shù)時(shí),可以使用這個(gè)指針數(shù)字找到復(fù)制圖片數(shù)據(jù)的內(nèi)存開始位置。
我們?cè)?HTML 文件中添加額外的代碼如下:
<script?src="./a.out.js"></script>
<script>
??Module.onRuntimeInitialized?=?async?_?=>?{????
????const?api?=?{
??????version:?Module.cwrap('version',?'number',?[]),
??????create_buffer:?Module.cwrap('create_buffer',?'number',?['number',?'number']),
??????destroy_buffer:?Module.cwrap('destroy_buffer',?'',?['number']),
??????encode:?Module.cwrap("encode",?"",?["number","number","number","number",]),
??????free_result:?Module.cwrap("free_result",?"",?["number"]),
??????get_result_pointer:?Module.cwrap("get_result_pointer",?"number",?[]),
??????get_result_size:?Module.cwrap("get_result_size",?"number",?[]),
????};
????
????const?image?=?await?loadImage('./image.jpg');
????const?p?=?api.create_buffer(image.width,?image.height);
????Module.HEAP8.set(image.data,?p);
????
????//?...?call?encoder?...
????
????api.destroy_buffer(p);
??};
??
???async?function?loadImage(src)?{
?????//?加載圖片
??????const?imgBlob?=?await?fetch(src).then(resp?=>?resp.blob());
??????const?img?=?await?createImageBitmap(imgBlob);
??????
??????//?設(shè)置?canvas?畫布的大小與圖片一致
??????const?canvas?=?document.createElement('canvas');
??????canvas.width?=?img.width;
??????canvas.height?=?img.height;
??????
??????//?將圖片繪制到?canvas?上
??????const?ctx?=?canvas.getContext('2d');
??????ctx.drawImage(img,?0,?0);
??????return?ctx.getImageData(0,?0,?img.width,?img.height);
????}
</script>
可以看到上述代碼除了導(dǎo)入之前添加的 create_buffer 和 destroy_buffer 外,還有很多用于編碼文件等方面的函數(shù),我們將在后續(xù)講解,除此之外,代碼首先加載了一份 image.jpg 的圖片,然后調(diào)用 C 函數(shù)為此圖片數(shù)據(jù)分配內(nèi)存,并相應(yīng)的拿到返回的指針傳給 WebAssembly 的 Module.HEAP8 ,在內(nèi)存開始位置 p,寫入圖片的數(shù)據(jù),最后會(huì)釋放分配的內(nèi)存。
編碼圖片
現(xiàn)在圖片數(shù)據(jù)已經(jīng)加載進(jìn) wasm 的內(nèi)存中,可以調(diào)用 libwebp 的 encoder 方法來(lái)完成編碼過(guò)程了,通過(guò)查閱 WebP 的文檔[18],發(fā)現(xiàn)可以使用 WebPEncodeRGBA 函數(shù)來(lái)完成工作。這個(gè)函數(shù)接收一個(gè)指向圖片數(shù)據(jù)的指針以及它的尺寸,以及每次需要跨越的 stride 步長(zhǎng),這里為 4 個(gè)字節(jié)(RGBA),一個(gè)區(qū)間在 0-100 的可選的質(zhì)量參數(shù)。在編碼的過(guò)程中,WebPEncodeRGBA 會(huì)分配一塊用于輸出數(shù)據(jù)的內(nèi)存,我們需要在編碼完成之后調(diào)用 WebPFree 來(lái)釋放這塊內(nèi)存。
我們打開 webp.c 文件,添加如下處理編碼的代碼:
int?result[2];
EMSCRIPTEN_KEEPALIVE
void?encode(uint8_t*?img_in,?int?width,?int?height,?float?quality)?{
??uint8_t*?img_out;
??size_t?size;
??size?=?WebPEncodeRGBA(img_in,?width,?height,?width?*?4,?quality,?&img_out);
??result[0]?=?(int)img_out;
??result[1]?=?size;
}
EMSCRIPTEN_KEEPALIVE
void?free_result(uint8_t*?result)?{
??WebPFree(result);
}
EMSCRIPTEN_KEEPALIVE
int?get_result_pointer()?{
??return?result[0];
}
EMSCRIPTEN_KEEPALIVE
int?get_result_size()?{
??return?result[1];
}
上述 WebPEncodeRGBA 函數(shù)執(zhí)行的結(jié)果為分配一塊輸出數(shù)據(jù)的內(nèi)存以及返回內(nèi)存的大小。因?yàn)?C 函數(shù)無(wú)法使用數(shù)組作為返回值(除非我們需要進(jìn)行動(dòng)態(tài)內(nèi)存分配),所以我們使用一個(gè)全局靜態(tài)數(shù)組來(lái)獲取返回的結(jié)果,這可能不是很規(guī)范的 C 代碼寫法,同時(shí)它要求 wasm 指針為 32 比特長(zhǎng),但是為了簡(jiǎn)單起見我們可以暫時(shí)容忍這種做法。
現(xiàn)在 C 側(cè)的相關(guān)邏輯已經(jīng)編寫完畢,可以在 JavaScript 側(cè)調(diào)用編碼函數(shù),獲取圖片數(shù)據(jù)的指針和圖片所占用的內(nèi)存大小,將這份數(shù)據(jù)保存到 WASM 的緩沖中,然后釋放 wasm 在處理圖片時(shí)所分配的內(nèi)存,讓我們打開 HTML 文件完成上述描述的邏輯:
<script?src="./a.out.js"></script>
<script>
??Module.onRuntimeInitialized?=?async?_?=>?{????
????const?api?=?{
??????version:?Module.cwrap('version',?'number',?[]),
??????create_buffer:?Module.cwrap('create_buffer',?'number',?['number',?'number']),
??????destroy_buffer:?Module.cwrap('destroy_buffer',?'',?['number']),
??????encode:?Module.cwrap("encode",?"",?["number","number","number","number",]),
??????free_result:?Module.cwrap("free_result",?"",?["number"]),
??????get_result_pointer:?Module.cwrap("get_result_pointer",?"number",?[]),
??????get_result_size:?Module.cwrap("get_result_size",?"number",?[]),
????};
????
????const?image?=?await?loadImage('./image.jpg');
????const?p?=?api.create_buffer(image.width,?image.height);
????Module.HEAP8.set(image.data,?p);
????
????api.encode(p,?image.width,?image.height,?100);
????const?resultPointer?=?api.get_result_pointer();
????const?resultSize?=?api.get_result_size();
????const?resultView?=?new?Uint8Array(Module.HEAP8.buffer,?resultPointer,?resultSize);
????const?result?=?new?Uint8Array(resultView);
????api.free_result(resultPointer);
????
????api.destroy_buffer(p);
??};
??
???async?function?loadImage(src)?{
?????//?加載圖片
??????const?imgBlob?=?await?fetch(src).then(resp?=>?resp.blob());
??????const?img?=?await?createImageBitmap(imgBlob);
??????
??????//?設(shè)置?canvas?畫布的大小與圖片一致
??????const?canvas?=?document.createElement('canvas');
??????canvas.width?=?img.width;
??????canvas.height?=?img.height;
??????
??????//?將圖片繪制到?canvas?上
??????const?ctx?=?canvas.getContext('2d');
??????ctx.drawImage(img,?0,?0);
??????return?ctx.getImageData(0,?0,?img.width,?img.height);
????}
</script>
在上述代碼中我們通過(guò) loadImage 函數(shù)加載了一張本地的 image.jpg 圖片,你需要事先準(zhǔn)備一張圖片放置在 emcc 編譯器輸出的目錄下,也就是我們的 HTML 文件目錄下使用。
注意:
new Uint8Array(someBuffer)將會(huì)在同樣的內(nèi)存塊上創(chuàng)建一個(gè)新視圖,而new Uint8Array(someTypedArray)只會(huì)復(fù)制someTypedArray的數(shù)據(jù),確保使用復(fù)制的數(shù)據(jù)進(jìn)行操作,不會(huì)修改原內(nèi)存數(shù)據(jù)。
當(dāng)你的圖片比較大時(shí),因?yàn)?wasm 不能自動(dòng)擴(kuò)充內(nèi)存,如果默認(rèn)分配的內(nèi)存無(wú)法容納 input 和 output 圖片數(shù)據(jù)的內(nèi)存,你可能會(huì)遇到如下報(bào)錯(cuò):

但是我們例子中使用的圖片比較小,所以只需要單純的在編譯時(shí)加上一個(gè)過(guò)濾參數(shù) -s ALLOW_MEMORY_GROWTH=1 忽略這個(gè)報(bào)錯(cuò)信息即可:
emcc?-O3?-s?WASM=1?-s?EXTRA_EXPORTED_RUNTIME_METHODS='["cwrap"]'?\
????-I?libwebp?\
????webp.c?\
????libwebp/src/{dec,dsp,demux,enc,mux,utils}/*.c?\
????-s?ALLOW_MEMORY_GROWTH=1
再次運(yùn)行上述命令,得到添加了編碼函數(shù)的 wasm 代碼和對(duì)應(yīng)的 JavaScript 膠水代碼,這樣當(dāng)我們打開 HTML 文件時(shí),它已經(jīng)能夠?qū)⒁环?JPG 文件編碼成 WebP 的格式,為了進(jìn)一步證實(shí)這個(gè)觀點(diǎn),我們可以將圖片展示到 Web 界面上,通過(guò)修改 HTML 文件,添加如下代碼:
<script>
??//?...
????api.encode(p,?image.width,?image.height,?100);
????const?resultPointer?=?api.get_result_pointer();
????const?resultSize?=?api.get_result_size();
????const?resultView?=?new?Uint8Array(Module.HEAP8.buffer,?resultPointer,?resultSize);
????const?result?=?new?Uint8Array(resultView);
????
????//?添加到這里
????const?blob?=?new?Blob([result],?{type:?'image/webp'});
????const?blobURL?=?URL.createObjectURL(blob);
????const?img?=?document.createElement('img');
????img.src?=?blobURL;
????document.body.appendChild(img)
????
????api.free_result(resultPointer);
????
????api.destroy_buffer(p);
</script>
然后刷新瀏覽器,你應(yīng)該可以看到如下界面:

通過(guò)將這個(gè)文件下載到本地,可以看到其格式轉(zhuǎn)成了 WebP:

通過(guò)上述的流程我們成功編譯了現(xiàn)有的 libwebp C 庫(kù)到 wasm 使用,并將 JPG 圖片轉(zhuǎn)成了 WebP 格式并展示在 Web 界面上,通過(guò) wasm 來(lái)處理計(jì)算密集型的轉(zhuǎn)碼操作可以大大提高網(wǎng)頁(yè)的性能,這也是 WebAssembly 帶來(lái)的主要優(yōu)勢(shì)之一。
如何編譯 FFmpeg 到 WebAssembly?
好家伙,剛剛教會(huì) 1+1,就開始解二次方程了。??
在上個(gè)例子中我們成功編譯了已經(jīng)存在的 C 模塊到 WebAssembly,但是有很多更大型的項(xiàng)目依賴于 C 標(biāo)準(zhǔn)庫(kù)、操作系統(tǒng)、文件系統(tǒng)或其他依賴,這些項(xiàng)目在編譯前依賴 autoconfig/automake 等庫(kù)來(lái)生成系統(tǒng)特定的代碼。
所以你經(jīng)常會(huì)看到一些庫(kù)在使用之前,需要經(jīng)過(guò)如下的步驟:
./configure?#?處理前置依賴
make?#?使用?gcc?等進(jìn)行編譯構(gòu)建,生成對(duì)象文件
而 Emscripten 提供了 emconfigure 和 emmake 來(lái)封裝這些命令,并注入合適的參數(shù)來(lái)抹平那些有前置依賴的項(xiàng)目,如果使用 emcc 來(lái)處理這些有大量前置依賴的項(xiàng)目,命令會(huì)變成如下操作:
emmconfigure?./configure?#?將配置中的默認(rèn)編譯器,如?gcc?替換成?emcc?編譯器
emmake?make?# emmake make -j4 調(diào)起多核編譯,生成 wasm 對(duì)象文件,而非傳統(tǒng)的 C 對(duì)象文件
emcc?xxx.o?#?將?make?生成的對(duì)象文件編譯成?wasm?文件?+?JS?膠水代碼
接下來(lái)我們通過(guò)實(shí)際編譯 ffmpeg 來(lái)講解如何處理這種依賴 autoconfig/automake 等庫(kù)來(lái)生成特定的代碼。
經(jīng)過(guò)實(shí)踐發(fā)現(xiàn) ffmpeg 的編譯依賴于特定的 ffmpeg 版本、Emscripten 版本、操作系統(tǒng)環(huán)境等,所以以下的 ffmpeg 的編譯都是限制在特定的條件下進(jìn)行的,主要是為之后通用的 ffmpeg 的編譯提供一種思路和調(diào)試方法。
準(zhǔn)備目錄
這一次我們創(chuàng)建 WebAssembly 目錄,然后在這個(gè)目錄下放置 ffmpeg 源碼、以及后續(xù)要用到的 x264 解碼器的相關(guān)代碼:
mkdir?WebAssembly
#?Clone?代碼倉(cāng)庫(kù)
git?clone?https:?//?github?.?com?/?emscripten-core?/?emsdk?.?git
#?進(jìn)入倉(cāng)庫(kù)
cd?emsdk
#?獲取最新代碼,如果是新?clone?的這一步可以不需要
git?pull
編譯步驟
使用 Emscripten 編譯大部分復(fù)雜的 C/C++ 庫(kù)時(shí),主要需要三個(gè)步驟:
- 使用
emconfigure運(yùn)行項(xiàng)目的configure文件將 C/C++ 代碼編譯器從gcc/g++換成emcc/em++ - 通過(guò)
emmake make來(lái)構(gòu)建 C/C++ 項(xiàng)目,生成 wasm 對(duì)象的.o文件 - 調(diào)用
emcc接收編譯的對(duì)象文件.o文件,然后輸出最終的 WASM 和 JS 膠水代碼
安裝特定依賴
注意:這一步我們?cè)谥v解 Emscripten 的開頭就已經(jīng)安裝了對(duì)應(yīng)的版本,這里只是再?gòu)?qiáng)調(diào)一下版本。
為了驗(yàn)證 ffmpeg 的驗(yàn)證,我們需要依賴特定的版本,下面詳細(xì)講解依賴的各種文件版本。
首先安裝 1.39.18 版本的 Emscripten 編譯器,進(jìn)入之前我們 Clone 到本地的 emsdk 項(xiàng)目運(yùn)行如下命令:
./emsdk?install?1.39.18
./emsdk?activate?1.39.18
source?./emsdk_env.sh
通過(guò)在命令行中輸入如下命令驗(yàn)證是否切換成功:
emcc?-v?#?輸出?1.39.18
在 emsdk 同級(jí)下載分支為 n4.3.1 的 ffmpeg 代碼:
git?clone?--depth?1?--branch?n4.3.1?https://github.com/FFmpeg/FFmpeg
使用 emconfigure 處理 configure 文件
通過(guò)如下腳本來(lái)處理 configure 文件:
export?CFLAGS="-s?USE_PTHREADS?-O3"
export?LDFLAGS="$CFLAGS?-s?INITIAL_MEMORY=33554432"
emconfigure?./configure?\
??--target-os=none?\?#?設(shè)置為?none?來(lái)去除特定操作系統(tǒng)的一些依賴
??--arch=x86_32?\?#?選中架構(gòu)為?x86_32????????????????????????????????????????????????????????????????????????????????????????????????????????????????
??--enable-cross-compile?\?#?處理跨平臺(tái)操作
??--disable-x86asm?\??#?關(guān)閉?x86asm????????????????????????????????????????????????????????????????????????????????????????????????????????????????
??--disable-inline-asm?\??#?關(guān)閉內(nèi)聯(lián)的?asm????????????????????????????????????????????????????????
??--disable-stripping?\?#?關(guān)閉處理?strip?的功能,避免誤刪一些內(nèi)容
??--disable-programs?\?#?加速編譯
??--disable-doc?\??#?添加一些?flag?輸出
??--extra-cflags="$CFLAGS"?\
??--extra-cxxflags="$CFLAGS"?\
??--extra-ldflags="$LDFLAGS"?\??????????????????
??--nm="llvm-nm"?\??#?使用?llvm?的編譯器?????????????????????????????????????????????????????????????
??--ar=emar?\????????????????????????
??--ranlib=emranlib?\
??--cc=emcc?\?#?將?gcc?替換為?emcc
??--cxx=em++?\?#?將?g++?替換為?em++
??--objcc=emcc?\
??--dep-cc=emcc?
上述腳本主要做了如下幾件事:
USE_PTHREADS開啟pthreads支持-O3表示在編譯時(shí)優(yōu)化代碼體積,一般可以從 30MB 壓縮到 15MBINITIAL_MEMORY設(shè)置為 33554432 (32MB),主要是 Emscripten 可能占用 19MB,所以設(shè)置更大的內(nèi)存容量來(lái)避免在編譯過(guò)程中可分配的內(nèi)存不足的問(wèn)題- 實(shí)際使用
emconfigure來(lái)配置configure文件,替換gcc編譯器為emcc,以及設(shè)置一些必要的操作來(lái)處理可能遇到的編譯 BUG,最終生成用于編譯構(gòu)建的配置文件
使用 emmake make 來(lái)構(gòu)建依賴
通過(guò)上述步驟,就處理好了配置文件,接下來(lái)需要通過(guò) emmake 來(lái)構(gòu)建實(shí)際的依賴,通過(guò)在命令行中運(yùn)行如下命令:
#?構(gòu)建最終的?ffmpeg.wasm?文件
emmake?make?-j4
通過(guò)上述的編譯,會(huì)生成如下四個(gè)文件:
- ffmpeg
- ffmpeg_g
- ffmpeg_g.wasm
- ffmpeg_g.worker.js
前兩個(gè)都是 JS 文件,第三個(gè)為 wasm 模塊,第四個(gè)是處理 worker 中運(yùn)行相關(guān)邏輯的函數(shù),上述生成的文件的理想形式應(yīng)該為三個(gè),為了達(dá)成這種自定義的編譯,有必要自定義使用 emcc 命令來(lái)進(jìn)行處理。
使用 emcc 進(jìn)行編譯輸出
在 FFmpeg 目錄下創(chuàng)建 wasm 文件夾,用于放置構(gòu)建之后的文件,然后自定義編譯文件輸出如下:
mkdir?-p?wasm/dist
emcc?\???????????????????
?-I.?-I./fftools?\??
??-Llibavcodec?-Llibavdevice?-Llibavfilter?-Llibavformat?-Llibavresample?-Llibavutil?-Llibpostproc?-Llibswscale?-Llibswresample?\
??-Qunused-arguments?\????
??-o?wasm/dist/ffmpeg-core.js?fftools/ffmpeg_opt.c?fftools/ffmpeg_filter.c?fftools/ffmpeg_hw.c?fftools/cmdutils.c?fftools/ffmpeg.c?\
??-lavdevice?-lavfilter?-lavformat?-lavcodec?-lswresample?-lswscale?-lavutil?-lm?\
??-O3?\????????????????
??-s?USE_SDL=2?\????#?使用?SDL2
??-s?USE_PTHREADS=1?\
??-s?PROXY_TO_PTHREAD=1?\?#?將?main?函數(shù)與瀏覽器/UI主線程分離??
??-s?INVOKE_RUN=0?\?#?執(zhí)行?C?函數(shù)時(shí)不首先執(zhí)行?main?函數(shù)???????????
??-s?EXPORTED_FUNCTIONS="[_main,?_proxy_main]"?\
??-s?EXTRA_EXPORTED_RUNTIME_METHODS="[FS,?cwrap,?setValue,?writeAsciiToMemory]"?\
??-s?INITIAL_MEMORY=33554432
上述的腳本主要有如下幾點(diǎn)改進(jìn):
-s PROXY_TO_PTHREAD=1在編譯時(shí)設(shè)置了pthread時(shí),使得程序具備響應(yīng)式特效-o wasm/dist/ffmpeg-core.js則將原ffmpegjs 文件的輸出重命名為ffmpeg-core.js,對(duì)應(yīng)的輸出ffmpeg-core.wasm和ffmpeg-core.worker.js-s EXPORTED_FUNCTIONS="[_main, _proxy_main]"導(dǎo)出 ffmpeg 對(duì)應(yīng)的 C 文件里的main函數(shù),proxy_main則是通過(guò)設(shè)置PROXY_TO_PTHREAD代理main函數(shù)用于外部使用-s EXTRA_EXPORTED_RUNTIME_METHODS="[FS, cwrap, setValue, writeAsciiToMemory]"則是導(dǎo)出一些 runtime 的輔助函數(shù),用于導(dǎo)出 C 函數(shù)、處理文件系統(tǒng)、指針的操作
通過(guò)上述編譯命令最終輸出下面三個(gè)文件:
ffmpeg-core.js
ffmpeg-core.wasm
ffmpeg-core.worker.js
使用編譯完成的 ffmpeg wasm 模塊
在 wasm 目錄下創(chuàng)建 ffmpeg.js 文件,在其中寫入如下代碼:
const?Module?=?require('./dist/ffmpeg-core.js');
Module.onRuntimeInitialized?=?()?=>?{
??const?ffmpeg?=?Module.cwrap('proxy_main',?'number',?['number',?'number']);
};
然后通過(guò)如下命令運(yùn)行上述代碼:
node?--experimental-wasm-threads?--experimental-wasm-bulk-memory?ffmpeg.js
上述代碼解釋如下:
onRuntimeInitialized是加載 WebAssembly 模塊完成之后執(zhí)行的邏輯,我們所有相關(guān)邏輯需要在這個(gè)函數(shù)中編寫cwrap則用于導(dǎo)出 C 文件中(fftools/ffmpeg.c)的proxy_main使用,函數(shù)的簽名為int main(int argc, char **argv),其中int對(duì)應(yīng)到 JavaScript 就是number,argc 表示參數(shù)的個(gè)數(shù) ,而char **argv是 C 中的指針,表示實(shí)際參數(shù)的指針數(shù)組,也可以映射到number接著處理
ffmpeg的傳參兼容邏輯,對(duì)于命令行中運(yùn)行ffmpeg -hide_banner,在我們代碼里通過(guò)函數(shù)調(diào)用需要main(2, ["./ffmpeg", "-hide_banner"]),第一個(gè)參數(shù)很好解決,那么我們?nèi)绾蝹鬟f一個(gè)字符串?dāng)?shù)組呢?這個(gè)問(wèn)題可以分解為兩個(gè)部分:- 我們需要將 JavaScript 的字符串轉(zhuǎn)換成 C 中的字符數(shù)組
- 我們需要將 JavaScript 中的數(shù)組轉(zhuǎn)換為 C 中的指針數(shù)組
第一部分很簡(jiǎn)單,因?yàn)?Emscripten 提供了一個(gè)輔助函數(shù) writeAsciiToMemory 來(lái)完成這一工作:
const?str?=?"FFmpeg.wasm";
const?buf?=?Module._malloc(str.length?+?1);?//?額外分配一個(gè)字節(jié)的空間來(lái)存放?0?表示字符串的結(jié)束
Module.writeAsciiToMemory(str,?buf);
第二部分有一點(diǎn)困難,我們需要?jiǎng)?chuàng)建 C 中的 32 位整數(shù)的指針數(shù)組,可以借助 setValue 來(lái)幫助我們創(chuàng)建這個(gè)數(shù)組:
const?ptrs?=?[123,?3455];
const?buf?=?Module._malloc(ptrs.length?*?Uint32Array.BYTES_PER_ELEMENT);
ptrs.forEach((p,?idx)?=>?{
??Module.setValue(buf?+?(Uint32Array.BYTES_PER_ELEMENT?*?idx),?p,?'i32');
});
將上述的代碼合并起來(lái),我們就可以獲取一個(gè)能與 ffmpeg 交互的程序:
const?Module?=?require('./dist/ffmpeg-core');
Module.onRuntimeInitialized?=?()?=>?{
??const?ffmpeg?=?Module.cwrap('proxy_main',?'number',?['number',?'number']);
??const?args?=?['ffmpeg',?'-hide_banner'];
??const?argsPtr?=?Module._malloc(args.length?*?Uint32Array.BYTES_PER_ELEMENT);
??args.forEach((s,?idx)?=>?{
????const?buf?=?Module._malloc(s.length?+?1);
????Module.writeAsciiToMemory(s,?buf);
????Module.setValue(argsPtr?+?(Uint32Array.BYTES_PER_ELEMENT?*?idx),?buf,?'i32');
??})
??ffmpeg(args.length,?argsPtr);
};
然后通過(guò)同樣的命令運(yùn)行程序:
node?--experimental-wasm-threads?--experimental-wasm-bulk-memory?ffmpeg.js
上述運(yùn)行的結(jié)果如下:

可以看到我們成功編譯并運(yùn)行了 ffmpeg ??。
處理 Emscripten 文件系統(tǒng)
Emscripten 內(nèi)建了一個(gè)虛擬的文件系統(tǒng)來(lái)支持 C 中標(biāo)準(zhǔn)的文件讀取和寫入,所以我們需要將音頻文件傳給 ffmpeg.wasm 時(shí)先寫入到文件系統(tǒng)中。
可以戳此查看更多關(guān)于文件系統(tǒng) API[19] 。
為了完成上述的任務(wù),只需要使用到 FS 模塊的兩個(gè)函數(shù) FS.writeFile() 和 FS.readFile() ,對(duì)于從文件系統(tǒng)中讀取和寫入的所有數(shù)據(jù)都要求是 JavaScript 中的 Uint8Array 類型,所以在消費(fèi)數(shù)據(jù)之前有必要約定數(shù)據(jù)類型。
我們將通過(guò) fs.readFileSync() 方法讀取名為 flame.avi 的視頻文件,然后使用 FS.writeFile() 將其寫入到 Emscripten 文件系統(tǒng)。
const?fs?=?require('fs');
const?Module?=?require('./dist/ffmpeg-core');
Module.onRuntimeInitialized?=?()?=>?{
??const?data?=?Uint8Array.from(fs.readFileSync('./flame.avi'));
??Module.FS.writeFile('flame.avi',?data);
??const?ffmpeg?=?Module.cwrap('proxy_main',?'number',?['number',?'number']);
??const?args?=?['ffmpeg',?'-hide_banner'];
??const?argsPtr?=?Module._malloc(args.length?*?Uint32Array.BYTES_PER_ELEMENT);
??args.forEach((s,?idx)?=>?{
????const?buf?=?Module._malloc(s.length?+?1);
????Module.writeAsciiToMemory(s,?buf);
????Module.setValue(argsPtr?+?(Uint32Array.BYTES_PER_ELEMENT?*?idx),?buf,?'i32');
??})
??ffmpeg(args.length,?argsPtr);
};
使用 ffmpeg.wasm 編譯視頻
現(xiàn)在我們已經(jīng)可以將視頻文件保存到 Emscripten 文件系統(tǒng)了,接下來(lái)就是實(shí)際使用編譯好的 ffmepg 來(lái)進(jìn)行視頻的轉(zhuǎn)碼了。
我們修改代碼如下:
const?fs?=?require('fs');
const?Module?=?require('./dist/ffmpeg-core');
Module.onRuntimeInitialized?=?()?=>?{
??const?data?=?Uint8Array.from(fs.readFileSync('./flame.avi'));
??Module.FS.writeFile('flame.avi',?data);
??const?ffmpeg?=?Module.cwrap('proxy_main',?'number',?['number',?'number']);
??const?args?=?['ffmpeg',?'-hide_banner',?'-report',?'-i',?'flame.avi',?'flame.mp4'];
??const?argsPtr?=?Module._malloc(args.length?*?Uint32Array.BYTES_PER_ELEMENT);
??args.forEach((s,?idx)?=>?{
????const?buf?=?Module._malloc(s.length?+?1);
????Module.writeAsciiToMemory(s,?buf);
????Module.setValue(argsPtr?+?(Uint32Array.BYTES_PER_ELEMENT?*?idx),?buf,?'i32');
??});
??ffmpeg(args.length,?argsPtr);
??const?timer?=?setInterval(()?=>?{
????const?logFileName?=?Module.FS.readdir('.').find(name?=>?name.endsWith('.log'));
????if?(typeof?logFileName?!==?'undefined')?{
??????const?log?=?String.fromCharCode.apply(null,?Module.FS.readFile(logFileName));
??????if?(log.includes("frames?successfully?decoded"))?{
????????clearInterval(timer);
????????const?output?=?Module.FS.readFile('flame.mp4');
????????fs.writeFileSync('flame.mp4',?output);
??????}
????}
??},?500);
};
在上述代碼中,我們添加了一個(gè)定時(shí)器,因?yàn)?ffmpeg 轉(zhuǎn)碼視頻的過(guò)程是異步的,所以我們需要不斷的去讀取 Emscripten 文件系統(tǒng)中是否有轉(zhuǎn)碼好的文件標(biāo)志,當(dāng)拿到文件標(biāo)志且不為 undefined,我們就使用 Module.FS.readFile() 方法從 Emscripten 文件系統(tǒng)中讀取轉(zhuǎn)碼好的視頻文件,然后通過(guò) fs.writeFileSync() 將視頻寫入到本地文件系統(tǒng)。最終我們會(huì)收到如下結(jié)果:

在瀏覽器中使用 ffmpeg 轉(zhuǎn)碼視頻并播放
在上一步中,我們成功在 Node 端使用了編譯好的 ffmpeg 完成從了 avi 格式到 mp4 格式的轉(zhuǎn)碼,接下來(lái)我們將在瀏覽器中使用 ffmpeg 轉(zhuǎn)碼視頻,并在瀏覽器中播放。
之前我們編譯的 ffmpeg 雖然可以將 avi 格式轉(zhuǎn)碼到 mp4 ,但是這種通過(guò)默認(rèn)編碼格式轉(zhuǎn)碼的 mp4 的文件無(wú)法直接在瀏覽器中播放,因?yàn)闉g覽器不支持這種編碼,所以我們需要使用 libx264 編碼器來(lái)將 mp4 文件編碼成瀏覽器可播放的編碼格式。
首先在 WebAssembly 目錄下下載 x264 的編碼器源碼:
curl?-OL?https://download.videolan.org/pub/videolan/x264/snapshots/x264-snapshot-20170226-2245-stable.tar.bz2
tar?xvfj?x264-snapshot-20170226-2245-stable.tar.bz2
然后進(jìn)入 x264 的文件夾,可以創(chuàng)建一個(gè) build-x264.sh 文件,并加入如下內(nèi)容:
?#!/bin/bash?-x
ROOT=$PWD
BUILD_DIR=$ROOT/build
cd?$ROOT/x264-snapshot-20170226-2245-stable
ARGS=(
??--prefix=$BUILD_DIR
??--host=i686-gnu?????????????????????#?use?i686?gnu
??--enable-static?????????????????????#?enable?building?static?library
??--disable-cli???????????????????????#?disable?cli?tools
??--disable-asm???????????????????????#?disable?asm?optimization
??--extra-cflags="-s?USE_PTHREADS=1"??#?pass?this?flags?for?using?pthreads
)
emconfigure?./configure?"${ARGS[@]}"
emmake?make?install-lib-static?-j4
cd?-
注意需要在 WebAssembly 目錄下運(yùn)行如下命令來(lái)構(gòu)建 x264:
bash?x264-snapshot-20170226-2245-stable/build-x264.sh
安裝了 x264 編碼器之后,就可以在 ffmpeg 的編譯腳本中加入打開 x264 的開關(guān),這一次我們?cè)?ffmpeg 文件夾下創(chuàng)建 Bash 腳本用于構(gòu)建,創(chuàng)建 build.sh 如下:
?#!/bin/bash?-x
emcc?-v
ROOT=$PWD
BUILD_DIR=$ROOT/build
cd?$ROOT/FFmpeg
CFLAGS="-s?USE_PTHREADS?-I$BUILD_DIR/include"
LDFLAGS="$CFLAGS?-L$BUILD_DIR/lib?-s?INITIAL_MEMORY=33554432"?#?33554432?bytes?=?32?MB
CONFIG_ARGS=(
?--target-os=none????????#?use?none?to?prevent?any?os?specific?configurations
?--arch=x86_32???????????#?use?x86_32?to?achieve?minimal?architectural?optimization
?--enable-cross-compile??#?enable?cross?compile
?--disable-x86asm????????#?disable?x86?asm
?--disable-inline-asm????#?disable?inline?asm
?--disable-stripping
?--disable-programs??????#?disable?programs?build?(incl.?ffplay,?ffprobe?&?ffmpeg)
?--disable-doc???????????#?disable?doc
?--enable-gpl????????????##?required?by?x264
?--enable-libx264????????##?enable?x264
?--extra-cflags="$CFLAGS"
?--extra-cxxflags="$CFLAGS"
?--extra-ldflags="$LDFLAGS"
?--nm="llvm-nm"
?--ar=emar
?--ranlib=emranlib
?--cc=emcc
?--cxx=em++
?--objcc=emcc
?--dep-cc=emcc
?)
emconfigure?./configure?"${CONFIG_ARGS[@]}"
?#?build?ffmpeg.wasm
emmake?make?-j4
cd?-
針對(duì)上述編譯腳本,在 WebAssembly 目錄下運(yùn)行如下命令來(lái)進(jìn)行配置文件的處理以及文件編譯:
bash?FFmpeg/build.sh
然后創(chuàng)建用于自定義輸出構(gòu)建文件的腳本文件 build-with-emcc.sh :
ROOT=$PWD
BUILD_DIR=$ROOT/build
cd?FFmpeg
ARGS=(
??-I.?-I./fftools?-I$BUILD_DIR/include
??-Llibavcodec?-Llibavdevice?-Llibavfilter?-Llibavformat?-Llibavresample?-Llibavutil?-Llibpostproc?-Llibswscale?-Llibswresample?-L$BUILD_DIR/lib
??-Qunused-arguments
??#?這一行加入?-lpostproc?和?-lx264,添加加入?x264?的編譯
??-o?wasm/dist/ffmpeg-core.js?fftools/ffmpeg_opt.c?fftools/ffmpeg_filter.c?fftools/ffmpeg_hw.c?fftools/cmdutils.c?fftools/ffmpeg.c
??-lavdevice?-lavfilter?-lavformat?-lavcodec?-lswresample?-lswscale?-lavutil?-lpostproc?-lm?-lx264?-pthread
??-O3???????????????????????????????????????????#?Optimize?code?with?performance?first
??-s?USE_SDL=2??????????????????????????????????#?use?SDL2
??-s?USE_PTHREADS=1?????????????????????????????#?enable?pthreads?support
??-s?PROXY_TO_PTHREAD=1?????????????????????????#?detach?main()?from?browser/UI?main?thread
??-s?INVOKE_RUN=0???????????????????????????????#?not?to?run?the?main()?in?the?beginning
??-s?EXPORTED_FUNCTIONS="[_main,?_proxy_main]"??#?export?main?and?proxy_main?funcs
??-s?EXTRA_EXPORTED_RUNTIME_METHODS="[FS,?cwrap,?setValue,?writeAsciiToMemory]"???#?export?preamble?funcs
??-s?INITIAL_MEMORY=268435456????????????????????#?268435456?bytes?=?268435456?MB
)
emcc?"${ARGS[@]}"
cd?-
然后運(yùn)行這個(gè)腳本,接收上一步編譯的對(duì)象文件,編譯成 WASM 和 JS 膠水代碼:
bash?FFmpeg/build-with-emcc.sh
實(shí)際使用 ffmpeg 轉(zhuǎn)碼
我們將創(chuàng)建一個(gè) Web 網(wǎng)頁(yè),然后提供一個(gè)上傳視頻文件的按鈕,以及播放上傳的視頻文件。盡管無(wú)法直接在 Web 端播放 avi 格式的視頻文件,但是我們可以通過(guò) ffmpeg 轉(zhuǎn)碼之后播放。
在 ffmpeg 目錄下的 wasm 文件夾下創(chuàng)建 index.html 文件,然后添加如下內(nèi)容:
<html>????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????
??<head>??????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????
????<style>???????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????
??????html,?body?{???????????????????????????????????????????????????????
????????margin:?0;???????????????????????????????????????????????????????
????????width:?100%;?????????????????????????????????????????????????????
????????height:?100%?????????????????????????????????????????????????????
??????}??????????????????????????????????????????????????????????????????
??????body?{??????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????
????????display:?flex;???????????????????????????????????????????????????
????????flex-direction:?column;
????????align-items:?center;?????????????????????????????????????????????
??????}???
????</style>??????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????
??</head>????????????????????????????????????????????????????????????????
??<body>?????????????????????????????????????????????????????????????????
????<h3>上傳視頻文件,然后轉(zhuǎn)碼到?mp4?(x264)?進(jìn)行播放!</h3>
????<video?id="output-video"?controls></video><br/>?
????<input?type="file"?id="uploader">???????????????????
????<p?id="message">ffmpeg?腳本需要等待?5S?左右加載完成</p>
????<script?type="text/javascript">???????????????????????????????????????????????????????????????????????????????????????????????????????????????
??????const?readFromBlobOrFile?=?(blob)?=>?(
????????new?Promise((resolve,?reject)?=>?{
??????????const?fileReader?=?new?FileReader();
??????????fileReader.onload?=?()?=>?{
????????????resolve(fileReader.result);
??????????};
??????????fileReader.onerror?=?({?target:?{?error:?{?code?}?}?})?=>?{
????????????reject(Error(`File?could?not?be?read!?Code=${code}`));
??????????};
??????????fileReader.readAsArrayBuffer(blob);
????????})
??????);
??????
??????const?message?=?document.getElementById('message');
??????const?transcode?=?async?({?target:?{?files?}?})?=>?{
????????const?{?name?}?=?files[0];
????????message.innerHTML?=?'將文件寫入到?Emscripten?文件系統(tǒng)';
????????const?data?=?await?readFromBlobOrFile(files[0]);??????????????????????????????????????????????????????????????????????????????????????????
????????Module.FS.writeFile(name,?new?Uint8Array(data));??????????????????????????????????????????????????????????????????????????????????????????
????????const?ffmpeg?=?Module.cwrap('proxy_main',?'number',?['number',?'number']);
????????const?args?=?['ffmpeg',?'-hide_banner',?'-nostdin',?'-report',?'-i',?name,?'out.mp4'];
????????
????????const?argsPtr?=?Module._malloc(args.length?*?Uint32Array.BYTES_PER_ELEMENT);
????????args.forEach((s,?idx)?=>?{???????????????????????????????????????
??????????const?buf?=?Module._malloc(s.length?+?1);??????????????????????
??????????Module.writeAsciiToMemory(s,?buf);??????????????????????????????????????????????????????????????????????????????????????????????????????
??????????Module.setValue(argsPtr?+?(Uint32Array.BYTES_PER_ELEMENT?*?idx),?buf,?'i32');
????????});???????????????????
?????????
????????message.innerHTML?=?'開始轉(zhuǎn)碼';????????????????????????
????????ffmpeg(args.length,?argsPtr);
???????????????????????????????????????????????????????????
????????const?timer?=?setInterval(()?=>?{???????????????
??????????const?logFileName?=?Module.FS.readdir('.').find(name?=>?name.endsWith('.log'));
??????????if?(typeof?logFileName?!==?'undefined')?{???????????????????????????????????????????????????????????????????????????????????????????????
????????????const?log?=?String.fromCharCode.apply(null,?Module.FS.readFile(logFileName));
????????????if?(log.includes("frames?successfully?decoded"))?{
??????????????clearInterval(timer);??????????????????????????????????????
??????????????message.innerHTML?=?'完成轉(zhuǎn)碼';
??????????????const?out?=?Module.FS.readFile('out.mp4');
??????????????const?video?=?document.getElementById('output-video');
??????????????video.src?=?URL.createObjectURL(new?Blob([out.buffer],?{?type:?'video/mp4'?}));
????????????}????????????????????????????????????????????????????????????
??????????}?
????????},?500);?????????????????????????????????????????????????????????
??????};??
??????document.getElementById('uploader').addEventListener('change',?transcode);
????</script>????????????????????????????????????????????????????????????
????<script?type="text/javascript"?src="./dist/ffmpeg-core.js"></script>
??</body>?????????????????????????
</html>???????????
打開上述網(wǎng)頁(yè)運(yùn)行,我們可以看到如下效果:
恭喜你!成功編譯 ffmpeg 并在 Web 端使用。
如何調(diào)試 WebAssembly 代碼?WebAssembly 的原始調(diào)試方式
Chrome 開發(fā)者工具目前已經(jīng)支持 WebAssembly 的調(diào)試,雖然存在一些限制,但是針對(duì) WebAssembly 的文本格式的文件能進(jìn)行單個(gè)指令的分析以及查看原始的堆棧追蹤,具體見如下圖:

上述的方法對(duì)于一些無(wú)其他依賴函數(shù)的 WebAssembly 模塊來(lái)說(shuō)可以很好的運(yùn)行,因?yàn)檫@些模塊只涉及到很小的調(diào)試范圍。但是對(duì)于復(fù)雜的應(yīng)用來(lái)說(shuō),如 C/C++ 編寫的復(fù)雜應(yīng)用,一個(gè)模塊依賴其他很多模塊,且源代碼與編譯后的 WebAssembly 的文本格式的映射有較大的區(qū)別時(shí),上述的調(diào)試方式就不太直觀了,只能靠猜的方式才能理解其中的代碼運(yùn)行方式,且大多數(shù)人很難以看懂復(fù)雜的匯編代碼。
更加直觀的調(diào)試方式
現(xiàn)代的 JavaScript 項(xiàng)目在開發(fā)時(shí)通常也會(huì)存在編譯的過(guò)程,使用 ES6 進(jìn)行開發(fā),編譯到 ES5 及以下的版本進(jìn)行運(yùn)行,這個(gè)時(shí)候如果需要調(diào)試代碼,就涉及到 Source Map 的概念,source map 用于映射編譯后的對(duì)應(yīng)代碼在源代碼中的位置,source map 使得客戶端的代碼更具可讀性、更方便調(diào)試,但是又不會(huì)對(duì)性能造成很大的影響。
而 C/C++ 到 WebAssembly 代碼的編譯器 Emscripten 則支持在編譯時(shí),為代碼注入相關(guān)的調(diào)試信息,生成對(duì)應(yīng)的 source map,然后安裝 Chrome 團(tuán)隊(duì)編寫的 C/C++ Devtools Support[20] 瀏覽器擴(kuò)展,就可以使用 Chrome 開發(fā)者工具調(diào)試 C/C++ 代碼了。
這里的原理其實(shí)就是,Emscripten 在編譯時(shí),會(huì)生成一種 DWARF 格式的調(diào)試文件,這是一種被大多數(shù)編譯器使用的通用調(diào)試文件格式,而 C/C++ Devtools Support[21] 則會(huì)解析 DWARF 文件,為 Chrome Devtools 在調(diào)試時(shí)提供 source map 相關(guān)的信息,使得開發(fā)者可以在 89+ 版本以上的 Chrome Devtools 上調(diào)試 C/C++ 代碼。

調(diào)試簡(jiǎn)單的 C 應(yīng)用
因?yàn)?DWARF 格式的調(diào)試文件可以提供處理變量名、格式化類型打印消息、在源代碼中執(zhí)行表達(dá)式等等,現(xiàn)在就讓我們實(shí)際來(lái)編寫一個(gè)簡(jiǎn)單的 C 程序,然后編譯到 WebAssembly 并在瀏覽器中運(yùn)行,查看實(shí)際的調(diào)試效果吧。
首先讓我們進(jìn)入到之前創(chuàng)建的 WebAssembly 目錄下,激活 emcc 相關(guān)的命令,然后查看激活效果:
cd?emsdk?&&?source?emsdk_env.sh
emcc?--version?#?emcc?(Emscripten?gcc/clang-like?replacement)?1.39.18?(a3beeb0d6c9825bd1757d03677e817d819949a77)
接著在 WebAssembly 創(chuàng)建一個(gè) temp 文件夾,然后創(chuàng)建 temp.c 文件,填充如下內(nèi)容并保存:
#include?<stdlib.h>
void?assert_less(int?x,?int?y)?{
??if?(x?>=?y)?{
????abort();
??}
}
int?main()?{
??assert_less(10,?20);
??assert_less(30,?20);
}
上述代碼在執(zhí)行 asset_less 時(shí),如果遇到 x >= y 的情況會(huì)拋出異常,終止程序執(zhí)行。
在終端切換目錄到 temp 目錄下執(zhí)行 emcc 命令進(jìn)行編譯:
emcc?-g?temp.c?-o?temp.html
上述命令在普通的編譯形式上,加入了 -g 參數(shù),告訴 Emscripten 在編譯時(shí)為代碼注入 DWARF 調(diào)試信息。
現(xiàn)在可以開啟一個(gè) HTTP 服務(wù)器,可以使用 npx serve . ,然后訪問(wèn) localhost:5000/temp.html 查看運(yùn)行效果。

需要確保已經(jīng)安裝了 Chrome 擴(kuò)展:https://chrome.google.com/webstore/detail/cc++-devtools-support-dwa/pdcpmagijalfljmkmjngeonclgbbannb,以及 Chrome Devtools 升級(jí)到 89+ 版本。
為了查看調(diào)試效果,需要設(shè)置一些內(nèi)容。
- 打開 Chrome Devtools 里面的 WebAssembly 調(diào)試選項(xiàng)


設(shè)置完之后,在工具欄頂部會(huì)出現(xiàn)一個(gè) Reload 的藍(lán)色按鈕,需要重新加載配置,點(diǎn)擊一下就好。

- 設(shè)置調(diào)試選項(xiàng),在遇到異常的地方暫停

- 刷新瀏覽器,然后你會(huì)發(fā)現(xiàn)斷點(diǎn)停在了
temp.js,由 Emscripten 編譯生成的 JS 膠水代碼,然后順著調(diào)用棧去找,可以查看到temp.c并定位到拋出異常的位置:


可以看到,我們成功在 Chrome Devtools 里面查看了 C 代碼,并且代碼停在了 abort() 處,同時(shí)還可以類似我們調(diào)試 JS 時(shí)一樣,查看當(dāng)前 scope 下的值:

如上述可以查看 x 、y 值,將鼠標(biāo)浮動(dòng)到 x 上還可以顯示此時(shí)的值。
查看復(fù)雜類型值
實(shí)際上 Chrome Devtools 不僅可以查看原 C/C++ 代碼中一些變量的普通類型值,如數(shù)字、字符串,還可以查看更加復(fù)雜的結(jié)構(gòu),如結(jié)構(gòu)體、數(shù)組、類等內(nèi)容,我們拿另外一個(gè)例子來(lái)展現(xiàn)這個(gè)效果。
我們通過(guò)一個(gè)在 C++ 里面繪制 曼德博圖形 的例子來(lái)展示上述的效果,同樣在 WebAssembly 目錄下創(chuàng)建 mandelbrot 文件夾,然后添加 mandelbrot.cc 文件,并填入如下內(nèi)容:
#include?<SDL2/SDL.h>
#include?<complex>
int?main()?{
??//?初始化?SDL?
??int?width?=?600,?height?=?600;
??SDL_Init(SDL_INIT_VIDEO);
??SDL_Window*?window;
??SDL_Renderer*?renderer;
??SDL_CreateWindowAndRenderer(width,?height,?SDL_WINDOW_OPENGL,?&window,
??????????????????????????????&renderer);
??//?為畫板填充隨機(jī)的顏色
??enum?{?MAX_ITER_COUNT?=?256?};
??SDL_Color?palette[MAX_ITER_COUNT];
??srand(time(0));
??for?(int?i?=?0;?i?<?MAX_ITER_COUNT;?++i)?{
????palette[i]?=?{
????????.r?=?(uint8_t)rand(),
????????.g?=?(uint8_t)rand(),
????????.b?=?(uint8_t)rand(),
????????.a?=?255,
????};
??}
??
??
??//?計(jì)算?曼德博?集合并繪制?曼德博?圖形
??std::complex<double>?center(0.5,?0.5);
??double?scale?=?4.0;
??for?(int?y?=?0;?y?<?height;?y++)?{
????for?(int?x?=?0;?x?<?width;?x++)?{
??????std::complex<double>?point((double)x?/?width,?(double)y?/?height);
??????std::complex<double>?c?=?(point?-?center)?*?scale;
??????std::complex<double>?z(0,?0);
??????int?i?=?0;
??????for?(;?i?<?MAX_ITER_COUNT?-?1;?i++)?{
????????z?=?z?*?z?+?c;
????????if?(abs(z)?>?2.0)
??????????break;
??????}
??????SDL_Color?color?=?palette[i];
??????SDL_SetRenderDrawColor(renderer,?color.r,?color.g,?color.b,?color.a);
??????SDL_RenderDrawPoint(renderer,?x,?y);
????}
??}
??//?將我們?cè)?canvas?繪制的內(nèi)容渲染出來(lái)
??SDL_RenderPresent(renderer);
??//?SDL_Quit();
}
上述代碼差不多 50 行左右,但是引用了兩個(gè) C++ 標(biāo)準(zhǔn)庫(kù):SDL[22] 和 complex numbers[23] ,這使得我們的代碼變得有一點(diǎn)復(fù)雜了,我們接下來(lái)編譯上述代碼,來(lái)看看 Chrome Devtools 的調(diào)試效果如何。
通過(guò)在編譯時(shí)帶上 -g 標(biāo)簽,告訴 Emscripten 編譯器帶上調(diào)試信息,并尋求 Emscripten 在編譯時(shí)注入 SDL2 庫(kù)以及允許庫(kù)在運(yùn)行時(shí)可以使用任意內(nèi)存大小:
emcc?-g?mandelbrot.cc?-o?mandelbrot.html?\
?????-s?USE_SDL=2?\
?????-s?ALLOW_MEMORY_GROWTH=1
同樣使用 npx serve . 命令開啟一個(gè)本地的 Web 服務(wù)器,然后訪問(wèn) http://localhost:5000/mandelbrot.html 可以看到如下效果:

打開開發(fā)者工具,然后可以搜索到 mandelbrot.cc 文件,我們可以看到如下內(nèi)容:

我們可以在第一個(gè) for 循環(huán)里面的 palette 賦值語(yǔ)句哪一行打一個(gè)斷點(diǎn),然后重新刷新網(wǎng)頁(yè),我們發(fā)現(xiàn)執(zhí)行邏輯會(huì)暫停到我們的斷點(diǎn)處,通過(guò)查看右側(cè)的 Scope 面板,可以看到一些有意思的內(nèi)容。
使用 Scope 面板
我們可以看到復(fù)雜類型如 center 、palette ,還可以展開它們,查看復(fù)雜類型里面具體的值:

直接在程序中查看
同時(shí)將鼠標(biāo)移動(dòng)到 palette 等變量上面,同樣可以查看值的類型:

在控制臺(tái)中使用
同時(shí)在控制臺(tái)里面也可以通過(guò)輸入變量名獲取到值,依然可以查看復(fù)雜類型:

還可以對(duì)復(fù)雜類型進(jìn)行取值、計(jì)算相關(guān)的操作:

使用 watch 功能
我們也可以把使用調(diào)試面板里面的 watch 功能,添加 for 循環(huán)里面的 i 到 watch 列表,然后恢復(fù)程序執(zhí)行就可以看到 i 的變化:

更加復(fù)雜的步進(jìn)調(diào)試
我們同樣可以使用另外幾個(gè)調(diào)試工具:step over、step in、step out、step 等,如我們使用 step over,向后執(zhí)行兩步:

可以查看到當(dāng)前步的變量值,也可以在 Scope 面板中看到對(duì)應(yīng)的值。
針對(duì)非源碼編譯的第三方庫(kù)進(jìn)行調(diào)試
在之前我們只編譯了 mandelbrot.cc 文件,并在編譯時(shí)要求 Emscripten 為我們提供內(nèi)建的 SDL 相關(guān)的庫(kù),由于 SDL 庫(kù)并不是我們從源碼編譯而來(lái),所以不會(huì)帶上調(diào)試相關(guān)的信息,所以我們僅僅在 mandelbrot.cc 里面可以通過(guò)查看 C++ 代碼的形式來(lái)調(diào)試,而對(duì)于 SDL 相關(guān)的內(nèi)容則只能查看 WebAssembly 相關(guān)的代碼來(lái)進(jìn)行調(diào)試。
如我們?cè)?41 行,SDL_SetRenderDrawColor 調(diào)用處打上斷點(diǎn),并使用 step in 進(jìn)入到函數(shù)內(nèi)部:

會(huì)變成如下的形式:

我們又回到了原始的 WebAssembly 的調(diào)試形式,這也是難以避免的一種情況,因?yàn)槲覀冊(cè)陂_發(fā)過(guò)程中可能會(huì)遇到各種第三方庫(kù),但是我們并不能保證每個(gè)庫(kù)都能從源碼編譯而來(lái)且?guī)狭祟愃?DWARF 的調(diào)試信息,絕大部分情況下我們無(wú)法控制第三方庫(kù)的行為;而另外一種情況則是有時(shí)我們會(huì)在生產(chǎn)情況下遇到問(wèn)題,而生產(chǎn)環(huán)境也是沒(méi)有調(diào)試信息的。
上述情況暫時(shí)還沒(méi)有比較好的處理方法,但是開發(fā)者工具卻改進(jìn)了上述的調(diào)試體驗(yàn),將所有的代碼都打包成單一的 WebAssembly 文件,對(duì)應(yīng)到我們這次就是 mandelbrot.wasm 文件,這樣我們?cè)僖矡o(wú)需擔(dān)心其中的某段代碼到底來(lái)自哪個(gè)源文件。
新的命名生成策略
之前的調(diào)試面板里面,針對(duì) WebAssembly 只有一些數(shù)字索引,而對(duì)于函數(shù)則連名字都沒(méi)有,如果沒(méi)有必要的類型信息,那么很難追蹤到某個(gè)具體的值,因?yàn)橹羔槍⒁哉麛?shù)的形式展示出來(lái),但你不知道這些整數(shù)背后存儲(chǔ)著什么。
新的命名策略參考了其他反匯編工具的命名策略,使用了 WebAssembly 命名策略[24]部分的內(nèi)容、import/export 的路徑相關(guān)的內(nèi)容,可以看到我們現(xiàn)在的調(diào)試面板中針對(duì)函數(shù)可以展示函數(shù)名相關(guān)的信息:

即使遇到了程序錯(cuò)誤,基于語(yǔ)句的類型和索引也可以生成類似 $func123 這樣的名字,大大提高了棧追蹤和反匯編的體驗(yàn)。
查看內(nèi)存面板
如果想要調(diào)試此時(shí)程序占用的內(nèi)存相關(guān)的內(nèi)容,可以在 WebAssembly 的上下文下,查看 Scope 面板里的 Module.memories.$env.memory ,但是這只能看到一些獨(dú)立的字節(jié),無(wú)法了解到這些字節(jié)對(duì)應(yīng)到的其他數(shù)據(jù)格式,如 ASCII 格式。但是 Chrome 開發(fā)者工具還為我們提供了一些其他更加強(qiáng)大的內(nèi)存查看形式,當(dāng)我們右鍵點(diǎn)擊 env.memory 時(shí),可以選擇 Reveal in Memory Inspector panel:

或者點(diǎn)擊 env.memory 旁邊的小圖標(biāo):

可以打開內(nèi)存面板:

從內(nèi)存面板里面可以查看以十六進(jìn)制或 ASCII 的形式查看 WebAssembly 的內(nèi)存,導(dǎo)航到特定的內(nèi)存地址,將特定數(shù)據(jù)解析成各種不同的格式,如十六進(jìn)制 65 代表的 e 這個(gè) ASCII 字符。
對(duì) WebAssembly 代碼進(jìn)行性能分析
因?yàn)槲覀冊(cè)诰幾g時(shí)為代碼注入了很多調(diào)試信息,運(yùn)行的代碼是未經(jīng)優(yōu)化且冗長(zhǎng)的代碼,所以運(yùn)行時(shí)會(huì)很慢,所以如果為了評(píng)估程序運(yùn)行的性能,你不能使用 performance.now 或者 console.time 等 API,因?yàn)檫@些函數(shù)調(diào)用獲得的性能相關(guān)的數(shù)字通常不能反應(yīng)真實(shí)世界的效果。
所以如果需要對(duì)代碼進(jìn)行性能分析,你需要使用開發(fā)者工具提供的性能面板,性能面板里面會(huì)全速運(yùn)行代碼,并且提供不同函數(shù)執(zhí)行時(shí)花費(fèi)時(shí)間的明確斷點(diǎn)信息:

可以看到上述幾個(gè)比較典型的時(shí)間點(diǎn)如 161ms,或者 461ms 的 LCP 與 FCP ,這些都是能反應(yīng)真實(shí)世界下的性能指標(biāo)。
或者你可以在加載網(wǎng)頁(yè)時(shí)關(guān)閉控制臺(tái),這樣就不會(huì)涉及到調(diào)試信息等相關(guān)內(nèi)容的調(diào)用,可以確保比較真實(shí)的效果,等到頁(yè)面加載完成,然后再打開控制臺(tái)查看相關(guān)的指標(biāo)信息。
在不同的機(jī)器上進(jìn)行調(diào)試
當(dāng)在 Docker、虛擬機(jī)或者其他原創(chuàng)服務(wù)器上進(jìn)行構(gòu)建時(shí),你可能會(huì)遇到那種構(gòu)建時(shí)使用的源文件路徑和本地文件系統(tǒng)上的文件路徑不一致,這會(huì)導(dǎo)致開發(fā)者工具在運(yùn)行時(shí)可以在 Sources 面板里展示出有這個(gè)文件,但是無(wú)法加載文件內(nèi)容。
為了解決這個(gè)問(wèn)題,我們需要在之前安裝的 C/C++ Devtools Support[25] 配置里面設(shè)置路徑映射,點(diǎn)擊擴(kuò)展的 “選項(xiàng)”:

然后添加路徑映射,在 old/path 里填入之前的源文件構(gòu)建時(shí)的路徑,在 new/path 里填入現(xiàn)在存在本地文件系統(tǒng)上的文件路徑:

上述映射的功能和一些 C++ 的調(diào)試器如 GDB 的 set substitute-path 以及 LLDB 的 target.source-map 很像。這樣開發(fā)者工具在查找源文件時(shí),會(huì)查看是否在配置的路徑映射里有對(duì)應(yīng)的映射,如果源路徑無(wú)法加載文件,那么開發(fā)者工具會(huì)嘗試從映射路徑加載文件,否則會(huì)加載失敗。
調(diào)試優(yōu)化性構(gòu)建的代碼
如果你想調(diào)試一些在構(gòu)建時(shí)進(jìn)行優(yōu)化后的代碼,可能會(huì)獲得不太理想的調(diào)試體驗(yàn),因?yàn)檫M(jìn)行優(yōu)化構(gòu)建時(shí),函數(shù)內(nèi)聯(lián)在一起,可能還會(huì)對(duì)代碼進(jìn)行重排序或去除一部分無(wú)用的代碼,這些都可能會(huì)混淆調(diào)試者。
目前開發(fā)者工具除了對(duì)函數(shù)內(nèi)聯(lián)時(shí)不能搞很好的支持外,能夠支持絕大部分優(yōu)化后代碼的調(diào)試體驗(yàn),為了減少函數(shù)內(nèi)聯(lián)支持能力欠缺帶來(lái)的調(diào)試影響,建議在對(duì)代碼進(jìn)行編譯時(shí)加入 -fno-inline 標(biāo)志來(lái)取消優(yōu)化構(gòu)建時(shí)(通常是帶上 -O 參數(shù))對(duì)函數(shù)進(jìn)行內(nèi)聯(lián)處理的功能,未來(lái)開發(fā)者工具會(huì)修復(fù)這個(gè)問(wèn)題。所以針對(duì)之前提到的簡(jiǎn)單 C 程序的編譯腳本如下:
emcc?-g?temp.c?-o?temp.html?\
?????-O3?-fno-inline
將調(diào)試信息單獨(dú)存儲(chǔ)
調(diào)試信息包含代碼的詳細(xì)信息,定義的類型、變量、函數(shù)、函數(shù)作用域、以及文件位置等任何有利于調(diào)試器使用的信息,所以通常調(diào)試信息比源代碼還要大。
為了加速 WebAssembly 模塊的編譯和加載速度,你可以在編譯時(shí)將調(diào)試信息拆分成獨(dú)立的 WebAssembly 文件,然后單獨(dú)加載,為了實(shí)現(xiàn)拆分單獨(dú)文件,可以在編譯時(shí)加入 -gseparate-dwarf 操作:
emcc?-g?temp.c?-o?temp.html?\
?????-gseparate-dwarf=temp.debug.wasm
進(jìn)行上述操作之后,編譯之后的主應(yīng)用代碼只會(huì)存儲(chǔ)一個(gè) temp.debug.wasm 的文件名,然后在代碼加載時(shí),插件會(huì)定位到調(diào)試文件的位置并將其加載進(jìn)開發(fā)者工具。
如果我們想同時(shí)進(jìn)行優(yōu)化構(gòu)建,并將調(diào)試信息單獨(dú)拆分,并在之后需要調(diào)試時(shí),加載本地的調(diào)試文件進(jìn)行調(diào)試,在這種場(chǎng)景下,我們需要重載調(diào)試文件存儲(chǔ)的地址來(lái)幫助插件能夠找到這個(gè)文件,可以運(yùn)行如下命令來(lái)處理:
emcc?-g?temp.c?-o?temp.html?\
?????-O3?-fno-inline?\
?????-gseparate-dwarf=temp.debug.wasm?\
?????-s?SEPARATE_DWARF_URL=file://[temp.debug.wasm?在本地文件系統(tǒng)的存儲(chǔ)地址]
在瀏覽器中調(diào)試 ffmpeg 代碼
通過(guò)這篇文章我們深入了解了如何在瀏覽器中調(diào)試通過(guò) Emscripten 構(gòu)建而來(lái)的 C/C++ 代碼,上述講解了一個(gè)普通無(wú)依賴的例子以及一個(gè)依賴于 C++ 標(biāo)準(zhǔn)庫(kù) SDL 的例子,并且講解了現(xiàn)階段調(diào)試工具可以做的事情和限制,接下來(lái)我們就通過(guò)學(xué)到的知識(shí)來(lái)了解如何在瀏覽器中調(diào)試 ffmpeg 相關(guān)的代碼。
帶上調(diào)試信息的構(gòu)建
我們只需要修改在之前的文章中提到的構(gòu)建腳本 build-with-emcc.sh ,加入 -g 對(duì)應(yīng)的標(biāo)志:
ROOT=$PWD
BUILD_DIR=$ROOT/build
cd?ffmpeg-4.3.2-3
ARGS=(
??-g?#?在這里添加,告訴編譯器需要添加調(diào)試
??-I.?-I./fftools?-I$BUILD_DIR/include
??-Llibavcodec?-Llibavdevice?-Llibavfilter?-Llibavformat?-Llibavresample?-Llibavutil?-Llibpostproc?-Llibswscale?-Llibswresample?-L$BUILD_DIR/lib
??-Qunused-arguments
??-o?wasm/dist/ffmpeg-core.js?fftools/ffmpeg_opt.c?fftools/ffmpeg_filter.c?fftools/ffmpeg_hw.c?fftools/cmdutils.c?fftools/ffmpeg.c
??-lavdevice?-lavfilter?-lavformat?-lavcodec?-lswresample?-lswscale?-lavutil?-lpostproc?-lm?-lx264?-pthread
??-O3???????????????????????????????????????????#?Optimize?code?with?performance?first
??-s?USE_SDL=2??????????????????????????????????#?use?SDL2
??-s?USE_PTHREADS=1?????????????????????????????#?enable?pthreads?support
??-s?PROXY_TO_PTHREAD=1?????????????????????????#?detach?main()?from?browser/UI?main?thread
??-s?INVOKE_RUN=0???????????????????????????????#?not?to?run?the?main()?in?the?beginning
??-s?EXPORTED_FUNCTIONS="[_main,?_proxy_main]"??#?export?main?and?proxy_main?funcs
??-s?EXTRA_EXPORTED_RUNTIME_METHODS="[FS,?cwrap,?setValue,?writeAsciiToMemory]"???#?export?preamble?funcs
??-s?INITIAL_MEMORY=268435456????????????????????#?268435456?bytes?=?268435456?MB
)
emcc?"${ARGS[@]}"
cd?-
然后以此執(zhí)行其他操作,最后通過(guò) node server.js 運(yùn)行我們的腳本,然后打開 http://localhost:8080/ 查看效果如下:

可以看到,我們?cè)?Sources 面板里面可以搜索到構(gòu)建后的 ffmpeg.c 文件,我們可以在 4865 行,在循環(huán)操作 nb_output 時(shí)打一個(gè)斷點(diǎn):

然后在網(wǎng)頁(yè)中上傳一個(gè) avi 格式的視頻,接著程序會(huì)暫停到斷點(diǎn)位置:


可以發(fā)現(xiàn),我們依然可以像之前一樣在程序中鼠標(biāo)移動(dòng)上去查看變量值,以及在右側(cè)的 Scope 面板里查看變量值,以及可以在控制臺(tái)中查看變量值。
類似的,我們也可以進(jìn)行 step over、step in、step out、step 等復(fù)雜調(diào)試操作,或者 watch 某個(gè)變量值,或查看此時(shí)的內(nèi)存等。
可以看到通過(guò)這篇文章介紹的知識(shí),你可以在瀏覽器中對(duì)任意大小的 C/C++ 項(xiàng)目進(jìn)行調(diào)試,并且可以使用目前開發(fā)者工具提供的絕大部分功能。
關(guān)于 WebAssembly 的未來(lái)本文僅僅列舉了一些 WebAssembly 當(dāng)前的一些主要應(yīng)用場(chǎng)景,包含 WebAssembly 的高性能、輕量和跨平臺(tái),使得我們可以將 C/C++ 等語(yǔ)言運(yùn)行在 Web,也可以將桌面端應(yīng)用跑在 Web 容器。
但是這篇文章沒(méi)有涉及到的內(nèi)容有 WASI[26],一種將 WebAssembly 跑在任何系統(tǒng)上的標(biāo)準(zhǔn)化系統(tǒng)接口,當(dāng) WebAssembly 的性能逐漸增強(qiáng)時(shí),WASI 可以提供一種確實(shí)可行的方式,可以在任意平臺(tái)上運(yùn)行任意的代碼,就像 Docker 所做的一樣,但是不需要受限于操作系統(tǒng)。正如 Docker 的創(chuàng)始人所說(shuō):
“ 如果 WASM+WASI 在 2008 年就出現(xiàn)的話,那么就不需要?jiǎng)?chuàng)造 Docker 了,服務(wù)器上的 WASM 是計(jì)算的未來(lái),是我們期待已久的標(biāo)準(zhǔn)化的系統(tǒng)接口。
另一個(gè)有意思的內(nèi)容是 WASM 的客戶端開發(fā)框架如 yew[27],未來(lái)可能將像 React/Vue/Angular 一樣流行。
而 WASM 的包管理工具 WAPM[28],得益于 WASM 的跨平臺(tái)特性,可能會(huì)變成一種在不同語(yǔ)言的不同框架之間共享包的首選方式。
同時(shí) WebAssembly 也是由 W3C 主要負(fù)責(zé)開發(fā),各大廠商,包括 Microsoft、Google、Mozilla 等贊助和共同維護(hù)的一個(gè)項(xiàng)目,相信 WebAssembly 會(huì)有一個(gè)非常值得期待的未來(lái)。
Q & A答疑...
- 如何將復(fù)雜的 CMake 項(xiàng)目編譯到 WebAssembly?
- 在編譯復(fù)雜的 CMake 項(xiàng)目到 WebAssembly 時(shí)如何探索一套通用的最佳實(shí)踐?
- 如何和 CMake 項(xiàng)目結(jié)合起來(lái)進(jìn)行 Debug?
問(wèn)題:
- 編譯之后的代碼的體積
- https://www.ruanyifeng.com/blog/2017/09/asmjs_emscripten.html
- https://pspdfkit.com/blog/2017/webassembly-a-new-hope/
- https://hacks.mozilla.org/2017/02/what-makes-webassembly-fast/
- https://www.sitepoint.com/understanding-asm-js/
- http://www.cmake.org/download/
- https://developer.mozilla.org/en-US/docs/WebAssembly/existing_C_to_wasm
- https://research.mozilla.org/webassembly/
- https://itnext.io/build-ffmpeg-webassembly-version-ffmpeg-js-part-2-compile-with-emscripten-4c581e8c9a16?gi=e525b34f2c21
- https://dev.to/alfg/ffmpeg-webassembly-2cbl
- https://gist.github.com/rinthel/f4df3023245dd3e5a27218e8b3d79926
- https://github.com/Kagami/ffmpeg.js/
- https://qdmana.com/2021/04/20210401214625324n.html
- https://github.com/leandromoreira/ffmpeg-libav-tutorial
- http://ffmpeg.org/doxygen/4.1/examples.html
- https://github.com/alfg/ffmpeg-webassembly-example
- https://github.com/alfg/ffprobe-wasm
- https://gist.github.com/rinthel/f4df3023245dd3e5a27218e8b3d79926#file-ffmpeg-emscripten-build-sh
- https://emscripten.org/docs/compiling/Building-Projects.html#integrating-with-a-build-system
- https://itnext.io/build-ffmpeg-webassembly-version-ffmpeg-js-part-2-compile-with-emscripten-4c581e8c9a16
- https://github.com/mymindstorm/setup-emsdk
- https://github.com/emscripten-core/emsdk
- https://github.com/FFmpeg/FFmpeg/blob/n4.3.1/INSTALL.md
- https://yeasy.gitbook.io/docker_practice/container/run
- Debugging WebAssembly with modern tools - Chrome Developers[29]
- https://www.infoq.com/news/2021/01/chrome-extension-debug-wasm-c/
- https://developer.chrome.com/blog/wasm-debugging-2020/
- https://lucumr.pocoo.org/2020/11/30/how-to-wasm-dwarf/
- https://v8.dev/docs/wasm-compilation-pipeline
- [Debugging WebAssembly with Chrome DevTools | by Charuka Herath | Bits and Pieces (bitsrc.io)](https://blog.bitsrc.io/debugging-webassembly-with-chrome-devtools-99dbad485451 "Debugging WebAssembly with Chrome DevTools | by Charuka Herath | Bits and Pieces (bitsrc.io "Debugging WebAssembly with Chrome DevTools | by Charuka Herath | Bits and Pieces (bitsrc.io)")")
- Making Web Assembly Even Faster: Debugging Web Assembly Performance with AssemblyScript and a Gameboy Emulator | by Aaron Turner | Medium[30]
- https://zhuanlan.zhihu.com/p/68048524
- https://www.ruanyifeng.com/blog/2017/09/asmjs_emscripten.html
- https://www.jianshu.com/p/e4a75cb6f268
- https://www.cloudsavvyit.com/13696/why-webassembly-frameworks-are-the-future-of-the-web/
- https://mp.weixin.qq.com/s/LSIi2P6FKnJ0GTodaTUGKw
參考資料
[1]WebAssembly 入門:如何和有 C 項(xiàng)目結(jié)合使用: https://bytedance.feishu.cn/docs/doccnmiuQS1dKSWaMwUABoHkxez
[2]ArrayBuffer: https://es6.ruanyifeng.com/#docs/arraybuffer
[3]文本格式: https://webassembly.github.io/spec/core/text/index.html
[4]wabt: https://github.com/WebAssembly/wabt
[5]wabt: https://github.com/WebAssembly/wabt
[6]AssemblyScript: https://www.assemblyscript.org/
[7]WebAssembly 類型: https://www.assemblyscript.org/types.html#type-rules
[8]Binaryen: https://github.com/WebAssembly/binaryen
[9]Emscripten: https://github.com/emscripten-core/emscripten
[10]SDL: https://en.wikipedia.org/wiki/Simple_DirectMedia_Layer
[11]OpenGL: https://en.wikipedia.org/wiki/OpenGL
[12]OpenAL: https://en.wikipedia.org/wiki/OpenAL
[13]POSIX: https://en.wikipedia.org/wiki/POSIX
[14]Unreal Engine 4: https://blog.mozilla.org/blog/2014/03/12/mozilla-and-epic-preview-unreal-engine-4-running-in-firefox/
[15]Unity: https://blogs.unity3d.com/2018/08/15/webassembly-is-here/
[16]Github: https://github.com/webmproject/libwebp
[17]API 文檔: https://developers.google.com/speed/webp/docs/api
[18]WebP 的文檔: https://developers.google.com/speed/webp/docs/api#simple_encoding_api
[19]文件系統(tǒng) API: https://emscripten.org/docs/api_reference/Filesystem-API.html
[20]C/C++ Devtools Support: https://chrome.google.com/webstore/detail/cc++-devtools-support-dwa/pdcpmagijalfljmkmjngeonclgbbannb
[21]C/C++ Devtools Support: https://chrome.google.com/webstore/detail/cc++-devtools-support-dwa/pdcpmagijalfljmkmjngeonclgbbannb
[22]SDL: https://en.wikipedia.org/wiki/Simple_DirectMedia_Layer
[23]complex numbers: https://en.cppreference.com/w/cpp/numeric/complex
[24]WebAssembly 命名策略: https://webassembly.github.io/spec/core/appendix/custom.html#name-section
[25]C/C++ Devtools Support: https://chrome.google.com/webstore/detail/cc%20%20-devtools-support-dwa/pdcpmagijalfljmkmjngeonclgbbannb
[26]WASI: https://github.com/WebAssembly/WASI
[27]yew: https://github.com/yewstack/yew
[28]WAPM: https://wapm.io/
[29]Debugging WebAssembly with modern tools - Chrome Developers: https://developer.chrome.com/blog/wasm-debugging-2020/
[30]Making Web Assembly Even Faster: Debugging Web Assembly Performance with AssemblyScript and a Gameboy Emulator | by Aaron Turner | Medium: https://medium.com/@torch2424/making-web-assembly-even-faster-debugging-web-assembly-performance-with-assemblyscript-and-a-4d30cb6463f1
???謝謝支持
以上便是本次分享的全部內(nèi)容,希望對(duì)你有所幫助^_^
喜歡的話別忘了?分享、點(diǎn)贊、收藏?三連哦~。
歡迎關(guān)注公眾號(hào)?前端Sharing?收獲大廠一手好文章~


