1. 為什么說 WebAssembly 是 Web 的未來?

        共 54406字,需瀏覽 109分鐘

         ·

        2022-01-27 02:27

        術(shù)????cb6fbbfbd744ca4f7753d846e01d919b.webp這篇文章打算講什么?

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

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

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

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

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

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

        動態(tài)語言之踵

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

        a5cb5c495c0ac2ca360222f192bbcbe7.webp

        上述是 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ù)組,這就會導致 JIT 失效,需要重新進行 Parser-Compiler-Interpreter-Execuation,而 Parser-Compiler 這兩步是整個代碼執(zhí)行過程中最耗費時間的兩步,這也是為什么 JavaScript 語言背景下,Web 無法執(zhí)行一些高性能應(yīng)用,如大型游戲、視頻剪輯等。

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

        通過上面的說明了解到,其實 JS 執(zhí)行慢的一個主要原因是因為其動態(tài)語言的特性,導致 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?a?=?1;

        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++ 等原生代碼。

        相當于下面的過程:

        5fb60276f623606271c7ac1cd778ee2f.webpWebAssembly 初探

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

        b8bd8414f649052fa74dbb50ebcbeae4.webp

        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 模塊導入了一個函數(shù) imported_func ,將其命名為 $i ,接收參數(shù) i32
        • 然后導出一個名為 exported_func 的函數(shù),可以從 Web App,如 JS 中導入這個函數(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é)果如下:

        c965162f5de5ec20b0ecbb1d373b4d92.webp

        可以看到,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ù)學操作

          • 內(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),例如導出一個模塊:

        //?env.ts

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

        導入一個模塊:

        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 形式的語法,遵循靜態(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)建的一個五子棋游戲。

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

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

        WebAssembly 的設(shè)計者顯然在設(shè)計上同時考慮到了各種完善的情況,既然 WebAssembly 是一種二進制格式,那么其就可以作為其他語言的編譯目標,如果能夠構(gòu)建一種編譯器,能夠?qū)⒁延械摹⒊墒斓?、且兼具海量的開發(fā)者和強大的生態(tài)的語言編譯到 WebAssembly 使用,那么相當于可以直接復(fù)用這個語言多年的積累,并用它們來完善 WebAssembly 生態(tài),將它們運行在 Web、Node.js 中。

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

        9c99a6dd4d13cc4f7dc978b1599b3717.webp

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

        cd320ac12df9a1f3936cf4431c78a2bf.webp

        即將 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 的作用更適合下面這張圖:

        cb6fbbfbd744ca4f7753d846e01d919b.webp

        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

        維護:活躍

        4e6464164774d2066fb8e02a0db68542.webp

        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 支持:

        3933a5f760c82f5cc842a2f33c4e1707.webp

        簡單體驗一下 “Hello World”

        值得一提的是,WebAssembly 相關(guān)工具鏈的安裝幾乎都是以源碼的形式提供,這可能和 C/C++ 生態(tài)的習慣不無關(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)境。

        580617c3b40ee12ca301e9433253281e.webp

        接下來我們嘗試一下將代碼運行在 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é)果:

        2989c8c140a3fb55066428f9ddc2ca70.webp

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

        5d7eb5b12fea25cdd4325352b0064e31.webp

        嘗試在 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)的代碼。

        我們需要導入 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']" 則表示導出一個運行時的函數(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ù):

        57e85dc0f623b4edb2feee26a227bdae.webp

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

        7558f6da28a33de10352b3b2c8c601c0.webp79ec258518654cda64abdd486a0a0268.webp

        可以看到首先進行 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é)果:

        1229bad47ffac3d78ebb6e3b176ddb2c.webp

        嘗試編譯已存在的 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ù),而我們是通過導入 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é)果:

        32b567cfc29f9c258773f2da11888fe8.webp

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

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

        5a54795fd8cb40d66545a9224e29a404.webp

        在 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>?//?此頭文件導入用于分配內(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>

        可以看到上述代碼除了導入之前添加的 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)存,你可能會遇到如下報錯:

        7e5d92130aa772cb9daf9cfd91ea8517.webp

        但是我們例子中使用的圖片比較小,所以只需要單純的在編譯時加上一個過濾參數(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)該可以看到如下界面:

        edebd7592247154bf47e473118ec04ea.webp

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

        4edcac752b39e58ca7510435a88cffee.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]" 導出 ffmpeg 對應(yīng)的 C 文件里的 main 函數(shù),proxy_main 則是通過設(shè)置 PROXY_TO_PTHREAD代理 main 函數(shù)用于外部使用
        4. -s EXTRA_EXPORTED_RUNTIME_METHODS="[FS, cwrap, setValue, writeAsciiToMemory]" 則是導出一些 runtime 的輔助函數(shù),用于導出 C 函數(shù)、處理文件系統(tǒng)、指針的操作

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

        • ffmpeg-core.js
        • ffmpeg-core.wasm
        • ffmpeg-core.worker.js

        使用編譯完成的 ffmpeg wasm 模塊

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

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



        Module.onRuntimeInitialized?=?()?=>?{

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

        };

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

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

        上述代碼解釋如下:

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

        • cwrap 則用于導出 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é)果如下:

        0aa4186127714e9afc878d14bd693e83.webp

        可以看到我們成功編譯并運行了 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é)果:

        23265917a9bf10c18158c4a348f035c1.webp

        在瀏覽器中使用 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 的文本格式的文件能進行單個指令的分析以及查看原始的堆棧追蹤,具體見如下圖:

        a4585b5dcace1ed279800edc234b9d21.webp

        上述的方法對于一些無其他依賴函數(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++ 代碼。

        8647fd8e492816d65b7a475d18b4c289.webp

        調(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 查看運行效果。

        5d1e953b7906c884d7ea931204216c83.webp

        需要確保已經(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)試選項
        1ad808cf8493514f7b4c76c3f7fcd8ee.webpd6ed7c726693f20db266ee3eb4d3be22.webp

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

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

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

        0cb3cb12b74d69c7976035ac2e4cafae.webp

        如上述可以查看 x 、y 值,將鼠標浮動到 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)存大?。?/p>

        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 可以看到如下效果:

        0027f5c033305be354456481603c26b4.webp

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

        d3b91c6fd0f7590d5e0d63e1b4e00c51.webp

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

        使用 Scope 面板

        我們可以看到復(fù)雜類型如 centerpalette ,還可以展開它們,查看復(fù)雜類型里面具體的值:

        0a165e9f9a44c86b1af10cf88b456f25.webp

        直接在程序中查看

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

        5ecb233cba9e869177e23eef931fc532.webp

        在控制臺中使用

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

        affb8931561c78c5c43369d5b0d6169b.webp

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

        aef0412fe3171cd73f7bdb838eb3acc9.webp

        使用 watch 功能

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

        557890eb897ff8f226ea795adc5d9616.webp

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

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

        0c6a2ac639090e1a4c37b575a9dadffe.webp

        可以查看到當前步的變量值,也可以在 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)部:

        e695e163ed56f12b14585cdaba7f2a36.webp

        會變成如下的形式:

        5ce84059332436b67aa645f471896efb.webp

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

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

        新的命名生成策略

        之前的調(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)的信息:

        3f24c1ebd105b4ff1cd5205e921ece69.webp

        即使遇到了程序錯誤,基于語句的類型和索引也可以生成類似 $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:

        a9e888ca977511a798c3c20feb35c49f.webp

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

        866502ea759005d0eb09505f078446ba.webp

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

        ca644d28f3ce017f9bfa6232f3588b80.webp

        從內(nèi)存面板里面可以查看以十六進制或 ASCII 的形式查看 WebAssembly 的內(nèi)存,導航到特定的內(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í)行時花費時間的明確斷點信息:

        f185440bc79190f6d1593be5325b750e.webp

        可以看到上述幾個比較典型的時間點如 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)上的文件路徑不一致,這會導致開發(fā)者工具在運行時可以在 Sources 面板里展示出有這個文件,但是無法加載文件內(nèi)容。

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

        7feea15ebace7d188a7af024a516f504.webp

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

        05f0f9e54610c37b8f7bf9fedb6c4ba3.webp

        上述映射的功能和一些 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)試工具可以做的事情和限制,接下來我們就通過學到的知識來了解如何在瀏覽器中調(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/ 查看效果如下:

        7cda9548c38e9bdb6381e7507b0dba9f.webp

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

        b68f6ed1efd72de088050098a71a9c1f.webp

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

        c4d9f8dd35f0e442fb991d68ec696728.webp17a737b0079395b1fc3a006db5f29c71.webp

        可以發(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 主要負責開發(fā),各大廠商,包括 Microsoft、Google、Mozilla 等贊助和共同維護的一個項目,相信 WebAssembly 會有一個非常值得期待的未來。

        3b53024b9aa5c7e79e4025fbf1481c2a.webpQ & A

        答疑...

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

        問題:

        • 編譯之后的代碼的體積
        參考鏈接
        • https://www.ruanyifeng.com/blog/2017/09/asmjs_emscripten.html
        • https://pspdfkit.com/blog/2017/webassembly-a-new-hope/
        • https://hacks.mozilla.org/2017/02/what-makes-webassembly-fast/
        • https://www.sitepoint.com/understanding-asm-js/
        • http://www.cmake.org/download/
        • https://developer.mozilla.org/en-US/docs/WebAssembly/existing_C_to_wasm
        • https://research.mozilla.org/webassembly/
        • https://itnext.io/build-ffmpeg-webassembly-version-ffmpeg-js-part-2-compile-with-emscripten-4c581e8c9a16?gi=e525b34f2c21
        • https://dev.to/alfg/ffmpeg-webassembly-2cbl
        • https://gist.github.com/rinthel/f4df3023245dd3e5a27218e8b3d79926
        • https://github.com/Kagami/ffmpeg.js/
        • https://qdmana.com/2021/04/20210401214625324n.html
        • https://github.com/leandromoreira/ffmpeg-libav-tutorial
        • http://ffmpeg.org/doxygen/4.1/examples.html
        • https://github.com/alfg/ffmpeg-webassembly-example
        • https://github.com/alfg/ffprobe-wasm
        • https://gist.github.com/rinthel/f4df3023245dd3e5a27218e8b3d79926#file-ffmpeg-emscripten-build-sh
        • https://emscripten.org/docs/compiling/Building-Projects.html#integrating-with-a-build-system
        • https://itnext.io/build-ffmpeg-webassembly-version-ffmpeg-js-part-2-compile-with-emscripten-4c581e8c9a16
        • https://github.com/mymindstorm/setup-emsdk
        • https://github.com/emscripten-core/emsdk
        • https://github.com/FFmpeg/FFmpeg/blob/n4.3.1/INSTALL.md
        • https://yeasy.gitbook.io/docker_practice/container/run
        • Debugging WebAssembly with modern tools - Chrome Developers[29]
        • https://www.infoq.com/news/2021/01/chrome-extension-debug-wasm-c/
        • https://developer.chrome.com/blog/wasm-debugging-2020/
        • https://lucumr.pocoo.org/2020/11/30/how-to-wasm-dwarf/
        • https://v8.dev/docs/wasm-compilation-pipeline
        • [Debugging WebAssembly with Chrome DevTools | by Charuka Herath | Bits and Pieces (bitsrc.io)](https://blog.bitsrc.io/debugging-webassembly-with-chrome-devtools-99dbad485451 "Debugging WebAssembly with Chrome DevTools | by Charuka Herath | Bits and Pieces (bitsrc.io "Debugging WebAssembly with Chrome DevTools | by Charuka Herath | Bits and Pieces (bitsrc.io)")")
        • Making Web Assembly Even Faster: Debugging Web Assembly Performance with AssemblyScript and a Gameboy Emulator | by Aaron Turner | Medium[30]
        • https://zhuanlan.zhihu.com/p/68048524
        • https://www.ruanyifeng.com/blog/2017/09/asmjs_emscripten.html
        • https://www.jianshu.com/p/e4a75cb6f268
        • https://www.cloudsavvyit.com/13696/why-webassembly-frameworks-are-the-future-of-the-web/
        • https://mp.weixin.qq.com/s/LSIi2P6FKnJ0GTodaTUGKw

        參考資料

        [1]

        WebAssembly 入門:如何和有 C 項目結(jié)合使用: https://bytedance.feishu.cn/docs/doccnmiuQS1dKSWaMwUABoHkxez

        [2]

        ArrayBuffer: https://es6.ruanyifeng.com/#docs/arraybuffer

        [3]

        文本格式: https://webassembly.github.io/spec/core/text/index.html

        [4]

        wabt: https://github.com/WebAssembly/wabt

        [5]

        wabt: https://github.com/WebAssembly/wabt

        [6]

        AssemblyScript: https://www.assemblyscript.org/

        [7]

        WebAssembly 類型: https://www.assemblyscript.org/types.html#type-rules

        [8]

        Binaryen: https://github.com/WebAssembly/binaryen

        [9]

        Emscripten: https://github.com/emscripten-core/emscripten

        [10]

        SDL: https://en.wikipedia.org/wiki/Simple_DirectMedia_Layer

        [11]

        OpenGL: https://en.wikipedia.org/wiki/OpenGL

        [12]

        OpenAL: https://en.wikipedia.org/wiki/OpenAL

        [13]

        POSIX: https://en.wikipedia.org/wiki/POSIX

        [14]

        Unreal Engine 4: https://blog.mozilla.org/blog/2014/03/12/mozilla-and-epic-preview-unreal-engine-4-running-in-firefox/

        [15]

        Unity: https://blogs.unity3d.com/2018/08/15/webassembly-is-here/

        [16]

        Github: https://github.com/webmproject/libwebp

        [17]

        API 文檔: https://developers.google.com/speed/webp/docs/api

        [18]

        WebP 的文檔: https://developers.google.com/speed/webp/docs/api#simple_encoding_api

        [19]

        文件系統(tǒng) API: https://emscripten.org/docs/api_reference/Filesystem-API.html

        [20]

        C/C++ Devtools Support: https://chrome.google.com/webstore/detail/cc++-devtools-support-dwa/pdcpmagijalfljmkmjngeonclgbbannb

        [21]

        C/C++ Devtools Support: https://chrome.google.com/webstore/detail/cc++-devtools-support-dwa/pdcpmagijalfljmkmjngeonclgbbannb

        [22]

        SDL: https://en.wikipedia.org/wiki/Simple_DirectMedia_Layer

        [23]

        complex numbers: https://en.cppreference.com/w/cpp/numeric/complex

        [24]

        WebAssembly 命名策略: https://webassembly.github.io/spec/core/appendix/custom.html#name-section

        [25]

        C/C++ Devtools Support: https://chrome.google.com/webstore/detail/cc%20%20-devtools-support-dwa/pdcpmagijalfljmkmjngeonclgbbannb

        [26]

        WASI: https://github.com/WebAssembly/WASI

        [27]

        yew: https://github.com/yewstack/yew

        [28]

        WAPM: https://wapm.io/

        [29]

        Debugging WebAssembly with modern tools - Chrome Developers: https://developer.chrome.com/blog/wasm-debugging-2020/

        [30]

        Making Web Assembly Even Faster: Debugging Web Assembly Performance with AssemblyScript and a Gameboy Emulator | by Aaron Turner | Medium: https://medium.com/@torch2424/making-web-assembly-even-faster-debugging-web-assembly-performance-with-assemblyscript-and-a-4d30cb6463f1

        ???

        便內(nèi),^_^

        ?、?~。

        關(guān)?前端Sharing?收獲~



        瀏覽 69
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
        評論
        圖片
        表情
        推薦
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
          
          

            1. 91香蕉视频污版 | 免费无码婬片AAAA片老婦 | 久久精品国产9久久综合日本欧 | 亚洲视频网免费在线 | 香港一级艳片 |