爲什麼說 WebAssembly 是 Web 的未來?

這篇文章打算講什麼?

瞭解 WebAssembly 的前世今生,這一致力於讓 Web 更廣泛使用的偉大創造是如何在整個 Web/Node.js 的生命週期起作用的。

在整篇文章的講解過程中,你可以瞭解到 WebAssembly 原生、AssemblyScript、Emscripten 編譯器、以及如何在瀏覽器調試 WebAssembly 程序的。

最後還對 WebAssembly 的未來進行了展望,列舉了一些令人興奮的技術的發展方向。

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

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

爲什麼需要 WebAssembly ?

動態語言之踵

首先先來看一下 JS 代碼的執行過程:

上述是 Microsoft Edge 之前的 ChakraCore 引擎結構,目前 Microsoft Edge 的 JS 引擎已經切換爲 V8 。

整體的流程就是:

但其實我們平時寫的代碼有很多可以優化的地方,如多次執行同一個函數,那麼可以將這個函數生成的 Machine Code 標記可優化,然後打包送到 JIT Compiler(Just-In-Time),下次再執行這個函數的時候,就不需要經過 Parser-Compiler-Interpreter 這個過程,可以直接執行這份準備好的 Machine Code,大大提高的代碼的執行效率。

但是上述的 JIT 優化只能針對靜態類型的變量,如我們要優化的函數,它只有兩個參數,每個參數的類型是確定的,而 JavaScript 卻是一門動態類型的語言,這也意味着,函數在執行過程中,可能類型會動態變化,參數可能變成三個,第一個參數的類型可能從對象變爲數組,這就會導致 JIT 失效,需要重新進行 Parser-Compiler-Interpreter-Execuation,而 Parser-Compiler 這兩步是整個代碼執行過程中最耗費時間的兩步,這也是爲什麼 JavaScript 語言背景下,Web 無法執行一些高性能應用,如大型遊戲、視頻剪輯等。

靜態語言優化

通過上面的說明了解到,其實 JS 執行慢的一個主要原因是因爲其動態語言的特性,導致 JIT 失效,所以如果我們能夠爲 JS 引入靜態特性,那麼可以保持有效的 JIT,勢必會加快 JS 的執行速度,這個時候 asm.js 出現了。

asm.js 只提供兩種數據類型:

其他類似如字符串、布爾值或對象都是以數值的形式保存在內存中,通過 TypedArray 調用。整數和浮點數表示如下:

ArrayBuffer對象、TypedArray視圖和DataView 視圖是 JavaScript 操作二進制數據的一個接口,以數組的語法處理二進制數據,統稱爲二進制數組。參考 ArrayBuffer[2] 。

var a = 1;

var x = a | 0;  // x 是32位整數

var y = +a;  // y 是64位浮點數

而函數的寫法如下:

function add(x, y) {

  x = x | 0;

  y = y | 0;

  return (x + y) | 0;

}

上述的函數參數及返回值都需要聲明類型,這裏都是 32 位整數。

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

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

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

function compiledCode(ptr) {

  HEAP[ptr] = 12;

  return HEAP[ptr + 4];

}

從上可見,asm.js 是一個嚴格的 JavaScript 子集要求變量的類型在運行時確定且不可改變,且去除了 JavaScript 擁有的垃圾回收機制,需要開發者手動管理內存。這樣 JS 引擎就可以基於 asm.js 的代碼進行大量的 JIT 優化,據統計 asm.js 在瀏覽器裏面的運行速度,大約是原生代碼(機器碼)的 50% 左右。

推陳出新

但是不管 asm.js 再怎麼靜態化,幹掉一些需要耗時的上層抽象(垃圾收集等),也還是屬於 JavaScript 的範疇,代碼執行也需要 Parser-Compiler 這兩個過程,而這兩個過程也是代碼執行中最耗時的。

爲了極致的性能,Web 的前沿開發者們拋棄 JavaScript,創造了一門可以直接和 Machine Code 打交道的彙編語言 WebAssembly,直接幹掉 Parser-Compiler,同時 WebAssembly 是一門強類型的靜態語言,能夠進行最大限度的 JIT 優化,使得 WebAssembly 的速度能夠無限逼近 C/C++ 等原生代碼。

相當於下面的過程:

WebAssembly 初探

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

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

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

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

WebAssembly 文本格式

實際上 WASM 是一堆可以直接執行二進制格式,但是爲了易於在文本編輯器或開發者工具裏面展示,WASM 也設計了一種 “中間態” 的文本格式 [3],以 .wat.wast 爲擴展命名,然後通過 wabt[4] 等工具,將文本格式下的 WASM 轉爲二進制格式的可執行代碼,以 .wasm 爲擴展的格式。

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

(module

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

  (func (export "exported_func")

    i32.const 42

    call $i

  )

)

上述代碼邏輯如下:

我們通過 wabt 將上述文本格式轉爲二進制代碼:

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

wat2wasm simple.wat -o simple.wasm

雖然轉換成了二進制,但是無法在文本編輯器中查看其內容,爲了查看二進制的內容,我們可以在編譯時加上 -v 選項,讓內容在命令行輸出:

wat2wasm simple.wat -v

輸出結果如下:

可以看到,WebAssembly 其實是二進制格式的代碼,即使其提供了稍爲易讀的文本格式,也很難真正用於實際的編碼,更別提開發效率了。

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

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

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

WebAssembly 類型大致如下:

Binaryen 會前置將 AssemblyScript 靜態編譯成強類型的 WebAssembly 二進制,然後纔會交給 JS 引擎去執行,所以說雖然 AssemblyScript 帶來了一層抽象,但是實際用於生產的代碼依然是 WebAssembly,保有 WebAssembly 的性能優勢。AssemblyScript 被設計的和 TypeScript 非常相似,提供了一組內建的函數可以直接操作 WebAssembly 以及編譯器的特性.

內建函數:

然後基於這套內建的函數向上構建一套標準庫。

標準庫:

如一個典型的 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++ 等靜態強類型的要求,如不初始化,進行內存分配就訪問就會報錯。

還有一些擴展庫,如 Node.js 的 process、crypto 等,JS 的 console,還有一些和內存相關的 StaticArray、heap 等。

可以看到通過上面基礎的類型、內建庫、標準庫和擴展庫,AssemblyScript 基本上構造了 JavaScript 所擁有的的全部特性,同時 AssemblyScript 提供了類似 TypeScript 的語法,在寫法上嚴格遵循強類型靜態語言的規範。

值得一提的是,因爲當前 WebAssembly 的 ES 模塊規範依然在草案中,AssemblyScript 自行進行了模塊的實現,例如導出一個模塊:

// env.ts

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

導入一個模塊:

import { doSomething } from "./env";

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

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 形式的語法,遵循靜態強類型的規範進行高效編碼,同時又能夠便捷的操作 WebAssembly / 編譯器相關的 API,代碼寫完之後,通過 Binaryen 編譯器將其編譯爲 WASM 二進制,然後獲取到 WASM 的執行性能。

得益於 AssemblyScript 兼具靈活性與性能,目前使用 AssemblyScript 構建的應用生態已經初具繁榮,目前在區塊鏈、構建工具、編輯器、模擬器、遊戲、圖形編輯工具、庫、IoT、測試工具等方面都有大量使用 AssemblyScript 構建的產物:https://www.assemblyscript.org/built-with-assemblyscript.html#games

您的瀏覽器不支持 video 標籤

上面是使用 AssemblyScript 構建的一個五子棋遊戲。

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

雖然 AssemblyScript 的出現極大的改善了 WebAssembly 在高效率編碼方面的缺陷,但是作爲一門新的編程語言,其最大的劣勢就是生態、開發者與積累。

WebAssembly 的設計者顯然在設計上同時考慮到了各種完善的情況,既然 WebAssembly 是一種二進制格式,那麼其就可以作爲其他語言的編譯目標,如果能夠構建一種編譯器,能夠將已有的、成熟的、且兼具海量的開發者和強大的生態的語言編譯到 WebAssembly 使用,那麼相當於可以直接複用這個語言多年的積累,並用它們來完善 WebAssembly 生態,將它們運行在 Web、Node.js 中。

幸運的是,針對 C/C++ 已經有 Emscripten[9] 這樣優秀的編譯器存在了。

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

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

上述的 JS “Gule” 代碼是必須的,因爲如果需要將 C/C++ 編譯到 WASM,還能在瀏覽器中執行,就得實現映射到 C/C++ 相關操作的 Web API,這樣才能保證執行有效,這些膠水代碼目前包含一些比較流行的 C/C++ 庫,如 SDL[10]、OpenGL[11]、OpenAL[12]、以及 POSIX[13] 的一部分 API。

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

WebAssembly 會取代 JavaScript 嗎?

答案是不會。

根據上面的層層闡述,實際上 WASM 的設計初衷就可以梳理爲以下幾點:

所以從初衷出發,WebAssembly 的作用更適合下面這張圖:

WASM 橋接各種系統編程語言的生態,進一步補齊了 Web 開發生態之外,還爲 JS 提供性能的補充,正是 Web 發展至今所缺失的重要的一塊版圖。

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 執行,Emscripten Runtime 其實還提供了兼容 C/C++ 標準庫、相關 API 到 Web/Node.js API 的映射,這份映射存在於編譯之後的 JS 膠水代碼中。

再看下面這張圖,紅色部分爲 Emscripten 編譯後的產物,綠色部分爲 Emscripten 爲保證 C/C++ 代碼能夠運行的一些 runtime 支持:

簡單體驗一下 “Hello World”

值得一提的是,WebAssembly 相關工具鏈的安裝幾乎都是以源碼的形式提供,這可能和 C/C++ 生態的習慣不無關係。

爲了完成簡單的 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



# 將相應的環境變量加入到系統 PATH

source ./emsdk_env.sh



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

emcc -v #

如果安裝成功,上述的命令運行之後會輸出如下結果:

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 環境。

接下來我們嘗試一下將代碼運行在 Web 環境,修改編譯代碼如下:

emcc main.c -o main.html

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

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

如果要在瀏覽器打開這個 HTML,需要在本地起一個服務器,因爲單純的打開通過 file:// 協議訪問時,主流瀏覽器不支持 XHR 請求,只有在 HTTP 服務器下,才能進行 XHR 請求,所以我們運行如下命令來打開網站:

npx serve .

打開網頁,訪問 localhost:3000/main.html,可以看到如下結果:

同時開發者工具裏面也會有相應的打印輸出:

嘗試在 JS 中調用 C/C++ 函數

上一小節我們初步體驗了一下如何在 Web 和 Node.js 中運行 C 程序,但其實如果我們想要讓複雜的 C/C++ 應用,如 Unity 運行在 Web,那我們還有很長的路要走,其中一條,就是能夠在 JS 中操作 C/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 默認編譯的代碼只會調用 main 函數,其他的代碼會作爲 “死代碼” 在編譯時被刪掉,所以爲了使用我們在上面定義的 myFunction ,我們需要在其定義之前加上 EMSCRIPTEN_KEEPALIVE 聲明,確保在編譯時不會刪掉 myFunction 函數相關的代碼。

我們需要導入 emscripten/emscripten.h 頭文件,才能使用 EMSCRIPTEN_KEEPALIVE 聲明。

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

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

上述額外增加了兩個參數:

進行編譯之後,我們還需要修改生成的 function.html 文件,加入我們的函數調用邏輯如下:

<html>

  <body>

    <!-- 其它 HTML 內容 -->

    <button class="mybutton">Run myFunction</button>

  </body>

  <!-- 其它 JS 引入 -->

  <script>

      document

        .querySelector(".mybutton")

        .addEventListener("click"function () {

          alert("check console");

          var result = Module.ccall(

            "myFunction", // 需要調用的 C 函數名

            null, // 函數返回類型

            null, // 函數參數類型,默認是數組

            null // 函數需要傳入的參數,默認是數組

          );

        });

    </script>

</html>

可以看到我們增加了一個 Button,然後增加了一段腳本,爲這個 Button 註冊了 click 事件,在回調函數里,我們調用了 myFunction 函數。

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

只執行 main 函數:

嘗試點擊按鈕執行 myFunction 函數:

可以看到首先進行 alert 彈框展示,然後打開控制檯,可以看到 myFunction 的調用結果,打印 "MyFunction Called"

初嘗 Emscripten 文件系統

我們可以在 C/C++ 程序中使用 libc stdio API 如 fopenfclose 來訪問你文件系統,但是 JS 是運行在瀏覽器提供的沙盒環境裏,無法直接訪問到本地文件系統。所以爲了兼容 C/C++ 程序訪問文件系統,編譯爲 WASM 之後依然能夠正常運行,Emscripten 會在其 JS 膠水代碼裏面模擬一個文件系統,並提供和 libc stdio 一致的 API。

讓我們重新創建一個名爲 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 ,然後一行一行的讀取文件內容,如果程序執行過程中有任何的出錯,就會打印錯誤。

我們在目錄下新建 file.txt 文件,並加入如下內容:

==

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 參數,提前將文件內容加載進 Emscripten runtime,因爲在 C/C++ 等程序上訪問文件都是同步操作,而 JS 是基於事件模型的異步操作,且在 Web 中只能通過 XHR 的形式去訪問文件(Web Worker、Node.js 可同步訪問文件),所以需要提前將文件加載好,確保在代碼編譯之前,文件已經準備好了,這樣 C/C++ 代碼可以直接訪問到文件。

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

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

上述添加了 -s EXIT_RUNTIME=1 ,依然是確保 main 邏輯執行完之後,程序不會退出。

然後運行我們的本地服務器,訪問 http://localhost:3000/file.html,可以查看結果:

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

通過上面三個例子,我們已經瞭解了基礎的 C/C++ 如打印、函數調用、文件系統相關的內容如何編譯爲 WASM,並在 JS 中運行,這裏的 JS 特指 Web 和 Node.js 環境,通過上面的例子基本上絕大部分自己寫的 C/C++ 程序都可以自行編譯到 WASM 使用了。

而之前我們也提到過,其實當前 WebAssembly 最大的一個應用場景,就是最大程度的複用當前已有語言的生態,如 C/C++ 生態的庫,這些庫通常都依賴 C 標準庫、操作系統、文件系統或其他依賴,而 Emscripten 最厲害的一點就在於能夠兼容絕大部分這些依賴的特性,儘管還存在一些限制,但是已經足夠可用。

簡單的測試

接下來我們來了解一下如何將一個現存的、比較複雜且廣泛使用的 C 模塊:libwebp,將其編譯到 WASM 並允許到 Web。libwebp 的源碼是用 C 實現的,能夠在 Github[16] 上找到它,同時可以瞭解到它的一些 API 文檔 [17]。

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

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

爲了快速測試是否正確的接入了 libwebp 進行使用,我們可以編寫一個簡單的 C 函數,然後在裏面調用 libwebp 獲取版本的函數,測試版本是否可以正確獲取。

我們在目錄下創建 webp.c 文件,添加如下內容:

#include "emscripten.h"

#include "src/webp/encode.h"



EMSCRIPTEN_KEEPALIVE int version() {

  return WebPGetEncoderVersion();

}

上述的 WebPGetEncoderVersion 就是 libwebp 裏面獲取當前版本的函數,而我們是通過導入 src/webp/encode.h 頭文件來獲取這個函數的,爲了讓編譯器在編譯時能夠找到這個頭文件,我們需要在編譯的時候將 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

上述命令中主要做了如下工作:

上述的編譯輸出只有 a.out.jsa.out.wasm ,我們還需要建一份 HTML 文檔來使用輸出的腳本代碼,新建 webp.html ,添加如下內容:

<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 的回調裏面去執行我們 WASM 相關的操作,因爲 WASM 相關的代碼從加載到可用是需要一段時間的,而 onRuntimeInitialized 的回調則是確保 WASM 相關的代碼已經加載完成,達到可用狀態。

接着我們可以運行 npx serve . ,然後訪問 http://localhost:3000/webp.html,查看結果:

可以看到控制檯打印了 66049 版本號。

libwebp 通過十六進制的 0xabc 的 abc 來表示當前版本 a.b.c ,例如 v0.6.1,則會被編碼成十六進制 0x000601 ,對應的十進制爲 1537。而這裏爲十進制 66049,轉成 16 進制則爲 0x010201 ,表示當前版本爲 v1.2.1。

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

剛剛通過調用編碼器的 WebPGetEncoderVersion 方法來獲取版本號來證實了已經成功編譯了 libwebp 庫到 wasm,然後可以在 JavaScript 使用它,接下來我們將瞭解更加複雜的操作,如何使用 libwebp 的編碼 API 來轉換圖片格式。

libwebp 的 encoding API 需要接收一個關於 RGB、RGBA、BGR 或 BGRA 的字節數組,幸運的是,Canvas API 有一個 CanvasRenderingContext2D.getImageData 方法,能夠返回一個 Uint8ClampedArray ,這個數組包含 RGBA 格式的圖片數據。

首先我們需要在 JavaScript 中編寫加載圖片的函數,將其寫到上一步創建的 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);

      

      // 設置 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>

現在剩下的操作則是如何將圖片數據從 JavaScript 複製到 wasm,爲了達成這個目的,需要在先前的 webp.c 函數里面暴露額外的方法:

修改 webp.c 如下:

#include <stdlib.h> // 此頭文件導入用於分配內存的 malloc 方法和釋放內存的 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 的圖片分配內存,RGBA 圖片一個像素包含 4 個字節,所以代碼中需要添加 4 * sizeof(uint8_t)malloc 函數返回的指針指向所分配內存的第一塊內存單元地址,當這個指針返回給 JavaScript 使用時,會被當做一個簡單的數字處理。當通過 cwrap 函數獲取暴露給 JavaScript 的對應 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);

      

      // 設置 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>

可以看到上述代碼除了導入之前添加的 create_bufferdestroy_buffer 外,還有很多用於編碼文件等方面的函數,我們將在後續講解,除此之外,代碼首先加載了一份 image.jpg 的圖片,然後調用 C 函數爲此圖片數據分配內存,並相應的拿到返回的指針傳給 WebAssembly 的 Module.HEAP8 ,在內存開始位置 p,寫入圖片的數據,最後會釋放分配的內存。

編碼圖片

現在圖片數據已經加載進 wasm 的內存中,可以調用 libwebp 的 encoder 方法來完成編碼過程了,通過查閱 WebP 的文檔 [18],發現可以使用 WebPEncodeRGBA 函數來完成工作。這個函數接收一個指向圖片數據的指針以及它的尺寸,以及每次需要跨越的 stride 步長,這裏爲 4 個字節(RGBA),一個區間在 0-100 的可選的質量參數。在編碼的過程中,WebPEncodeRGBA 會分配一塊用於輸出數據的內存,我們需要在編碼完成之後調用 WebPFree 來釋放這塊內存。

我們打開 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 函數執行的結果爲分配一塊輸出數據的內存以及返回內存的大小。因爲 C 函數無法使用數組作爲返回值(除非我們需要進行動態內存分配),所以我們使用一個全局靜態數組來獲取返回的結果,這可能不是很規範的 C 代碼寫法,同時它要求 wasm 指針爲 32 比特長,但是爲了簡單起見我們可以暫時容忍這種做法。

現在 C 側的相關邏輯已經編寫完畢,可以在 JavaScript 側調用編碼函數,獲取圖片數據的指針和圖片所佔用的內存大小,將這份數據保存到 WASM 的緩衝中,然後釋放 wasm 在處理圖片時所分配的內存,讓我們打開 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);

      

      // 設置 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 函數加載了一張本地的 image.jpg 圖片,你需要事先準備一張圖片放置在 emcc 編譯器輸出的目錄下,也就是我們的 HTML 文件目錄下使用。

注意:new Uint8Array(someBuffer) 將會在同樣的內存塊上創建一個新視圖,而 new Uint8Array(someTypedArray) 只會複製 someTypedArray 的數據,確保使用複製的數據進行操作,不會修改原內存數據。

當你的圖片比較大時,因爲 wasm 不能自動擴充內存,如果默認分配的內存無法容納 inputoutput 圖片數據的內存,你可能會遇到如下報錯:

但是我們例子中使用的圖片比較小,所以只需要單純的在編譯時加上一個過濾參數 -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

再次運行上述命令,得到添加了編碼函數的 wasm 代碼和對應的 JavaScript 膠水代碼,這樣當我們打開 HTML 文件時,它已經能夠將一份 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>

然後刷新瀏覽器,你應該可以看到如下界面:

通過將這個文件下載到本地,可以看到其格式轉成了 WebP:

通過上述的流程我們成功編譯了現有的 libwebp C 庫到 wasm 使用,並將 JPG 圖片轉成了 WebP 格式並展示在 Web 界面上,通過 wasm 來處理計算密集型的轉碼操作可以大大提高網頁的性能,這也是 WebAssembly 帶來的主要優勢之一。

如何編譯 FFmpeg 到 WebAssembly?

好傢伙,剛剛教會 1+1,就開始解二次方程了。🌚

在上個例子中我們成功編譯了已經存在的 C 模塊到 WebAssembly,但是有很多更大型的項目依賴於 C 標準庫、操作系統、文件系統或其他依賴,這些項目在編譯前依賴 autoconfig/automake 等庫來生成系統特定的代碼。

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

./configure # 處理前置依賴

make # 使用 gcc 等進行編譯構建,生成對象文件

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

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

emmake make # emmake make -j4 調起多核編譯,生成 wasm 對象文件,而非傳統的 C 對象文件

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

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

經過實踐發現 ffmpeg 的編譯依賴於特定的 ffmpeg 版本、Emscripten 版本、操作系統環境等,所以以下的 ffmpeg 的編譯都是限制在特定的條件下進行的,主要是爲之後通用的 ffmpeg 的編譯提供一種思路和調試方法。

準備目錄

這一次我們創建 WebAssembly 目錄,然後在這個目錄下放置 ffmpeg 源碼、以及後續要用到的 x264 解碼器的相關代碼:

mkdir WebAssembly



# Clone 代碼倉庫

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



# 進入倉庫

cd emsdk



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

git pull

編譯步驟

使用 Emscripten 編譯大部分複雜的 C/C++ 庫時,主要需要三個步驟:

  1. 使用 emconfigure 運行項目的 configure 文件將 C/C++ 代碼編譯器從 gcc/g++ 換成 emcc/em++

  2. 通過 emmake make 來構建 C/C++ 項目,生成 wasm 對象的 .o 文件

  3. 調用 emcc 接收編譯的對象文件 .o 文件,然後輸出最終的 WASM 和 JS 膠水代碼

安裝特定依賴

注意:這一步我們在講解 Emscripten 的開頭就已經安裝了對應的版本,這裏只是再強調一下版本。

爲了驗證 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 # 設置爲 none 來去除特定操作系統的一些依賴

  --arch=x86_32 # 選中架構爲 x86_32                                                                                                                

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

  --disable-x86asm  # 關閉 x86asm                                                                                                                

  --disable-inline-asm  # 關閉內聯的 asm                                                        

  --disable-stripping # 關閉處理 strip 的功能,避免誤刪一些內容

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

上述腳本主要做了如下幾件事:

使用 emmake make 來構建依賴

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

# 構建最終的 ffmpeg.wasm 文件

emmake make -j4

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

前兩個都是 JS 文件,第三個爲 wasm 模塊,第四個是處理 worker 中運行相關邏輯的函數,上述生成的文件的理想形式應該爲三個,爲了達成這種自定義的編譯,有必要自定義使用 emcc 命令來進行處理。

使用 emcc 進行編譯輸出

FFmpeg 目錄下創建 wasm 文件夾,用於放置構建之後的文件,然後自定義編譯文件輸出如下:

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=   # 使用 SDL2

  -s USE_PTHREADS=\

  -s PROXY_TO_PTHREAD=# 將 main 函數與瀏覽器/UI主線程分離  

  -s INVOKE_RUN=# 執行 C 函數時不首先執行 main 函數           

  -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 在編譯時設置了 pthread 時,使得程序具備響應式特效

  2. -o wasm/dist/ffmpeg-core.js 則將原 ffmpeg js 文件的輸出重命名爲 ffmpeg-core.js ,對應的輸出 ffmpeg-core.wasmffmpeg-core.worker.js

  3. -s EXPORTED_FUNCTIONS="[_main, _proxy_main]" 導出 ffmpeg 對應的 C 文件裏的 main 函數,proxy_main 則是通過設置 PROXY_TO_PTHREAD代理 main 函數用於外部使用

  4. -s EXTRA_EXPORTED_RUNTIME_METHODS="[FS, cwrap, setValue, writeAsciiToMemory]" 則是導出一些 runtime 的輔助函數,用於導出 C 函數、處理文件系統、指針的操作

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

使用編譯完成的 ffmpeg wasm 模塊

wasm 目錄下創建 ffmpeg.js 文件,在其中寫入如下代碼:

const 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

上述代碼解釋如下:

第一部分很簡單,因爲 Emscripten 提供了一個輔助函數 writeAsciiToMemory 來完成這一工作:

const str = "FFmpeg.wasm";

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

Module.writeAsciiToMemory(str, buf);

第二部分有一點困難,我們需要創建 C 中的 32 位整數的指針數組,可以藉助 setValue 來幫助我們創建這個數組:

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

上述運行的結果如下:

可以看到我們成功編譯並運行了 ffmpeg 🎉。

處理 Emscripten 文件系統

Emscripten 內建了一個虛擬的文件系統來支持 C 中標準的文件讀取和寫入,所以我們需要將音頻文件傳給 ffmpeg.wasm 時先寫入到文件系統中。

可以戳此查看更多關於文件系統 API[19] 。

爲了完成上述的任務,只需要使用到 FS 模塊的兩個函數 FS.writeFile()FS.readFile() ,對於從文件系統中讀取和寫入的所有數據都要求是 JavaScript 中的 Uint8Array 類型,所以在消費數據之前有必要約定數據類型。

我們將通過 fs.readFileSync() 方法讀取名爲 flame.avi 的視頻文件,然後使用 FS.writeFile() 將其寫入到 Emscripten 文件系統。

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 編譯視頻

現在我們已經可以將視頻文件保存到 Emscripten 文件系統了,接下來就是實際使用編譯好的 ffmepg 來進行視頻的轉碼了。

我們修改代碼如下:

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 轉碼視頻的過程是異步的,所以我們需要不斷的去讀取 Emscripten 文件系統中是否有轉碼好的文件標誌,當拿到文件標誌且不爲 undefined,我們就使用 Module.FS.readFile() 方法從 Emscripten 文件系統中讀取轉碼好的視頻文件,然後通過 fs.writeFileSync() 將視頻寫入到本地文件系統。最終我們會收到如下結果:

在瀏覽器中使用 ffmpeg 轉碼視頻並播放

在上一步中,我們成功在 Node 端使用了編譯好的 ffmpeg 完成從了 avi 格式到 mp4 格式的轉碼,接下來我們將在瀏覽器中使用 ffmpeg 轉碼視頻,並在瀏覽器中播放。

之前我們編譯的 ffmpeg 雖然可以將 avi 格式轉碼到 mp4 ,但是這種通過默認編碼格式轉碼的 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 的文件夾,可以創建一個 build-x264.sh 文件,並加入如下內容:

 #!/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 目錄下運行如下命令來構建 x264:

bash x264-snapshot-20170226-2245-stable/build-x264.sh

安裝了 x264 編碼器之後,就可以在 ffmpeg 的編譯腳本中加入打開 x264 的開關,這一次我們在 ffmpeg 文件夾下創建 Bash 腳本用於構建,創建 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

然後創建用於自定義輸出構建文件的腳本文件 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 轉碼

我們將創建一個 Web 網頁,然後提供一個上傳視頻文件的按鈕,以及播放上傳的視頻文件。儘管無法直接在 Web 端播放 avi 格式的視頻文件,但是我們可以通過 ffmpeg 轉碼之後播放。

在 ffmpeg 目錄下的 wasm 文件夾下創建 index.html 文件,然後添加如下內容:

<html>                                                                                                                                            

  <head>                                                                                                                                          

    <style>                                                                                                                                       

      html, body {                                                       

        margin: 0;                                                       

        width: 100%;                                                     

        height: 100%                                                     

      }                                                                  

      body {                                                                                                                                      

        display: flex;                                                   

        flex-direction: column;

        align-items: center;                                             

      }   

    </style>                                                                                                                                      

  </head>                                                                

  <body>                                                                 

    <h3>上傳視頻文件,然後轉碼到 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 文件系統';

        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 = '開始轉碼';                        

        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 = '完成轉碼';

              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>

打開上述網頁運行,我們可以看到如下效果:

您的瀏覽器不支持 video 標籤 視頻詳情

恭喜你!成功編譯 ffmpeg 並在 Web 端使用。

如何調試 WebAssembly 代碼?

WebAssembly 的原始調試方式

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

上述的方法對於一些無其他依賴函數的 WebAssembly 模塊來說可以很好的運行,因爲這些模塊只涉及到很小的調試範圍。但是對於複雜的應用來說,如 C/C++ 編寫的複雜應用,一個模塊依賴其他很多模塊,且源代碼與編譯後的 WebAssembly 的文本格式的映射有較大的區別時,上述的調試方式就不太直觀了,只能靠猜的方式才能理解其中的代碼運行方式,且大多數人很難以看懂複雜的彙編代碼。

更加直觀的調試方式

現代的 JavaScript 項目在開發時通常也會存在編譯的過程,使用 ES6 進行開發,編譯到 ES5 及以下的版本進行運行,這個時候如果需要調試代碼,就涉及到 Source Map 的概念,source map 用於映射編譯後的對應代碼在源代碼中的位置,source map 使得客戶端的代碼更具可讀性、更方便調試,但是又不會對性能造成很大的影響。

而 C/C++ 到 WebAssembly 代碼的編譯器 Emscripten 則支持在編譯時,爲代碼注入相關的調試信息,生成對應的 source map,然後安裝 Chrome 團隊編寫的 C/C++ Devtools Support[20] 瀏覽器擴展,就可以使用 Chrome 開發者工具調試 C/C++ 代碼了。

這裏的原理其實就是,Emscripten 在編譯時,會生成一種 DWARF 格式的調試文件,這是一種被大多數編譯器使用的通用調試文件格式,而 C/C++ Devtools Support[21] 則會解析 DWARF 文件,爲 Chrome Devtools 在調試時提供 source map 相關的信息,使得開發者可以在 89+ 版本以上的 Chrome Devtools 上調試 C/C++ 代碼。

調試簡單的 C 應用

因爲 DWARF 格式的調試文件可以提供處理變量名、格式化類型打印消息、在源代碼中執行表達式等等,現在就讓我們實際來編寫一個簡單的 C 程序,然後編譯到 WebAssembly 並在瀏覽器中運行,查看實際的調試效果吧。

首先讓我們進入到之前創建的 WebAssembly 目錄下,激活 emcc 相關的命令,然後查看激活效果:

cd emsdk && source emsdk_env.sh

emcc --version # emcc (Emscripten gcc/clang-like replacement) 1.39.18 (a3beeb0d6c9825bd1757d03677e817d819949a77)

接着在 WebAssembly 創建一個 temp 文件夾,然後創建 temp.c 文件,填充如下內容並保存:

#include <stdlib.h>



void assert_less(int x, int y) {

  if (x >= y) {

    abort();

  }

}



int main() {

  assert_less(10, 20);

  assert_less(30, 20);

}

上述代碼在執行 asset_less 時,如果遇到 x >= y 的情況會拋出異常,終止程序執行。

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

emcc -g temp.c -o temp.html

上述命令在普通的編譯形式上,加入了 -g 參數,告訴 Emscripten 在編譯時爲代碼注入 DWARF 調試信息。

現在可以開啓一個 HTTP 服務器,可以使用 npx serve . ,然後訪問 localhost:5000/temp.html 查看運行效果。

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

爲了查看調試效果,需要設置一些內容。

  1. 打開 Chrome Devtools 裏面的 WebAssembly 調試選項

設置完之後,在工具欄頂部會出現一個 Reload 的藍色按鈕,需要重新加載配置,點擊一下就好。

  1. 設置調試選項,在遇到異常的地方暫停

  1. 刷新瀏覽器,然後你會發現斷點停在了 temp.js ,由 Emscripten 編譯生成的 JS 膠水代碼,然後順着調用棧去找,可以查看到 temp.c 並定位到拋出異常的位置:

可以看到,我們成功在 Chrome Devtools 裏面查看了 C 代碼,並且代碼停在了 abort() 處,同時還可以類似我們調試 JS 時一樣,查看當前 scope 下的值:

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

查看複雜類型值

實際上 Chrome Devtools 不僅可以查看原 C/C++ 代碼中一些變量的普通類型值,如數字、字符串,還可以查看更加複雜的結構,如結構體、數組、類等內容,我們拿另外一個例子來展現這個效果。

我們通過一個在 C++ 裏面繪製 曼德博圖形 的例子來展示上述的效果,同樣在 WebAssembly 目錄下創建 mandelbrot 文件夾,然後添加 mandelbrot.cc 文件,並填入如下內容:

#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 繪製的內容渲染出來

  SDL_RenderPresent(renderer);





  // SDL_Quit();

}

上述代碼差不多 50 行左右,但是引用了兩個 C++ 標準庫:SDL[22] 和 complex numbers[23] ,這使得我們的代碼變得有一點複雜了,我們接下來編譯上述代碼,來看看 Chrome Devtools 的調試效果如何。

通過在編譯時帶上 -g 標籤,告訴 Emscripten 編譯器帶上調試信息,並尋求 Emscripten 在編譯時注入 SDL2 庫以及允許庫在運行時可以使用任意內存大小:

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

     -s USE_SDL=\

     -s ALLOW_MEMORY_GROWTH=1

同樣使用 npx serve . 命令開啓一個本地的 Web 服務器,然後訪問 http://localhost:5000/mandelbrot.html 可以看到如下效果:

打開開發者工具,然後可以搜索到 mandelbrot.cc 文件,我們可以看到如下內容:

我們可以在第一個 for 循環裏面的 palette 賦值語句哪一行打一個斷點,然後重新刷新網頁,我們發現執行邏輯會暫停到我們的斷點處,通過查看右側的 Scope 面板,可以看到一些有意思的內容。

使用 Scope 面板

我們可以看到複雜類型如 centerpalette ,還可以展開它們,查看複雜類型裏面具體的值:

直接在程序中查看

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

在控制檯中使用

同時在控制檯裏面也可以通過輸入變量名獲取到值,依然可以查看複雜類型:

還可以對複雜類型進行取值、計算相關的操作:

使用 watch 功能

我們也可以把使用調試面板裏面的 watch 功能,添加 for 循環裏面的 i 到 watch 列表,然後恢復程序執行就可以看到 i 的變化:

更加複雜的步進調試

我們同樣可以使用另外幾個調試工具:step over、step in、step out、step 等,如我們使用 step over,向後執行兩步:

可以查看到當前步的變量值,也可以在 Scope 面板中看到對應的值。

針對非源碼編譯的第三方庫進行調試

在之前我們只編譯了 mandelbrot.cc 文件,並在編譯時要求 Emscripten 爲我們提供內建的 SDL 相關的庫,由於 SDL 庫並不是我們從源碼編譯而來,所以不會帶上調試相關的信息,所以我們僅僅在 mandelbrot.cc 裏面可以通過查看 C++ 代碼的形式來調試,而對於 SDL 相關的內容則只能查看 WebAssembly 相關的代碼來進行調試。

如我們在 41 行,SDL_SetRenderDrawColor 調用處打上斷點,並使用 step in 進入到函數內部:

會變成如下的形式:

我們又回到了原始的 WebAssembly 的調試形式,這也是難以避免的一種情況,因爲我們在開發過程中可能會遇到各種第三方庫,但是我們並不能保證每個庫都能從源碼編譯而來且帶上了類似 DWARF 的調試信息,絕大部分情況下我們無法控制第三方庫的行爲;而另外一種情況則是有時我們會在生產情況下遇到問題,而生產環境也是沒有調試信息的。

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

新的命名生成策略

之前的調試面板裏面,針對 WebAssembly 只有一些數字索引,而對於函數則連名字都沒有,如果沒有必要的類型信息,那麼很難追蹤到某個具體的值,因爲指針將以整數的形式展示出來,但你不知道這些整數背後存儲着什麼。

新的命名策略參考了其他反彙編工具的命名策略,使用了 WebAssembly 命名策略 [24] 部分的內容、import/export 的路徑相關的內容,可以看到我們現在的調試面板中針對函數可以展示函數名相關的信息:

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

查看內存面板

如果想要調試此時程序佔用的內存相關的內容,可以在 WebAssembly 的上下文下,查看 Scope 面板裏的 Module.memories.$env.memory ,但是這隻能看到一些獨立的字節,無法瞭解到這些字節對應到的其他數據格式,如 ASCII 格式。但是 Chrome 開發者工具還爲我們提供了一些其他更加強大的內存查看形式,當我們右鍵點擊 env.memory 時,可以選擇 Reveal in Memory Inspector panel:

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

可以打開內存面板:

從內存面板裏面可以查看以十六進制或 ASCII 的形式查看 WebAssembly 的內存,導航到特定的內存地址,將特定數據解析成各種不同的格式,如十六進制 65 代表的 e 這個 ASCII 字符。

對 WebAssembly 代碼進行性能分析

因爲我們在編譯時爲代碼注入了很多調試信息,運行的代碼是未經優化且冗長的代碼,所以運行時會很慢,所以如果爲了評估程序運行的性能,你不能使用 performance.now 或者 console.time 等 API,因爲這些函數調用獲得的性能相關的數字通常不能反應真實世界的效果。

所以如果需要對代碼進行性能分析,你需要使用開發者工具提供的性能面板,性能面板裏面會全速運行代碼,並且提供不同函數執行時花費時間的明確斷點信息:

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

或者你可以在加載網頁時關閉控制檯,這樣就不會涉及到調試信息等相關內容的調用,可以確保比較真實的效果,等到頁面加載完成,然後再打開控制檯查看相關的指標信息。

在不同的機器上進行調試

當在 Docker、虛擬機或者其他原創服務器上進行構建時,你可能會遇到那種構建時使用的源文件路徑和本地文件系統上的文件路徑不一致,這會導致開發者工具在運行時可以在 Sources 面板裏展示出有這個文件,但是無法加載文件內容。

爲了解決這個問題,我們需要在之前安裝的 C/C++ Devtools Support[25] 配置裏面設置路徑映射,點擊擴展的 “選項”:

然後添加路徑映射,在 old/path 裏填入之前的源文件構建時的路徑,在 new/path 裏填入現在存在本地文件系統上的文件路徑:

上述映射的功能和一些 C++ 的調試器如 GDB 的 set substitute-path 以及 LLDB 的 target.source-map 很像。這樣開發者工具在查找源文件時,會查看是否在配置的路徑映射裏有對應的映射,如果源路徑無法加載文件,那麼開發者工具會嘗試從映射路徑加載文件,否則會加載失敗。

調試優化性構建的代碼

如果你想調試一些在構建時進行優化後的代碼,可能會獲得不太理想的調試體驗,因爲進行優化構建時,函數內聯在一起,可能還會對代碼進行重排序或去除一部分無用的代碼,這些都可能會混淆調試者。

目前開發者工具除了對函數內聯時不能搞很好的支持外,能夠支持絕大部分優化後代碼的調試體驗,爲了減少函數內聯支持能力欠缺帶來的調試影響,建議在對代碼進行編譯時加入 -fno-inline 標誌來取消優化構建時(通常是帶上 -O 參數)對函數進行內聯處理的功能,未來開發者工具會修復這個問題。所以針對之前提到的簡單 C 程序的編譯腳本如下:

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

     -O3 -fno-inline

將調試信息單獨存儲

調試信息包含代碼的詳細信息,定義的類型、變量、函數、函數作用域、以及文件位置等任何有利於調試器使用的信息,所以通常調試信息比源代碼還要大。

爲了加速 WebAssembly 模塊的編譯和加載速度,你可以在編譯時將調試信息拆分成獨立的 WebAssembly 文件,然後單獨加載,爲了實現拆分單獨文件,可以在編譯時加入 -gseparate-dwarf 操作:

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

     -gseparate-dwarf=temp.debug.wasm

進行上述操作之後,編譯之後的主應用代碼只會存儲一個 temp.debug.wasm 的文件名,然後在代碼加載時,插件會定位到調試文件的位置並將其加載進開發者工具。

如果我們想同時進行優化構建,並將調試信息單獨拆分,並在之後需要調試時,加載本地的調試文件進行調試,在這種場景下,我們需要重載調試文件存儲的地址來幫助插件能夠找到這個文件,可以運行如下命令來處理:

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

     -O3 -fno-inline \

     -gseparate-dwarf=temp.debug.wasm \

     -s SEPARATE_DWARF_URL=file://[temp.debug.wasm 在本地文件系統的存儲地址]

在瀏覽器中調試 ffmpeg 代碼

通過這篇文章我們深入瞭解瞭如何在瀏覽器中調試通過 Emscripten 構建而來的 C/C++ 代碼,上述講解了一個普通無依賴的例子以及一個依賴於 C++ 標準庫 SDL 的例子,並且講解了現階段調試工具可以做的事情和限制,接下來我們就通過學到的知識來了解如何在瀏覽器中調試 ffmpeg 相關的代碼。

帶上調試信息的構建

我們只需要修改在之前的文章中提到的構建腳本 build-with-emcc.sh ,加入 -g 對應的標誌:

ROOT=$PWD

BUILD_DIR=$ROOT/build





cd ffmpeg-4.3.2-3





ARGS=(

  -g # 在這裏添加,告訴編譯器需要添加調試

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

然後以此執行其他操作,最後通過 node server.js 運行我們的腳本,然後打開 http://localhost:8080/ 查看效果如下:

可以看到,我們在 Sources 面板裏面可以搜索到構建後的 ffmpeg.c 文件,我們可以在 4865 行,在循環操作 nb_output 時打一個斷點:

然後在網頁中上傳一個 avi 格式的視頻,接着程序會暫停到斷點位置:

可以發現,我們依然可以像之前一樣在程序中鼠標移動上去查看變量值,以及在右側的 Scope 面板裏查看變量值,以及可以在控制檯中查看變量值。

類似的,我們也可以進行 step over、step in、step out、step 等複雜調試操作,或者 watch 某個變量值,或查看此時的內存等。

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

關於 WebAssembly 的未來

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

但是這篇文章沒有涉及到的內容有 WASI[26],一種將 WebAssembly 跑在任何系統上的標準化系統接口,當 WebAssembly 的性能逐漸增強時,WASI 可以提供一種確實可行的方式,可以在任意平臺上運行任意的代碼,就像 Docker 所做的一樣,但是不需要受限於操作系統。正如 Docker 的創始人所說:

“ 如果 WASM+WASI 在 2008 年就出現的話,那麼就不需要創造 Docker 了,服務器上的 WASM 是計算的未來,是我們期待已久的標準化的系統接口。

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

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

同時 WebAssembly 也是由 W3C 主要負責開發,各大廠商,包括 Microsoft、Google、Mozilla 等贊助和共同維護的一個項目,相信 WebAssembly 會有一個非常值得期待的未來。

Q & A

答疑...

問題:

參考鏈接

參考資料

[1]

WebAssembly 入門:如何和有 C 項目結合使用: 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]

文件系統 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

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/dEOIArtK6DIfewIva2zLKw