【轉(zhuǎn)載】為什么說 WebAssembly 是 Web 的未來?

這篇文章打算講什么于游?

了解 WebAssembly 的前世今生,這一致力于讓 Web 更廣泛使用的偉大創(chuàng)造是如何在整個 Web/Node.js 的生命周期起作用的垫言。

在整篇文章的講解過程中贰剥,你可以了解到 WebAssembly 原生、AssemblyScript筷频、Emscripten 編譯器蚌成、以及如何在瀏覽器調(diào)試 WebAssembly 程序的。

最后還對 WebAssembly 的未來進行了展望凛捏,列舉了一些令人興奮的技術(shù)的發(fā)展方向担忧。

本文旨在對那些有興趣了解 WebAssembly,但是一直沒有時間深入探究它的邊界的同學(xué)提供一個快速入門且具有一定深度的分享坯癣,希望本文能為你在學(xué)習(xí) WebAssembly 的路上一個比較有意思的指引瓶盛。

同時本文還試圖回答之前分享文章的一些問題:WebAssembly 入門:如何和有 C 項目結(jié)合使用[1]

  • 如何將復(fù)雜的 CMake 項目編譯到 WebAssembly?
  • 在編譯復(fù)雜的 CMake 項目到 WebAssembly 時如何探索一套通用的最佳實踐示罗?
  • 如何和 CMake 項目結(jié)合起來進行 Debug惩猫?

為什么需要 WebAssembly ?

動態(tài)語言之踵

首先先來看一下 JS 代碼的執(zhí)行過程:

上述是 Microsoft Edge 之前的 ChakraCore 引擎結(jié)構(gòu)蚜点,目前 Microsoft Edge 的 JS 引擎已經(jīng)切換為 V8 轧房。

整體的流程就是:

  • 拿到了 JS 源代碼,交給 Parser绍绘,生成 AST
  • ByteCode Compiler 將 AST 編譯為字節(jié)碼(ByteCode)
  • ByteCode 進入翻譯器奶镶,翻譯器將字節(jié)碼一行一行翻譯(Interpreter)為機器碼(Machine Code),然后執(zhí)行

但其實我們平時寫的代碼有很多可以優(yōu)化的地方脯倒,如多次執(zhí)行同一個函數(shù)藐俺,那么可以將這個函數(shù)生成的 Machine Code 標記可優(yōu)化字管,然后打包送到 JIT Compiler(Just-In-Time),下次再執(zhí)行這個函數(shù)的時候,就不需要經(jīng)過 Parser-Compiler-Interpreter 這個過程蒿涎,可以直接執(zhí)行這份準備好的 Machine Code,大大提高的代碼的執(zhí)行效率坷衍。

但是上述的 JIT 優(yōu)化只能針對靜態(tài)類型的變量注盈,如我們要優(yōu)化的函數(shù),它只有兩個參數(shù)斋否,每個參數(shù)的類型是確定的梨水,而 JavaScript 卻是一門動態(tài)類型的語言,這也意味著茵臭,函數(shù)在執(zhí)行過程中疫诽,可能類型會動態(tài)變化,參數(shù)可能變成三個,第一個參數(shù)的類型可能從對象變?yōu)閿?shù)組奇徒,這就會導(dǎo)致 JIT 失效雏亚,需要重新進行 Parser-Compiler-Interpreter-Execuation,而 Parser-Compiler 這兩步是整個代碼執(zhí)行過程中最耗費時間的兩步摩钙,這也是為什么 JavaScript 語言背景下罢低,Web 無法執(zhí)行一些高性能應(yīng)用,如大型游戲胖笛、視頻剪輯等网持。

靜態(tài)語言優(yōu)化

通過上面的說明了解到,其實 JS 執(zhí)行慢的一個主要原因是因為其動態(tài)語言的特性长踊,導(dǎo)致 JIT 失效功舀,所以如果我們能夠為 JS 引入靜態(tài)特性,那么可以保持有效的 JIT之斯,勢必會加快 JS 的執(zhí)行速度日杈,這個時候 asm.js 出現(xiàn)了。

asm.js 只提供兩種數(shù)據(jù)類型:

  • 32 位帶符號整數(shù)
  • 64 位帶符號浮點數(shù)

其他類似如字符串佑刷、布爾值或?qū)ο蠖际且詳?shù)值的形式保存在內(nèi)存中莉擒,通過 TypedArray 調(diào)用。整數(shù)和浮點數(shù)表示如下:

ArrayBuffer對象瘫絮、TypedArray視圖和DataView 視圖是 JavaScript 操作二進制數(shù)據(jù)的一個接口涨冀,以數(shù)組的語法處理二進制數(shù)據(jù),統(tǒng)稱為二進制數(shù)組麦萤。參考 ArrayBuffer[2] 鹿鳖。


var x = a | 0;  // x 是32位整數(shù)

var y = +a;  // y 是64位浮點數(shù)

而函數(shù)的寫法如下:

function add(x, y) {

  x = x | 0;

  y = y | 0;

  return (x + y) | 0;

}

上述的函數(shù)參數(shù)及返回值都需要聲明類型,這里都是 32 位整數(shù)壮莹。

而且 asm.js 也不提供垃圾回收機制翅帜,內(nèi)存操作都是由開發(fā)者自己控制,通過 TypedArray 直接讀寫內(nèi)存:

var buffer = new ArrayBuffer(32768); // 申請 32 MB 內(nèi)存

var HEAP8 = new Int8Array(buffer); // 每次讀 1 個字節(jié)的視圖 HEAP8

function compiledCode(ptr) {

  HEAP[ptr] = 12;

  return HEAP[ptr + 4];

}

從上可見命满,asm.js 是一個嚴格的 JavaScript 子集要求變量的類型在運行時確定且不可改變涝滴,且去除了 JavaScript 擁有的垃圾回收機制,需要開發(fā)者手動管理內(nèi)存胶台。這樣 JS 引擎就可以基于 asm.js 的代碼進行大量的 JIT 優(yōu)化歼疮,據(jù)統(tǒng)計 asm.js 在瀏覽器里面的運行速度,大約是原生代碼(機器碼)的 50% 左右诈唬。

推陳出新

但是不管 asm.js 再怎么靜態(tài)化韩脏,干掉一些需要耗時的上層抽象(垃圾收集等),也還是屬于 JavaScript 的范疇铸磅,代碼執(zhí)行也需要 Parser-Compiler 這兩個過程赡矢,而這兩個過程也是代碼執(zhí)行中最耗時的杭朱。

為了極致的性能,Web 的前沿開發(fā)者們拋棄 JavaScript吹散,創(chuàng)造了一門可以直接和 Machine Code 打交道的匯編語言 WebAssembly痕檬,直接干掉 Parser-Compiler,同時 WebAssembly 是一門強類型的靜態(tài)語言送浊,能夠進行最大限度的 JIT 優(yōu)化,使得 WebAssembly 的速度能夠無限逼近 C/C++ 等原生代碼丘跌。

相當于下面的過程:

WebAssembly 初探

我們可以通過一張圖來直觀了解 WebAssembly 在 Web 中的位置:

WebAssembly(也稱為 WASM)袭景,是一種可在 Web 中運行的全新語言格式,同時兼具體積小闭树、性能高耸棒、可移植性強等特點,在底層上類似 Web 中的 JavaScript报辱,同時也是 W3C 承認的 Web 中的第 4 門語言与殃。

為什么說在底層上類似 JavaScript,主要有以下幾個理由:

  • 和 JavaScript 在同一個層次執(zhí)行:JS Engine碍现,如 Chrome 的 V8
  • 和 JavaScript 一樣可以操作各種 Web API

同時 WASM 也可以運行在 Node.js 或其他 WASM Runtime 中幅疼。

WebAssembly 文本格式

實際上 WASM 是一堆可以直接執(zhí)行二進制格式,但是為了易于在文本編輯器或開發(fā)者工具里面展示昼接,WASM 也設(shè)計了一種 “中間態(tài)” 的文本格式[3]爽篷,以 .wat.wast 為擴展命名,然后通過 wabt[4] 等工具慢睡,將文本格式下的 WASM 轉(zhuǎn)為二進制格式的可執(zhí)行代碼逐工,以 .wasm 為擴展的格式。

來看一段 WASM 文本格式下的模塊代碼:

(module

  (func $i (import "imports" "imported_func") (param i32))

  (func (export "exported_func")

    i32.const 42

    call $i

  )

)

上述代碼邏輯如下:

  • 首先定義了一個 WASM 模塊漂辐,然后從一個 imports JS 模塊導(dǎo)入了一個函數(shù) imported_func 泪喊,將其命名為 $i ,接收參數(shù) i32
  • 然后導(dǎo)出一個名為 exported_func 的函數(shù)髓涯,可以從 Web App袒啼,如 JS 中導(dǎo)入這個函數(shù)使用
  • 接著為參數(shù) i32 傳入 42,然后調(diào)用函數(shù) $i

我們通過 wabt 將上述文本格式轉(zhuǎn)為二進制代碼:

  • 將上述代碼復(fù)制到一個新建的复凳,名為 simple.wat 的文件中保存
  • 使用 wabt[5] 進行編譯轉(zhuǎn)換

當你安裝好 wabt 之后瘤泪,運行如下命令進行編譯:

wat2wasm simple.wat -o simple.wasm

雖然轉(zhuǎn)換成了二進制,但是無法在文本編輯器中查看其內(nèi)容育八,為了查看二進制的內(nèi)容对途,我們可以在編譯時加上 -v 選項,讓內(nèi)容在命令行輸出:

wat2wasm simple.wat -v

輸出結(jié)果如下:

可以看到髓棋,WebAssembly 其實是二進制格式的代碼实檀,即使其提供了稍為易讀的文本格式惶洲,也很難真正用于實際的編碼,更別提開發(fā)效率了膳犹。

將 WebAssembly 作為編程語言的一種嘗試

因為上述的二進制和文本格式都不適合編碼恬吕,所以不適合將 WASM 作為一門可正常開發(fā)的語言。

為了突破這個限制须床,AssemblyScript[6] 走到臺前铐料,AssemblyScript 是 TypeScript 的一種變體,為 JavaScript 添加了 WebAssembly 類型[7] 豺旬, 可以使用 Binaryen[8] 將其編譯成 WebAssembly钠惩。

WebAssembly 類型大致如下:

  • i32、u32族阅、i64篓跛、v128 等

  • 小整數(shù)類型:i8、u8 等

  • 變量整數(shù)類型:isize坦刀、usize 等

Binaryen 會前置將 AssemblyScript 靜態(tài)編譯成強類型的 WebAssembly 二進制愧沟,然后才會交給 JS 引擎去執(zhí)行,所以說雖然 AssemblyScript 帶來了一層抽象鲤遥,但是實際用于生產(chǎn)的代碼依然是 WebAssembly沐寺,保有 WebAssembly 的性能優(yōu)勢。AssemblyScript 被設(shè)計的和 TypeScript 非常相似盖奈,提供了一組內(nèi)建的函數(shù)可以直接操作 WebAssembly 以及編譯器的特性.

內(nèi)建函數(shù):

  • 靜態(tài)類型檢查:

  • function isInteger<T>(value?: T): bool

  • 實用函數(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)建一套標準庫芽丹。

標準庫:

  • Globals

  • Array

  • ArrayBuffer

  • DataView

  • Date

  • Error

  • Map

  • Math

  • Number

  • Set

  • String

  • Symbol

  • TypedArray

如一個典型的 Array 的使用如下:

var arr = new Array<string>(10)

// arr[0]; // 會出錯 ??

// 進行初始化

for (let i = 0; i < arr.length; ++i) {

  arr[i] = ""

}

arr[0]; // 可以正確工作 ??

可以看到 AssemblyScript 在為 JavaScript 添加類似 TypeScript 那樣的語法,然后在使用上需要保持和 C/C++ 等靜態(tài)強類型的要求卜朗,如不初始化拔第,進行內(nèi)存分配就訪問就會報錯。

還有一些擴展庫场钉,如 Node.js 的 process蚊俺、crypto 等,JS 的 console逛万,還有一些和內(nèi)存相關(guān)的 StaticArray泳猬、heap 等。

可以看到通過上面基礎(chǔ)的類型宇植、內(nèi)建庫得封、標準庫和擴展庫,AssemblyScript 基本上構(gòu)造了 JavaScript 所擁有的的全部特性指郁,同時 AssemblyScript 提供了類似 TypeScript 的語法忙上,在寫法上嚴格遵循強類型靜態(tài)語言的規(guī)范。

值得一提的是闲坎,因為當前 WebAssembly 的 ES 模塊規(guī)范依然在草案中疫粥,AssemblyScript 自行進行了模塊的實現(xiàn)茬斧,例如導(dǎo)出一個模塊:

// env.ts

export declare function doSomething(foo: i32): void { /* ... 函數(shù)體 */ }

導(dǎo)入一個模塊:

import { doSomething } from "./env";

一個大段代碼、使用類的例子:


  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 形式的語法项秉,遵循靜態(tài)強類型的規(guī)范進行高效編碼,同時又能夠便捷的操作 WebAssembly/編譯器相關(guān)的 API慷彤,代碼寫完之后娄蔼,通過 Binaryen 編譯器將其編譯為 WASM 二進制,然后獲取到 WASM 的執(zhí)行性能底哗。

得益于 AssemblyScript 兼具靈活性與性能贷屎,目前使用 AssemblyScript 構(gòu)建的應(yīng)用生態(tài)已經(jīng)初具繁榮,目前在區(qū)塊鏈艘虎、構(gòu)建工具过蹂、編輯器油航、模擬器、游戲确买、圖形編輯工具恬叹、庫候生、IoT、測試工具等方面都有大量使用 AssemblyScript 構(gòu)建的產(chǎn)物:https://www.assemblyscript.org/built-with-assemblyscript.html#games

上面是使用 AssemblyScript 構(gòu)建的一個五子棋游戲绽昼。

一種鬼才哲學(xué):將 C/C++ 代碼跑在瀏覽器

雖然 AssemblyScript 的出現(xiàn)極大的改善了 WebAssembly 在高效率編碼方面的缺陷唯鸭,但是作為一門新的編程語言,其最大的劣勢就是生態(tài)硅确、開發(fā)者與積累目溉。

WebAssembly 的設(shè)計者顯然在設(shè)計上同時考慮到了各種完善的情況,既然 WebAssembly 是一種二進制格式菱农,那么其就可以作為其他語言的編譯目標缭付,如果能夠構(gòu)建一種編譯器,能夠?qū)⒁延械难础⒊墒斓南菝ā⑶壹婢吆A康拈_發(fā)者和強大的生態(tài)的語言編譯到 WebAssembly 使用,那么相當于可以直接復(fù)用這個語言多年的積累的妖,并用它們來完善 WebAssembly 生態(tài)绣檬,將它們運行在 Web、Node.js 中嫂粟。

幸運的是娇未,針對 C/C++ 已經(jīng)有 Emscripten[9] 這樣優(yōu)秀的編譯器存在了。

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

即將 C/C++ 的代碼(或者 Rust/Go 等)編譯成 WASM星虹,然后通過 JS 膠水代碼將 WASM 跑在瀏覽器中(或 Node.js)的 runtime忘蟹,如 ffmpeg 這個使用 C 編寫音視頻轉(zhuǎn)碼工具飒房,通過 Emscripten 編譯器編譯到 Web 中使用,可直接在瀏覽器前端轉(zhuǎn)碼音視頻媚值。

上述的 JS “Gule” 代碼是必須的狠毯,因為如果需要將 C/C++ 編譯到 WASM,還能在瀏覽器中執(zhí)行褥芒,就得實現(xiàn)映射到 C/C++ 相關(guān)操作的 Web API嚼松,這樣才能保證執(zhí)行有效,這些膠水代碼目前包含一些比較流行的 C/C++ 庫锰扶,如 SDL[10]献酗、OpenGL[11]、OpenAL[12]坷牛、以及 POSIX[13] 的一部分 API罕偎。

目前使用 WebAssembly 最大的場景也是這種將 C/C++ 模塊編譯到 WASM 的方式,比較有名的例子有 Unreal Engine 4[14]京闰、Unity[15] 之類的大型庫或應(yīng)用颜及。

WebAssembly 會取代 JavaScript 嗎?

答案是不會蹂楣。

根據(jù)上面的層層闡述俏站,實際上 WASM 的設(shè)計初衷就可以梳理為以下幾點:

  • 最大程度的復(fù)用現(xiàn)有的底層語言生態(tài),如 C/C++ 在游戲開發(fā)痊土、編譯器設(shè)計等方面的積淀
  • 在 Web肄扎、Node.js 或其他 WASM runtime 獲得近乎于原生的性能,也就是可以讓瀏覽器也能跑大型游戲赁酝、圖像剪輯等應(yīng)用
  • 還有最大程度的兼容 Web犯祠、保證安全
  • 同時在開發(fā)上(如果需要開發(fā))易于讀寫和可調(diào)試,這一點 AssemblyScript 走得更遠

所以從初衷出發(fā)酌呆,WebAssembly 的作用更適合下面這張圖:

WASM 橋接各種系統(tǒng)編程語言的生態(tài)雷则,進一步補齊了 Web 開發(fā)生態(tài)之外,還為 JS 提供性能的補充肪笋,正是 Web 發(fā)展至今所缺失的重要的一塊版圖月劈。

Rust Web Framework:https://github.com/yewstack/yew

深入探索 Emscripten

地址:https://github.com/emscripten-core/emscripten

下面所有的 demo 都可以在倉庫:https://code.byted.org/huangwei.fps/webassembly-demos/tree/master 找到

Star:21.4K

維護:活躍

Emscripten 是一個開源的,跨平臺的藤乙,用于將 C/C++ 編譯為 WebAssembly 的編譯器工具鏈猜揪,由 LLVM、Binaryen坛梁、Closure Compiler 和其他工具等組成而姐。

Emscripten 的核心工具為 Emscripten Compiler Frontend(emcc),emcc 是用于替代一些原生的編譯器如 gcc 或 clang划咐,對 C/C++ 代碼進行編譯拴念。

實際上為了能讓幾乎所有的可移植的 C/C++ 代碼庫能夠編譯為 WebAssembly钧萍,并在 Web 或 Node.js 執(zhí)行,Emscripten Runtime 其實還提供了兼容 C/C++ 標準庫政鼠、相關(guān) API 到 Web/Node.js API 的映射风瘦,這份映射存在于編譯之后的 JS 膠水代碼中。

再看下面這張圖公般,紅色部分為 Emscripten 編譯后的產(chǎn)物万搔,綠色部分為 Emscripten 為保證 C/C++ 代碼能夠運行的一些 runtime 支持:

簡單體驗一下 “Hello World”

值得一提的是,WebAssembly 相關(guān)工具鏈的安裝幾乎都是以源碼的形式提供官帘,這可能和 C/C++ 生態(tài)的習(xí)慣不無關(guān)系瞬雹。

為了完成簡單的 C/C++ 程序運行在 Web,我們首先需要安裝 Emscripten 的 SDK:

# Clone 代碼倉庫

git clone https: // github . com / emscripten-core / emsdk . git

# 進入倉庫

cd emsdk

# 獲取最新代碼刽虹,如果是新 clone 的這一步可以不需要

git pull

# 安裝 SDK 工具酗捌,我們安裝 1.39.18,方便測試

./emsdk install 1.39.18

# 激活 SDK

./emsdk activate 1.39.18

# 將相應(yīng)的環(huán)境變量加入到系統(tǒng) PATH

source ./emsdk_env.sh

# 運行命令測試是否安裝成功

emcc -v #

如果安裝成功涌哲,上述的命令運行之后會輸出如下結(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

讓我們準備初始代碼:

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 來編譯這段 C 代碼胖缤,在命令行切換到 webassembly/hello_world 目錄,運行:

emcc main.c
上述命令會輸出兩個文件:a.out.jsa.out.wasm 膛虫,后者為編譯之后的 wasm 代碼,前者為 JS 膠水代碼钓猬,提供了 WASM 運行的 runtime稍刀。

可以使用 Node.js 進行快速測試:

node a.out.js

會輸出 "hello, world!" ,我們成功將 C/C++ 代碼運行在了 Node.js 環(huán)境敞曹。

接下來我們嘗試一下將代碼運行在 Web 環(huán)境账月,修改編譯代碼如下:

emcc main.c -o main.html

上述命令會生成三個文件:

  • main.js 膠水代碼
  • main.wasm WASM 代碼
  • main.html 加載膠水代碼,執(zhí)行 WASM 的一些邏輯

Emscripten 生成代碼有一定的規(guī)則澳迫,具體可以參考:https://emscripten.org/docs/compiling/Building-Projects.html#emscripten-linker-output-files

如果要在瀏覽器打開這個 HTML局齿,需要在本地起一個服務(wù)器,因為單純的打開通過 file:// 協(xié)議訪問時橄登,主流瀏覽器不支持 XHR 請求抓歼,只有在 HTTP 服務(wù)器下,才能進行 XHR 請求拢锹,所以我們運行如下命令來打開網(wǎng)站:
npx serve .

打開網(wǎng)頁谣妻,訪問 localhost:3000/main.html,可以看到如下結(jié)果:

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

嘗試在 JS 中調(diào)用 C/C++ 函數(shù)

上一小節(jié)我們初步體驗了一下如何在 Web 和 Node.js 中運行 C 程序卒稳,但其實如果我們想要讓復(fù)雜的 C/C++ 應(yīng)用蹋半,如 Unity 運行在 Web,那我們還有很長的路要走充坑,其中一條减江,就是能夠在 JS 中操作 C/C++ 函數(shù)染突。

讓我們在目錄下新建 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 默認編譯的代碼只會調(diào)用 main 函數(shù)辈灼,其他的代碼會作為 “死代碼” 在編譯時被刪掉份企,所以為了使用我們在上面定義的 myFunction ,我們需要在其定義之前加上 EMSCRIPTEN_KEEPALIVE 聲明茵休,確保在編譯時不會刪掉 myFunction 函數(shù)相關(guān)的代碼薪棒。

我們需要導(dǎo)入 emscripten/emscripten.h 頭文件,才能使用 EMSCRIPTEN_KEEPALIVE 聲明榕莺。

同時我們還需要對編譯命令做一下改進如下:

emcc function.c -o function.html -s NO_EXIT_RUNTIME=1 -s "EXTRA_EXPORTED_RUNTIME_METHODS=['ccall']"

上述額外增加了兩個參數(shù):

  • -s NO_EXIT_RUNTIME=1 表示在 main 函數(shù)運行完之后俐芯,程序不退出,依然保持可執(zhí)行狀態(tài)钉鸯,方便后續(xù)可調(diào)用 myFunction 函數(shù)
  • -s "EXTRA_EXPORTED_RUNTIME_METHODS=['ccall']" 則表示導(dǎo)出一個運行時的函數(shù) ccall 吧史,這個函數(shù)可以在 JS 中調(diào)用 C 程序的函數(shù)

進行編譯之后,我們還需要修改生成的 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ù)類型贸营,默認是數(shù)組

            null // 函數(shù)需要傳入的參數(shù),默認是數(shù)組

          );

        });

    </script>

</html>

可以看到我們增加了一個 Button岩睁,然后增加了一段腳本钞脂,為這個 Button 注冊了 click 事件,在回調(diào)函數(shù)里捕儒,我們調(diào)用了 myFunction 函數(shù)冰啃。

在命令行中運行 npx serve . 打開瀏覽器訪問 http://localhost:3000/function.html,查看結(jié)果如下:

只執(zhí)行 main 函數(shù):

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

可以看到首先進行 alert 彈框展示刘莹,然后打開控制臺阎毅,可以看到 myFunction 的調(diào)用結(jié)果,打印 "MyFunction Called" 点弯。

初嘗 Emscripten 文件系統(tǒng)

我們可以在 C/C++ 程序中使用 libc stdio API 如 fopen 扇调、fclose 來訪問你文件系統(tǒng),但是 JS 是運行在瀏覽器提供的沙盒環(huán)境里抢肛,無法直接訪問到本地文件系統(tǒng)狼钮。所以為了兼容 C/C++ 程序訪問文件系統(tǒng),編譯為 WASM 之后依然能夠正常運行捡絮,Emscripten 會在其 JS 膠水代碼里面模擬一個文件系統(tǒng)燃领,并提供和 libc stdio 一致的 API。

讓我們重新創(chuàng)建一個名為 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 訪問 file.txt 猛蔽,然后一行一行的讀取文件內(nèi)容,如果程序執(zhí)行過程中有任何的出錯,就會打印錯誤曼库。

我們在目錄下新建 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.

==

如果我們要編譯這個程序,并確保能夠在 JS 中正常運行毁枯,還需要在編譯時加上 preload 參數(shù)慈缔,提前將文件內(nèi)容加載進 Emscripten runtime,因為在 C/C++ 等程序上訪問文件都是同步操作种玛,而 JS 是基于事件模型的異步操作藐鹤,且在 Web 中只能通過 XHR 的形式去訪問文件(Web Worker、Node.js 可同步訪問文件)赂韵,所以需要提前將文件加載好娱节,確保在代碼編譯之前,文件已經(jīng)準備好了祭示,這樣 C/C++ 代碼可以直接訪問到文件肄满。

運行如下命令進行代碼編譯:

emcc file.c -o file.html -s EXIT_RUNTIME=1 --preload-file file.txt

上述添加了 -s EXIT_RUNTIME=1 ,依然是確保 main 邏輯執(zhí)行完之后质涛,程序不會退出抱完。

然后運行我們的本地服務(wù)器泌绣,訪問 http://localhost:3000/file.html眷蜈,可以查看結(jié)果:

嘗試編譯已存在的 WebP 模塊并使用

通過上面三個例子宦搬,我們已經(jīng)了解了基礎(chǔ)的 C/C++ 如打印、函數(shù)調(diào)用毡代、文件系統(tǒng)相關(guān)的內(nèi)容如何編譯為 WASM阅羹,并在 JS 中運行,這里的 JS 特指 Web 和 Node.js 環(huán)境月趟,通過上面的例子基本上絕大部分自己寫的 C/C++ 程序都可以自行編譯到 WASM 使用了灯蝴。

而之前我們也提到過恢口,其實當前 WebAssembly 最大的一個應(yīng)用場景孝宗,就是最大程度的復(fù)用當前已有語言的生態(tài),如 C/C++ 生態(tài)的庫耕肩,這些庫通常都依賴 C 標準庫因妇、操作系統(tǒng)、文件系統(tǒng)或其他依賴猿诸,而 Emscripten 最厲害的一點就在于能夠兼容絕大部分這些依賴的特性婚被,盡管還存在一些限制,但是已經(jīng)足夠可用梳虽。

簡單的測試

接下來我們來了解一下如何將一個現(xiàn)存的址芯、比較復(fù)雜且廣泛使用的 C 模塊:libwebp,將其編譯到 WASM 并允許到 Web。libwebp 的源碼是用 C 實現(xiàn)的谷炸,能夠在 Github[16] 上找到它北专,同時可以了解到它的一些 API 文檔[17]

首先準備代碼旬陡,在我們的目錄下運行如下命令:

git clone https://github.com/webmproject/libwebp

為了快速測試是否正確的接入了 libwebp 進行使用拓颓,我們可以編寫一個簡單的 C 函數(shù),然后在里面調(diào)用 libwebp 獲取版本的函數(shù)描孟,測試版本是否可以正確獲取驶睦。

我們在目錄下創(chuàng)建 webp.c 文件,添加如下內(nèi)容:

#include "emscripten.h"

#include "src/webp/encode.h"

EMSCRIPTEN_KEEPALIVE int version() {

  return WebPGetEncoderVersion();

}

上述的 WebPGetEncoderVersion 就是 libwebp 里面獲取當前版本的函數(shù)匿醒,而我們是通過導(dǎo)入 src/webp/encode.h 頭文件來獲取這個函數(shù)的场航,為了讓編譯器在編譯時能夠找到這個頭文件,我們需要在編譯的時候?qū)?libwebp 庫的頭文件地址告訴編譯器青抛,并將編譯器需要的所有 libwebp 庫下的 C 文件傳給編譯器旗闽。

讓我們運行如下編譯命令:

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 庫的頭文件地址告訴編譯器
  • libwebp/src/{dec,dsp,demux,enc,mux,utils}/*.c 將編譯器所需的 C 文件傳給編譯器,這里將 dec,dsp,demux,enc,mux,utils 等目錄下的所有 C 文件都傳遞給了編譯器蜜另,避免了一個個列出所需文件的繁瑣适室,然后讓編譯器去自動識別那些沒有使用的文件,并將其過濾掉
  • webp.c 是我們編寫的 C 函數(shù)举瑰,用于調(diào)用 WebPGetEncoderVersion 獲取庫版本
  • -O3 代表在編譯時進行等級為 3 的優(yōu)化捣辆,包含內(nèi)聯(lián)函數(shù)、去除無用代碼此迅、對代碼進行各種壓縮優(yōu)化等
  • -s WASM=1 其實是默認的汽畴,就是在編譯時輸出 xx.out.wasm ,這里之所以會設(shè)置這個選項主要是針對那些不支持 WASM 的 runtime耸序,可以設(shè)置 -s WASM=0 忍些,輸出等價的 JS 代碼替代 WASM
  • EXTRA_EXPORTED_RUNTIME_METHODS= '["cwrap"]' 則是輸出 runtime 的函數(shù) cwrap ,類似 ccall 可以在 JS 中調(diào)用 C 函數(shù)

上述的編譯輸出只有 a.out.jsa.out.wasm 坎怪,我們還需要建一份 HTML 文檔來使用輸出的腳本代碼罢坝,新建 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)的操作嘁酿,因為 WASM 相關(guān)的代碼從加載到可用是需要一段時間的,而 onRuntimeInitialized 的回調(diào)則是確保 WASM 相關(guān)的代碼已經(jīng)加載完成男应,達到可用狀態(tài)闹司。

接著我們可以運行 npx serve . ,然后訪問 http://localhost:3000/webp.html沐飘,查看結(jié)果:

可以看到控制臺打印了 66049 版本號游桩。

libwebp 通過十六進制的 0xabc 的 abc 來表示當前版本 a.b.c 牲迫,例如 v0.6.1,則會被編碼成十六進制 0x000601 借卧,對應(yīng)的十進制為 1537恩溅。而這里為十進制 66049,轉(zhuǎn)成 16 進制則為 0x010201 谓娃,表示當前版本為 v1.2.1脚乡。

在 JavaScript 中獲取圖片并放入 wasm 中運行

剛剛通過調(diào)用編碼器的 WebPGetEncoderVersion 方法來獲取版本號來證實了已經(jīng)成功編譯了 libwebp 庫到 wasm,然后可以在 JavaScript 使用它滨达,接下來我們將了解更加復(fù)雜的操作奶稠,如何使用 libwebp 的編碼 API 來轉(zhuǎn)換圖片格式。

libwebp 的 encoding API 需要接收一個關(guān)于 RGB捡遍、RGBA锌订、BGR 或 BGRA 的字節(jié)數(shù)組,幸運的是画株,Canvas API 有一個 CanvasRenderingContext2D.getImageData 方法辆飘,能夠返回一個 Uint8ClampedArray ,這個數(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,為了達成這個目的续挟,需要在先前的 webp.c 函數(shù)里面暴露額外的方法:

  • 一個為 wasm 里面的圖片分配內(nèi)存的方法
  • 一個釋放內(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 圖片一個像素包含 4 個字節(jié),所以代碼中需要添加 4 * sizeof(uint8_t) 诗祸,malloc 函數(shù)返回的指針指向所分配內(nèi)存的第一塊內(nèi)存單元地址跑芳,當這個指針返回給 JavaScript 使用時,會被當做一個簡單的數(shù)字處理直颅。當通過 cwrap 函數(shù)獲取暴露給 JavaScript 的對應(yīng) C 函數(shù)時博个,可以使用這個指針數(shù)字找到復(fù)制圖片數(shù)據(jù)的內(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);

    // ... 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_bufferdestroy_buffer 外功偿,還有很多用于編碼文件等方面的函數(shù)盆佣,我們將在后續(xù)講解,除此之外脖含,代碼首先加載了一份 image.jpg 的圖片罪塔,然后調(diào)用 C 函數(shù)為此圖片數(shù)據(jù)分配內(nèi)存投蝉,并相應(yīng)的拿到返回的指針傳給 WebAssembly 的 Module.HEAP8 养葵,在內(nèi)存開始位置 p,寫入圖片的數(shù)據(jù)瘩缆,最后會釋放分配的內(nèi)存关拒。

編碼圖片

現(xiàn)在圖片數(shù)據(jù)已經(jīng)加載進 wasm 的內(nèi)存中,可以調(diào)用 libwebp 的 encoder 方法來完成編碼過程了,通過查閱 WebP 的文檔[18]着绊,發(fā)現(xiàn)可以使用 WebPEncodeRGBA 函數(shù)來完成工作谐算。這個函數(shù)接收一個指向圖片數(shù)據(jù)的指針以及它的尺寸,以及每次需要跨越的 stride 步長归露,這里為 4 個字節(jié)(RGBA)洲脂,一個區(qū)間在 0-100 的可選的質(zhì)量參數(shù)。在編碼的過程中剧包,WebPEncodeRGBA 會分配一塊用于輸出數(shù)據(jù)的內(nèi)存恐锦,我們需要在編碼完成之后調(diào)用 WebPFree 來釋放這塊內(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)存的大小一铅。因為 C 函數(shù)無法使用數(shù)組作為返回值(除非我們需要進行動態(tài)內(nèi)存分配),所以我們使用一個全局靜態(tài)數(shù)組來獲取返回的結(jié)果堕油,這可能不是很規(guī)范的 C 代碼寫法潘飘,同時它要求 wasm 指針為 32 比特長,但是為了簡單起見我們可以暫時容忍這種做法掉缺。

現(xiàn)在 C 側(cè)的相關(guān)邏輯已經(jīng)編寫完畢卜录,可以在 JavaScript 側(cè)調(diào)用編碼函數(shù),獲取圖片數(shù)據(jù)的指針和圖片所占用的內(nèi)存大小眶明,將這份數(shù)據(jù)保存到 WASM 的緩沖中暴凑,然后釋放 wasm 在處理圖片時所分配的內(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>

在上述代碼中我們通過 loadImage 函數(shù)加載了一張本地的 image.jpg 圖片赘来,你需要事先準備一張圖片放置在 emcc 編譯器輸出的目錄下现喳,也就是我們的 HTML 文件目錄下使用。

注意:new Uint8Array(someBuffer) 將會在同樣的內(nèi)存塊上創(chuàng)建一個新視圖犬辰,而 new Uint8Array(someTypedArray) 只會復(fù)制 someTypedArray 的數(shù)據(jù)嗦篱,確保使用復(fù)制的數(shù)據(jù)進行操作,不會修改原內(nèi)存數(shù)據(jù)幌缝。

當你的圖片比較大時灸促,因為 wasm 不能自動擴充內(nèi)存,如果默認分配的內(nèi)存無法容納 inputoutput 圖片數(shù)據(jù)的內(nèi)存涵卵,你可能會遇到如下報錯:

但是我們例子中使用的圖片比較小浴栽,所以只需要單純的在編譯時加上一個過濾參數(shù) -s ALLOW_MEMORY_GROWTH=1 忽略這個報錯信息即可:

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` 

再次運行上述命令,得到添加了編碼函數(shù)的 wasm 代碼和對應(yīng)的 JavaScript 膠水代碼轿偎,這樣當我們打開 HTML 文件時典鸡,它已經(jīng)能夠?qū)⒁环?JPG 文件編碼成 WebP 的格式,為了進一步證實這個觀點坏晦,我們可以將圖片展示到 Web 界面上萝玷,通過修改 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)該可以看到如下界面:

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

通過上述的流程我們成功編譯了現(xiàn)有的 libwebp C 庫到 wasm 使用蜓斧,并將 JPG 圖片轉(zhuǎn)成了 WebP 格式并展示在 Web 界面上,通過 wasm 來處理計算密集型的轉(zhuǎn)碼操作可以大大提高網(wǎng)頁的性能睁冬,這也是 WebAssembly 帶來的主要優(yōu)勢之一挎春。

如何編譯 FFmpeg 到 WebAssembly?

好家伙豆拨,剛剛教會 1+1搂蜓,就開始解二次方程了。??

在上個例子中我們成功編譯了已經(jīng)存在的 C 模塊到 WebAssembly辽装,但是有很多更大型的項目依賴于 C 標準庫帮碰、操作系統(tǒng)、文件系統(tǒng)或其他依賴拾积,這些項目在編譯前依賴 autoconfig/automake 等庫來生成系統(tǒng)特定的代碼殉挽。

所以你經(jīng)常會看到一些庫在使用之前,需要經(jīng)過如下的步驟:

./configure # 處理前置依賴

make # 使用 gcc 等進行編譯構(gòu)建拓巧,生成對象文件

而 Emscripten 提供了 emconfigureemmake 來封裝這些命令斯碌,并注入合適的參數(shù)來抹平那些有前置依賴的項目,如果使用 emcc 來處理這些有大量前置依賴的項目肛度,命令會變成如下操作:

emmconfigure ./configure # 將配置中的默認編譯器傻唾,如 gcc 替換成 emcc 編譯器

emmake make # emmake make -j4 調(diào)起多核編譯,生成 wasm 對象文件承耿,而非傳統(tǒng)的 C 對象文件

emcc xxx.o # 將 make 生成的對象文件編譯成 wasm 文件 + JS 膠水代碼

接下來我們通過實際編譯 ffmpeg 來講解如何處理這種依賴 autoconfig/automake 等庫來生成特定的代碼冠骄。

經(jīng)過實踐發(fā)現(xiàn) ffmpeg 的編譯依賴于特定的 ffmpeg 版本、Emscripten 版本加袋、操作系統(tǒng)環(huán)境等凛辣,所以以下的 ffmpeg 的編譯都是限制在特定的條件下進行的,主要是為之后通用的 ffmpeg 的編譯提供一種思路和調(diào)試方法职烧。

準備目錄

這一次我們創(chuàng)建 WebAssembly 目錄嫂便,然后在這個目錄下放置 ffmpeg 源碼抑月、以及后續(xù)要用到的 x264 解碼器的相關(guān)代碼:

mkdir WebAssembly

# Clone 代碼倉庫

git clone https: // github . com / emscripten-core / emsdk . git

# 進入倉庫

cd emsdk

# 獲取最新代碼,如果是新 clone 的這一步可以不需要

git pull

編譯步驟

使用 Emscripten 編譯大部分復(fù)雜的 C/C++ 庫時效床,主要需要三個步驟:

  1. 使用 emconfigure 運行項目的 configure 文件將 C/C++ 代碼編譯器從 gcc/g++ 換成 emcc/em++
  2. 通過 emmake make 來構(gòu)建 C/C++ 項目屎慢,生成 wasm 對象的 .o 文件
  3. 調(diào)用 emcc 接收編譯的對象文件 .o 文件秉氧,然后輸出最終的 WASM 和 JS 膠水代碼

安裝特定依賴

注意:這一步我們在講解 Emscripten 的開頭就已經(jīng)安裝了對應(yīng)的版本掖看,這里只是再強調(diào)一下版本捕发。

為了驗證 ffmpeg 的驗證,我們需要依賴特定的版本壹堰,下面詳細講解依賴的各種文件版本拭卿。

首先安裝 1.39.18 版本的 Emscripten 編譯器,進入之前我們 Clone 到本地的 emsdk 項目運行如下命令:

./emsdk install 1.39.18

./emsdk activate 1.39.18

source ./emsdk_env.sh

通過在命令行中輸入如下命令驗證是否切換成功:

emcc -v # 輸出 1.39.18

在 emsdk 同級下載分支為 n4.3.1 的 ffmpeg 代碼:

git clone --depth 1 --branch n4.3.1 https://github.com/FFmpeg/FFmpeg

使用 emconfigure 處理 configure 文件

通過如下腳本來處理 configure 文件:

export CFLAGS="-s USE_PTHREADS -O3"

export LDFLAGS="$CFLAGS -s INITIAL_MEMORY=33554432"

emconfigure ./configure \

  --target-os=none \ # 設(shè)置為 none 來去除特定操作系統(tǒng)的一些依賴

  --arch=x86_32 \ # 選中架構(gòu)為 x86_32                                                                                                                

  --enable-cross-compile \ # 處理跨平臺操作

  --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 表示在編譯時優(yōu)化代碼體積峻厚,一般可以從 30MB 壓縮到 15MB
  • INITIAL_MEMORY 設(shè)置為 33554432 (32MB),主要是 Emscripten 可能占用 19MB谆焊,所以設(shè)置更大的內(nèi)存容量來避免在編譯過程中可分配的內(nèi)存不足的問題
  • 實際使用 emconfigure 來配置 configure 文件惠桃,替換 gcc 編譯器為 emcc ,以及設(shè)置一些必要的操作來處理可能遇到的編譯 BUG辖试,最終生成用于編譯構(gòu)建的配置文件

使用 emmake make 來構(gòu)建依賴

通過上述步驟辜王,就處理好了配置文件,接下來需要通過 emmake 來構(gòu)建實際的依賴罐孝,通過在命令行中運行如下命令:

# 構(gòu)建最終的 ffmpeg.wasm 文件

emmake make -j4

通過上述的編譯呐馆,會生成如下四個文件:

  • ffmpeg

  • ffmpeg_g

  • ffmpeg_g.wasm

  • ffmpeg_g.worker.js

前兩個都是 JS 文件,第三個為 wasm 模塊莲兢,第四個是處理 worker 中運行相關(guān)邏輯的函數(shù)汹来,上述生成的文件的理想形式應(yīng)該為三個,為了達成這種自定義的編譯改艇,有必要自定義使用 emcc 命令來進行處理收班。

使用 emcc 進行編譯輸出

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ù)時不首先執(zhí)行 main 函數(shù)           

  -s EXPORTED_FUNCTIONS="[_main, _proxy_main]" \

  -s EXTRA_EXPORTED_RUNTIME_METHODS="[FS, cwrap, setValue, writeAsciiToMemory]" \

  -s INITIAL_MEMORY=33554432

上述的腳本主要有如下幾點改進:

  1. -s PROXY_TO_PTHREAD=1 在編譯時設(shè)置了 pthread 時摔桦,使得程序具備響應(yīng)式特效
  2. -o wasm/dist/ffmpeg-core.js 則將原 ffmpeg js 文件的輸出重命名為 ffmpeg-core.js ,對應(yīng)的輸出 ffmpeg-core.wasmffmpeg-core.worker.js
  3. -s EXPORTED_FUNCTIONS="[_main, _proxy_main]" 導(dǎo)出 ffmpeg 對應(yīng)的 C 文件里的 main 函數(shù)承疲,proxy_main 則是通過設(shè)置 PROXY_TO_PTHREAD代理 main 函數(shù)用于外部使用
  4. -s EXTRA_EXPORTED_RUNTIME_METHODS="[FS, cwrap, setValue, writeAsciiToMemory]" 則是導(dǎo)出一些 runtime 的輔助函數(shù)邻耕,用于導(dǎo)出 C 函數(shù)、處理文件系統(tǒng)燕鸽、指針的操作

通過上述編譯命令最終輸出下面三個文件:

  • ffmpeg-core.js

  • ffmpeg-core.wasm

  • ffmpeg-core.worker.js

使用編譯完成的 ffmpeg wasm 模塊

wasm 目錄下創(chuàng)建 ffmpeg.js 文件赊豌,在其中寫入如下代碼:

onst Module = require('./dist/ffmpeg-core.js');

Module.onRuntimeInitialized = () => {

  const ffmpeg = Module.cwrap('proxy_main', 'number', ['number', 'number']);

};

然后通過如下命令運行上述代碼:

node --experimental-wasm-threads --experimental-wasm-bulk-memory ffmpeg.js

上述代碼解釋如下:

  • onRuntimeInitialized 是加載 WebAssembly 模塊完成之后執(zhí)行的邏輯,我們所有相關(guān)邏輯需要在這個函數(shù)中編寫

  • cwrap 則用于導(dǎo)出 C 文件中(fftools/ffmpeg.c )的 proxy_main 使用绵咱,函數(shù)的簽名為 int main(int argc, char **argv) 碘饼,其中 int 對應(yīng)到 JavaScript 就是 number ,argc 表示參數(shù)的個數(shù) 悲伶,而 char **argv 是 C 中的指針艾恼,表示實際參數(shù)的指針數(shù)組,也可以映射到 number

  • 接著處理 ffmpeg 的傳參兼容邏輯麸锉,對于命令行中運行 ffmpeg -hide_banner 钠绍,在我們代碼里通過函數(shù)調(diào)用需要 main(2, ["./ffmpeg", "-hide_banner"]) ,第一個參數(shù)很好解決花沉,那么我們?nèi)绾蝹鬟f一個字符串數(shù)組呢柳爽?這個問題可以分解為兩個部分:

  • 我們需要將 JavaScript 的字符串轉(zhuǎn)換成 C 中的字符數(shù)組

  • 我們需要將 JavaScript 中的數(shù)組轉(zhuǎn)換為 C 中的指針數(shù)組

第一部分很簡單媳握,因為 Emscripten 提供了一個輔助函數(shù) writeAsciiToMemory 來完成這一工作:

const str = "FFmpeg.wasm";

const buf = Module._malloc(str.length + 1); // 額外分配一個字節(jié)的空間來存放 0 表示字符串的結(jié)束

Module.writeAsciiToMemory(str, buf);

第二部分有一點困難,我們需要創(chuàng)建 C 中的 32 位整數(shù)的指針數(shù)組磷脯,可以借助 setValue 來幫助我們創(chuàng)建這個數(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');

});

將上述的代碼合并起來蛾找,我們就可以獲取一個能與 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);

};

然后通過同樣的命令運行程序:

node --experimental-wasm-threads --experimental-wasm-bulk-memory ffmpeg.js
上述運行的結(jié)果如下:

可以看到我們成功編譯并運行了 ffmpeg ??。

處理 Emscripten 文件系統(tǒng)

Emscripten 內(nèi)建了一個虛擬的文件系統(tǒng)來支持 C 中標準的文件讀取和寫入赵誓,所以我們需要將音頻文件傳給 ffmpeg.wasm 時先寫入到文件系統(tǒng)中打毛。

可以戳此查看更多關(guān)于文件系統(tǒng) API[19]

為了完成上述的任務(wù)俩功,只需要使用到 FS 模塊的兩個函數(shù) FS.writeFile()FS.readFile() 幻枉,對于從文件系統(tǒng)中讀取和寫入的所有數(shù)據(jù)都要求是 JavaScript 中的 Uint8Array 類型,所以在消費數(shù)據(jù)之前有必要約定數(shù)據(jù)類型诡蜓。

我們將通過 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)了蔓罚,接下來就是實際使用編譯好的 ffmepg 來進行視頻的轉(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);

};

在上述代碼中,我們添加了一個定時器脚粟,因為 ffmpeg 轉(zhuǎn)碼視頻的過程是異步的覆旱,所以我們需要不斷的去讀取 Emscripten 文件系統(tǒng)中是否有轉(zhuǎn)碼好的文件標志,當拿到文件標志且不為 undefined核无,我們就使用 Module.FS.readFile() 方法從 Emscripten 文件系統(tǒng)中讀取轉(zhuǎn)碼好的視頻文件扣唱,然后通過 fs.writeFileSync() 將視頻寫入到本地文件系統(tǒng)。最終我們會收到如下結(jié)果:

在瀏覽器中使用 ffmpeg 轉(zhuǎn)碼視頻并播放

在上一步中团南,我們成功在 Node 端使用了編譯好的 ffmpeg 完成從了 avi 格式到 mp4 格式的轉(zhuǎn)碼噪沙,接下來我們將在瀏覽器中使用 ffmpeg 轉(zhuǎn)碼視頻,并在瀏覽器中播放吐根。

之前我們編譯的 ffmpeg 雖然可以將 avi 格式轉(zhuǎn)碼到 mp4 正歼,但是這種通過默認編碼格式轉(zhuǎn)碼的 mp4 的文件無法直接在瀏覽器中播放,因為瀏覽器不支持這種編碼拷橘,所以我們需要使用 libx264 編碼器來將 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

然后進入 x264 的文件夾,可以創(chuàng)建一個 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 目錄下運行如下命令來構(gòu)建 x264:

bash x264-snapshot-20170226-2245-stable/build-x264.sh
安裝了 x264 編碼器之后萄唇,就可以在 ffmpeg 的編譯腳本中加入打開 x264 的開關(guān),這一次我們在 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 -

針對上述編譯腳本另萤,在 WebAssembly 目錄下運行如下命令來進行配置文件的處理以及文件編譯:

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 -

然后運行這個腳本,接收上一步編譯的對象文件四敞,編譯成 WASM 和 JS 膠水代碼:

bash FFmpeg/build-with-emcc.sh

實際使用 ffmpeg 轉(zhuǎn)碼

我們將創(chuàng)建一個 Web 網(wǎng)頁泛源,然后提供一個上傳視頻文件的按鈕,以及播放上傳的視頻文件忿危。盡管無法直接在 Web 端播放 avi 格式的視頻文件达箍,但是我們可以通過 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) 進行播放!</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)頁運行兜畸,我們可以看到如下效果:


恭喜你努释!成功編譯 ffmpeg 并在 Web 端使用。

如何調(diào)試 WebAssembly 代碼咬摇?

WebAssembly 的原始調(diào)試方式

Chrome 開發(fā)者工具目前已經(jīng)支持 WebAssembly 的調(diào)試伐蒂,雖然存在一些限制,但是針對 WebAssembly 的文本格式的文件能進行單個指令的分析以及查看原始的堆棧追蹤肛鹏,具體見如下圖:

上述的方法對于一些無其他依賴函數(shù)的 WebAssembly 模塊來說可以很好的運行逸邦,因為這些模塊只涉及到很小的調(diào)試范圍。但是對于復(fù)雜的應(yīng)用來說在扰,如 C/C++ 編寫的復(fù)雜應(yīng)用缕减,一個模塊依賴其他很多模塊,且源代碼與編譯后的 WebAssembly 的文本格式的映射有較大的區(qū)別時芒珠,上述的調(diào)試方式就不太直觀了桥狡,只能靠猜的方式才能理解其中的代碼運行方式,且大多數(shù)人很難以看懂復(fù)雜的匯編代碼皱卓。

更加直觀的調(diào)試方式

現(xiàn)代的 JavaScript 項目在開發(fā)時通常也會存在編譯的過程裹芝,使用 ES6 進行開發(fā),編譯到 ES5 及以下的版本進行運行娜汁,這個時候如果需要調(diào)試代碼嫂易,就涉及到 Source Map 的概念,source map 用于映射編譯后的對應(yīng)代碼在源代碼中的位置掐禁,source map 使得客戶端的代碼更具可讀性怜械、更方便調(diào)試,但是又不會對性能造成很大的影響傅事。

而 C/C++ 到 WebAssembly 代碼的編譯器 Emscripten 則支持在編譯時宫盔,為代碼注入相關(guān)的調(diào)試信息,生成對應(yīng)的 source map享完,然后安裝 Chrome 團隊編寫的 C/C++ Devtools Support[20] 瀏覽器擴展灼芭,就可以使用 Chrome 開發(fā)者工具調(diào)試 C/C++ 代碼了。

這里的原理其實就是般又,Emscripten 在編譯時彼绷,會生成一種 DWARF 格式的調(diào)試文件巍佑,這是一種被大多數(shù)編譯器使用的通用調(diào)試文件格式,而 C/C++ Devtools Support[21] 則會解析 DWARF 文件寄悯,為 Chrome Devtools 在調(diào)試時提供 source map 相關(guān)的信息萤衰,使得開發(fā)者可以在 89+ 版本以上的 Chrome Devtools 上調(diào)試 C/C++ 代碼。

調(diào)試簡單的 C 應(yīng)用

因為 DWARF 格式的調(diào)試文件可以提供處理變量名猜旬、格式化類型打印消息脆栋、在源代碼中執(zhí)行表達式等等,現(xiàn)在就讓我們實際來編寫一個簡單的 C 程序洒擦,然后編譯到 WebAssembly 并在瀏覽器中運行椿争,查看實際的調(diào)試效果吧。

首先讓我們進入到之前創(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)建一個 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 時椅邓,如果遇到 x >= y 的情況會拋出異常,終止程序執(zhí)行昧狮。

在終端切換目錄到 temp 目錄下執(zhí)行 emcc 命令進行編譯:

emcc -g temp.c -o temp.html

上述命令在普通的編譯形式上景馁,加入了 -g 參數(shù),告訴 Emscripten 在編譯時為代碼注入 DWARF 調(diào)試信息逗鸣。

現(xiàn)在可以開啟一個 HTTP 服務(wù)器合住,可以使用 npx serve . ,然后訪問 localhost:5000/temp.html 查看運行效果慕购。

需要確保已經(jīng)安裝了 Chrome 擴展:https://chrome.google.com/webstore/detail/cc++-devtools-support-dwa/pdcpmagijalfljmkmjngeonclgbbannb聊疲,以及 Chrome Devtools 升級到 89+ 版本。

為了查看調(diào)試效果沪悲,需要設(shè)置一些內(nèi)容获洲。

  1. 打開 Chrome Devtools 里面的 WebAssembly 調(diào)試選項

設(shè)置完之后,在工具欄頂部會出現(xiàn)一個 Reload 的藍色按鈕殿如,需要重新加載配置贡珊,點擊一下就好。

  1. 設(shè)置調(diào)試選項涉馁,在遇到異常的地方暫停
  1. 刷新瀏覽器门岔,然后你會發(fā)現(xiàn)斷點停在了 temp.js ,由 Emscripten 編譯生成的 JS 膠水代碼烤送,然后順著調(diào)用棧去找寒随,可以查看到 temp.c 并定位到拋出異常的位置:

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

如上述可以查看 xy 值讯泣,將鼠標浮動到 x 上還可以顯示此時的值纫普。

查看復(fù)雜類型值

實際上 Chrome Devtools 不僅可以查看原 C/C++ 代碼中一些變量的普通類型值,如數(shù)字好渠、字符串昨稼,還可以查看更加復(fù)雜的結(jié)構(gòu),如結(jié)構(gòu)體拳锚、數(shù)組假栓、類等內(nèi)容,我們拿另外一個例子來展現(xiàn)這個效果晌畅。

我們通過一個在 C++ 里面繪制 曼德博圖形 的例子來展示上述的效果但指,同樣在 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);

  // 為畫板填充隨機的顏色

  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,

    };

  }

  // 計算 曼德博 集合并繪制 曼德博 圖形

  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);

    }

  }

  // 將我們在 canvas 繪制的內(nèi)容渲染出來

  SDL_RenderPresent(renderer);

  // SDL_Quit();

}

上述代碼差不多 50 行左右,但是引用了兩個 C++ 標準庫:SDL[22] 和 complex numbers[23] 拦坠,這使得我們的代碼變得有一點復(fù)雜了连躏,我們接下來編譯上述代碼,來看看 Chrome Devtools 的調(diào)試效果如何贞滨。

通過在編譯時帶上 -g 標簽,告訴 Emscripten 編譯器帶上調(diào)試信息,并尋求 Emscripten 在編譯時注入 SDL2 庫以及允許庫在運行時可以使用任意內(nèi)存大杏畲小:

emcc -g mandelbrot.cc -o mandelbrot.html \

     -s USE_SDL=2 \

     -s ALLOW_MEMORY_GROWTH=1

同樣使用 npx serve . 命令開啟一個本地的 Web 服務(wù)器姐仅,然后訪問 http://localhost:5000/mandelbrot.html 可以看到如下效果:

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

我們可以在第一個 for 循環(huán)里面的 palette 賦值語句哪一行打一個斷點尚困,然后重新刷新網(wǎng)頁,我們發(fā)現(xiàn)執(zhí)行邏輯會暫停到我們的斷點處链蕊,通過查看右側(cè)的 Scope 面板事甜,可以看到一些有意思的內(nèi)容。

使用 Scope 面板

我們可以看到復(fù)雜類型如 center 滔韵、palette 逻谦,還可以展開它們,查看復(fù)雜類型里面具體的值:

直接在程序中查看

同時將鼠標移動到 palette 等變量上面陪蜻,同樣可以查看值的類型:

在控制臺中使用

同時在控制臺里面也可以通過輸入變量名獲取到值邦马,依然可以查看復(fù)雜類型:

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

使用 watch 功能

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

更加復(fù)雜的步進調(diào)試

我們同樣可以使用另外幾個調(diào)試工具:step over忱嘹、step in、step out耕渴、step 等拘悦,如我們使用 step over,向后執(zhí)行兩步:

可以查看到當前步的變量值橱脸,也可以在 Scope 面板中看到對應(yīng)的值础米。

針對非源碼編譯的第三方庫進行調(diào)試

在之前我們只編譯了 mandelbrot.cc 文件,并在編譯時要求 Emscripten 為我們提供內(nèi)建的 SDL 相關(guān)的庫添诉,由于 SDL 庫并不是我們從源碼編譯而來屁桑,所以不會帶上調(diào)試相關(guān)的信息,所以我們僅僅在 mandelbrot.cc 里面可以通過查看 C++ 代碼的形式來調(diào)試栏赴,而對于 SDL 相關(guān)的內(nèi)容則只能查看 WebAssembly 相關(guān)的代碼來進行調(diào)試蘑斧。

如我們在 41 行,SDL_SetRenderDrawColor 調(diào)用處打上斷點须眷,并使用 step in 進入到函數(shù)內(nèi)部:

會變成如下的形式:

我們又回到了原始的 WebAssembly 的調(diào)試形式竖瘾,這也是難以避免的一種情況,因為我們在開發(fā)過程中可能會遇到各種第三方庫花颗,但是我們并不能保證每個庫都能從源碼編譯而來且?guī)狭祟愃?DWARF 的調(diào)試信息捕传,絕大部分情況下我們無法控制第三方庫的行為;而另外一種情況則是有時我們會在生產(chǎn)情況下遇到問題扩劝,而生產(chǎn)環(huán)境也是沒有調(diào)試信息的庸论。

上述情況暫時還沒有比較好的處理方法,但是開發(fā)者工具卻改進了上述的調(diào)試體驗棒呛,將所有的代碼都打包成單一的 WebAssembly 文件聂示,對應(yīng)到我們這次就是 mandelbrot.wasm 文件,這樣我們再也無需擔(dān)心其中的某段代碼到底來自哪個源文件簇秒。

新的命名生成策略

之前的調(diào)試面板里面鱼喉,針對 WebAssembly 只有一些數(shù)字索引,而對于函數(shù)則連名字都沒有宰睡,如果沒有必要的類型信息蒲凶,那么很難追蹤到某個具體的值,因為指針將以整數(shù)的形式展示出來拆内,但你不知道這些整數(shù)背后存儲著什么旋圆。

新的命名策略參考了其他反匯編工具的命名策略,使用了 WebAssembly 命名策略[24]部分的內(nèi)容麸恍、import/export 的路徑相關(guān)的內(nèi)容灵巧,可以看到我們現(xiàn)在的調(diào)試面板中針對函數(shù)可以展示函數(shù)名相關(guān)的信息:

即使遇到了程序錯誤搀矫,基于語句的類型和索引也可以生成類似 $func123 這樣的名字,大大提高了棧追蹤和反匯編的體驗刻肄。

查看內(nèi)存面板

如果想要調(diào)試此時程序占用的內(nèi)存相關(guān)的內(nèi)容瓤球,可以在 WebAssembly 的上下文下,查看 Scope 面板里的 Module.memories.$env.memory 敏弃,但是這只能看到一些獨立的字節(jié)卦羡,無法了解到這些字節(jié)對應(yīng)到的其他數(shù)據(jù)格式,如 ASCII 格式麦到。但是 Chrome 開發(fā)者工具還為我們提供了一些其他更加強大的內(nèi)存查看形式绿饵,當我們右鍵點擊 env.memory 時,可以選擇 Reveal in Memory Inspector panel:

或者點擊 env.memory 旁邊的小圖標:

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

從內(nèi)存面板里面可以查看以十六進制或 ASCII 的形式查看 WebAssembly 的內(nèi)存瓶颠,導(dǎo)航到特定的內(nèi)存地址拟赊,將特定數(shù)據(jù)解析成各種不同的格式,如十六進制 65 代表的 e 這個 ASCII 字符粹淋。

對 WebAssembly 代碼進行性能分析

因為我們在編譯時為代碼注入了很多調(diào)試信息吸祟,運行的代碼是未經(jīng)優(yōu)化且冗長的代碼,所以運行時會很慢桃移,所以如果為了評估程序運行的性能屋匕,你不能使用 performance.now 或者 console.time 等 API,因為這些函數(shù)調(diào)用獲得的性能相關(guān)的數(shù)字通常不能反應(yīng)真實世界的效果谴轮。

所以如果需要對代碼進行性能分析炒瘟,你需要使用開發(fā)者工具提供的性能面板吹埠,性能面板里面會全速運行代碼第步,并且提供不同函數(shù)執(zhí)行時花費時間的明確斷點信息:

可以看到上述幾個比較典型的時間點如 161ms,或者 461ms 的 LCP 與 FCP 缘琅,這些都是能反應(yīng)真實世界下的性能指標粘都。

或者你可以在加載網(wǎng)頁時關(guān)閉控制臺,這樣就不會涉及到調(diào)試信息等相關(guān)內(nèi)容的調(diào)用刷袍,可以確保比較真實的效果翩隧,等到頁面加載完成,然后再打開控制臺查看相關(guān)的指標信息呻纹。

在不同的機器上進行調(diào)試

當在 Docker堆生、虛擬機或者其他原創(chuàng)服務(wù)器上進行構(gòu)建時,你可能會遇到那種構(gòu)建時使用的源文件路徑和本地文件系統(tǒng)上的文件路徑不一致雷酪,這會導(dǎo)致開發(fā)者工具在運行時可以在 Sources 面板里展示出有這個文件淑仆,但是無法加載文件內(nèi)容。

為了解決這個問題哥力,我們需要在之前安裝的 C/C++ Devtools Support[25] 配置里面設(shè)置路徑映射蔗怠,點擊擴展的 “選項”:

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

上述映射的功能和一些 C++ 的調(diào)試器如 GDB 的 set substitute-path 以及 LLDB 的 target.source-map 很像寞射。這樣開發(fā)者工具在查找源文件時渔工,會查看是否在配置的路徑映射里有對應(yīng)的映射,如果源路徑無法加載文件桥温,那么開發(fā)者工具會嘗試從映射路徑加載文件引矩,否則會加載失敗。

調(diào)試優(yōu)化性構(gòu)建的代碼

如果你想調(diào)試一些在構(gòu)建時進行優(yōu)化后的代碼侵浸,可能會獲得不太理想的調(diào)試體驗脓魏,因為進行優(yōu)化構(gòu)建時,函數(shù)內(nèi)聯(lián)在一起通惫,可能還會對代碼進行重排序或去除一部分無用的代碼茂翔,這些都可能會混淆調(diào)試者。

目前開發(fā)者工具除了對函數(shù)內(nèi)聯(lián)時不能搞很好的支持外履腋,能夠支持絕大部分優(yōu)化后代碼的調(diào)試體驗珊燎,為了減少函數(shù)內(nèi)聯(lián)支持能力欠缺帶來的調(diào)試影響,建議在對代碼進行編譯時加入 -fno-inline 標志來取消優(yōu)化構(gòu)建時(通常是帶上 -O 參數(shù))對函數(shù)進行內(nèi)聯(lián)處理的功能遵湖,未來開發(fā)者工具會修復(fù)這個問題悔政。所以針對之前提到的簡單 C 程序的編譯腳本如下:

emcc -g temp.c -o temp.html \

     -O3 -fno-inline

將調(diào)試信息單獨存儲

調(diào)試信息包含代碼的詳細信息,定義的類型延旧、變量谋国、函數(shù)、函數(shù)作用域迁沫、以及文件位置等任何有利于調(diào)試器使用的信息芦瘾,所以通常調(diào)試信息比源代碼還要大。

為了加速 WebAssembly 模塊的編譯和加載速度集畅,你可以在編譯時將調(diào)試信息拆分成獨立的 WebAssembly 文件近弟,然后單獨加載,為了實現(xiàn)拆分單獨文件挺智,可以在編譯時加入 -gseparate-dwarf 操作:

emcc -g temp.c -o temp.html \

     -gseparate-dwarf=temp.debug.wasm

進行上述操作之后祷愉,編譯之后的主應(yīng)用代碼只會存儲一個 temp.debug.wasm 的文件名,然后在代碼加載時赦颇,插件會定位到調(diào)試文件的位置并將其加載進開發(fā)者工具二鳄。

如果我們想同時進行優(yōu)化構(gòu)建,并將調(diào)試信息單獨拆分媒怯,并在之后需要調(diào)試時订讼,加載本地的調(diào)試文件進行調(diào)試,在這種場景下沪摄,我們需要重載調(diào)試文件存儲的地址來幫助插件能夠找到這個文件躯嫉,可以運行如下命令來處理:

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)的存儲地址]

在瀏覽器中調(diào)試 ffmpeg 代碼

通過這篇文章我們深入了解了如何在瀏覽器中調(diào)試通過 Emscripten 構(gòu)建而來的 C/C++ 代碼纱烘,上述講解了一個普通無依賴的例子以及一個依賴于 C++ 標準庫 SDL 的例子,并且講解了現(xiàn)階段調(diào)試工具可以做的事情和限制祈餐,接下來我們就通過學(xué)到的知識來了解如何在瀏覽器中調(diào)試 ffmpeg 相關(guān)的代碼擂啥。

帶上調(diào)試信息的構(gòu)建

我們只需要修改在之前的文章中提到的構(gòu)建腳本 build-with-emcc.sh ,加入 -g 對應(yīng)的標志:

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í)行其他操作哺壶,最后通過 node server.js 運行我們的腳本,然后打開 http://localhost:8080/ 查看效果如下:

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

然后在網(wǎng)頁中上傳一個 avi 格式的視頻鳍徽,接著程序會暫停到斷點位置:

可以發(fā)現(xiàn)资锰,我們依然可以像之前一樣在程序中鼠標移動上去查看變量值,以及在右側(cè)的 Scope 面板里查看變量值阶祭,以及可以在控制臺中查看變量值绷杜。

類似的,我們也可以進行 step over濒募、step in鞭盟、step out、step 等復(fù)雜調(diào)試操作瑰剃,或者 watch 某個變量值齿诉,或查看此時的內(nèi)存等。

可以看到通過這篇文章介紹的知識晌姚,你可以在瀏覽器中對任意大小的 C/C++ 項目進行調(diào)試粤剧,并且可以使用目前開發(fā)者工具提供的絕大部分功能。

關(guān)于 WebAssembly 的未來

本文僅僅列舉了一些 WebAssembly 當前的一些主要應(yīng)用場景舀凛,包含 WebAssembly 的高性能俊扳、輕量和跨平臺,使得我們可以將 C/C++ 等語言運行在 Web猛遍,也可以將桌面端應(yīng)用跑在 Web 容器。

但是這篇文章沒有涉及到的內(nèi)容有 WASI[26]号坡,一種將 WebAssembly 跑在任何系統(tǒng)上的標準化系統(tǒng)接口懊烤,當 WebAssembly 的性能逐漸增強時,WASI 可以提供一種確實可行的方式宽堆,可以在任意平臺上運行任意的代碼腌紧,就像 Docker 所做的一樣,但是不需要受限于操作系統(tǒng)畜隶。正如 Docker 的創(chuàng)始人所說:

“ 如果 WASM+WASI 在 2008 年就出現(xiàn)的話壁肋,那么就不需要創(chuàng)造 Docker 了号胚,服務(wù)器上的 WASM 是計算的未來,是我們期待已久的標準化的系統(tǒng)接口浸遗。

另一個有意思的內(nèi)容是 WASM 的客戶端開發(fā)框架如 yew[27]猫胁,未來可能將像 React/Vue/Angular 一樣流行。

而 WASM 的包管理工具 WAPM[28]跛锌,得益于 WASM 的跨平臺特性弃秆,可能會變成一種在不同語言的不同框架之間共享包的首選方式。

同時 WebAssembly 也是由 W3C 主要負責(zé)開發(fā)髓帽,各大廠商菠赚,包括 Microsoft、Google郑藏、Mozilla 等贊助和共同維護的一個項目衡查,相信 WebAssembly 會有一個非常值得期待的未來。

Q & A

答疑...

  • 如何將復(fù)雜的 CMake 項目編譯到 WebAssembly必盖?
  • 在編譯復(fù)雜的 CMake 項目到 WebAssembly 時如何探索一套通用的最佳實踐峡捡?
  • 如何和 CMake 項目結(jié)合起來進行 Debug?

問題:

  • 編譯之后的代碼的體積

參考鏈接

參考資料

[1]WebAssembly 入門:如何和有 C 項目結(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]wabAssemblyScript: 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/deSDL: 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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末筑悴,一起剝皮案震驚了整個濱河市们拙,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌阁吝,老刑警劉巖砚婆,帶你破解...
    沈念sama閱讀 206,968評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異突勇,居然都是意外死亡装盯,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評論 2 382
  • 文/潘曉璐 我一進店門甲馋,熙熙樓的掌柜王于貴愁眉苦臉地迎上來埂奈,“玉大人,你說我怎么就攤上這事定躏≌嘶牵” “怎么了?”我有些...
    開封第一講書人閱讀 153,220評論 0 344
  • 文/不壞的土叔 我叫張陵痊远,是天一觀的道長垮抗。 經(jīng)常有香客問我,道長碧聪,這世上最難降的妖魔是什么冒版? 我笑而不...
    開封第一講書人閱讀 55,416評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮逞姿,結(jié)果婚禮上辞嗡,老公的妹妹穿的比我還像新娘捆等。我一直安慰自己,他們只是感情好续室,可當我...
    茶點故事閱讀 64,425評論 5 374
  • 文/花漫 我一把揭開白布惠窄。 她就那樣靜靜地躺著碍脏,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上列疗,一...
    開封第一講書人閱讀 49,144評論 1 285
  • 那天俊嗽,我揣著相機與錄音堕仔,去河邊找鬼躲因。 笑死,一個胖子當著我的面吹牛趁耗,可吹牛的內(nèi)容都是我干的沉唠。 我是一名探鬼主播,決...
    沈念sama閱讀 38,432評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼苛败,長吁一口氣:“原來是場噩夢啊……” “哼满葛!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起罢屈,我...
    開封第一講書人閱讀 37,088評論 0 261
  • 序言:老撾萬榮一對情侶失蹤嘀韧,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后缠捌,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體锄贷,經(jīng)...
    沈念sama閱讀 43,586評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,028評論 2 325
  • 正文 我和宋清朗相戀三年曼月,在試婚紗的時候發(fā)現(xiàn)自己被綠了谊却。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,137評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡哑芹,死狀恐怖炎辨,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情聪姿,我是刑警寧澤碴萧,帶...
    沈念sama閱讀 33,783評論 4 324
  • 正文 年R本政府宣布,位于F島的核電站咳燕,受9級特大地震影響勿决,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜招盲,卻給世界環(huán)境...
    茶點故事閱讀 39,343評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望嘉冒。 院中可真熱鬧曹货,春花似錦咆繁、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,333評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至礼饱,卻和暖如春坏为,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背镊绪。 一陣腳步聲響...
    開封第一講書人閱讀 31,559評論 1 262
  • 我被黑心中介騙來泰國打工匀伏, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人蝴韭。 一個月前我還...
    沈念sama閱讀 45,595評論 2 355
  • 正文 我出身青樓够颠,卻偏偏與公主長得像,于是被迫代替她去往敵國和親榄鉴。 傳聞我的和親對象是個殘疾皇子履磨,可洞房花燭夜當晚...
    茶點故事閱讀 42,901評論 2 345

推薦閱讀更多精彩內(nèi)容