为什么说 WebAssembly 是 Web 的未来?
Posted ELab团队
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了为什么说 WebAssembly 是 Web 的未来?相关的知识,希望对你有一定的参考价值。
视图是 javascript 操作二进制数据的一个接口,以数组的语法处理二进制数据,统称为二进制数组。参考 。或 .wast
为扩展命名,然后通过 等工具,将文本格式下的 WASM 转为二进制格式的可执行代码,以 .wasm
为扩展的格式。来看一段 WASM 文本格式下的模块代码:
JS 模块导入了一个函数 imported_func
,将其命名为 $i
,接收参数 i32
然后导出一个名为 exported_func
的函数,可以从 Web App,如 JS 中导入这个函数使用 接着为参数 i32
传入 42,然后调用函数 $i
我们通过 wabt 将上述文本格式转为二进制代码:
将上述代码复制到一个新建的,名为 simple.wat
的文件中保存 使用 进行编译转换 当你安装好 wabt 之后,运行如下命令进行编译:
选项,让内容在命令行输出:走到台前,AssemblyScript 是 TypeScript 的一种变体,为 JavaScript 添加了 , 可以使用 将其编译成 WebAssembly。WebAssembly 类型大致如下:
i32、u32、i64、v128 等
小整数类型:i8、u8 等
变量整数类型:isize、usize 等
Binaryen 会前置将 AssemblyScript 静态编译成强类型的 WebAssembly 二进制,然后才会交给 JS 引擎去执行,所以说虽然 AssemblyScript 带来了一层抽象,但是实际用于生产的代码依然是 WebAssembly,保有 WebAssembly 的性能优势。AssemblyScript 被设计的和 TypeScript 非常相似,提供了一组内建的函数可以直接操作 WebAssembly 以及编译器的特性.
内建函数:
静态类型检查:
function isInteger<T>(value?: T): bool
等
实用函数:
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
等
数学操作
内存操作
控制流
SIMD
Atomics
Inline instructions
然后基于这套内建的函数向上构建一套标准库。
标准库:
Globals
Array
ArrayBuffer
DataView
Date
Error
Map
Math
Number
Set
String
Symbol
TypedArray
如一个典型的 Array 的使用如下:
这样优秀的编译器存在了。可以通过下面这张图直观的阐述 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++ 库,如 的一部分 API。
目前使用 WebAssembly 最大的场景也是这种将 C/C++ 模块编译到 WASM 的方式,比较有名的例子有 之类的大型库或应用。
中加入如下代码:目录,运行:和 a.out.wasm
,后者为编译之后的 wasm 代码,前者为 JS 胶水代码,提供了 WASM 运行的 runtime。可以使用 Node.js 进行快速测试:
,我们成功将 C/C++ 代码运行在了 Node.js 环境。接下来我们尝试一下将代码运行在 Web 环境,修改编译代码如下:
胶水代码main.wasm
WASM 代码main.html
加载胶水代码,执行 WASM 的一些逻辑Emscripten 生成代码有一定的规则,具体可以参考:https://emscripten.org/docs/compiling/Building-Projects.html#emscripten-linker-output-files
如果要在浏览器打开这个 HTML,需要在本地起一个服务器,因为单纯的打开通过 file://
协议访问时,主流浏览器不支持 XHR 请求,只有在 HTTP 服务器下,才能进行 XHR 请求,所以我们运行如下命令来打开网站:
文件,添加如下代码:函数,其他的代码会作为 “死代码” 在编译时被删掉,所以为了使用我们在上面定义的 myFunction
,我们需要在其定义之前加上 EMSCRIPTEN_KEEPALIVE
声明,确保在编译时不会删掉 myFunction
函数相关的代码。我们需要导入 emscripten/emscripten.h
头文件,才能使用 EMSCRIPTEN_KEEPALIVE
声明。
同时我们还需要对编译命令做一下改进如下:
表示在 main
函数运行完之后,程序不退出,依然保持可执行状态,方便后续可调用 myFunction
函数-s "EXTRA_EXPORTED_RUNTIME_METHODS=[\'ccall\']"
则表示导出一个运行时的函数 ccall
,这个函数可以在 JS 中调用 C 程序的函数进行编译之后,我们还需要修改生成的 function.html
文件,加入我们的函数调用逻辑如下:
事件,在回调函数里,我们调用了 myFunction
函数。在命令行中运行 npx serve .
打开浏览器访问 http://localhost:3000/function.html,查看结果如下:
只执行 main
函数:
尝试点击按钮执行 myFunction
函数:
可以看到首先进行 alert 弹框展示,然后打开控制台,可以看到 myFunction
的调用结果,打印 "MyFunction Called"
。
、fclose
来访问你文件系统,但是 JS 是运行在浏览器提供的沙盒环境里,无法直接访问到本地文件系统。所以为了兼容 C/C++ 程序访问文件系统,编译为 WASM 之后依然能够正常运行,Emscripten 会在其 JS 胶水代码里面模拟一个文件系统,并提供和 libc stdio 一致的 API。让我们重新创建一个名为 file.c
的程序,添加如下代码:
访问 file.txt
,然后一行一行的读取文件内容,如果程序执行过程中有任何的出错,就会打印错误。我们在目录下新建 file.txt
文件,并加入如下内容:
参数,提前将文件内容加载进 Emscripten runtime,因为在 C/C++ 等程序上访问文件都是同步操作,而 JS 是基于事件模型的异步操作,且在 Web 中只能通过 XHR 的形式去访问文件(Web Worker、Node.js 可同步访问文件),所以需要提前将文件加载好,确保在代码编译之前,文件已经准备好了,这样 C/C++ 代码可以直接访问到文件。运行如下命令进行代码编译:
,依然是确保 main
逻辑执行完之后,程序不会退出。然后运行我们的本地服务器,访问 http://localhost:3000/file.html,可以查看结果:
上找到它,同时可以了解到它的一些 获取版本的函数,测试版本是否可以正确获取。我们在目录下创建 webp.c
文件,添加如下内容:
就是 libwebp 里面获取当前版本的函数,而我们是通过导入 src/webp/encode.h
头文件来获取这个函数的,为了让编译器在编译时能够找到这个头文件,我们需要在编译的时候将 libwebp 库的头文件地址告诉编译器,并将编译器需要的所有 libwebp 库下的 C 文件传给编译器。让我们运行如下编译命令:
将 libwebp 库的头文件地址告诉编译器libwebp/src/dec,dsp,demux,enc,mux,utils/*.c
将编译器所需的 C 文件传给编译器,这里将 dec,dsp,demux,enc,mux,utils
等目录下的所有 C 文件都传递给了编译器,避免了一个个列出所需文件的繁琐,然后让编译器去自动识别那些没有使用的文件,并将其过滤掉webp.c
是我们编写的 C 函数,用于调用 WebPGetEncoderVersion
获取库版本-O3
代表在编译时进行等级为 3 的优化,包含内联函数、去除无用代码、对代码进行各种压缩优化等而 -s WASM=1
其实是默认的,就是在编译时输出 xx.out.wasm
,这里之所以会设置这个选项主要是针对那些不支持 WASM 的 runtime,可以设置 -s WASM=0
,输出等价的 JS 代码替代 WASM EXTRA_EXPORTED_RUNTIME_METHODS= \'["cwrap"]\'
则是输出 runtime 的函数 cwrap
,类似 ccall
可以在 JS 中调用 C 函数上述的编译输出只有 a.out.js
和 a.out.wasm
,我们还需要建一份 HTML 文档来使用输出的脚本代码,新建 webp.html
,添加如下内容:
的回调里面去执行我们 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。
方法来获取版本号来证实了已经成功编译了 libwebp 库到 wasm,然后可以在 JavaScript 使用它,接下来我们将了解更加复杂的操作,如何使用 libwebp 的编码 API 来转换图片格式。libwebp 的 encoding API 需要接收一个关于 RGB、RGBA、BGR 或 BGRA 的字节数组,幸运的是,Canvas API 有一个 CanvasRenderingContext2D.getImageData
方法,能够返回一个 Uint8ClampedArray
,这个数组包含 RGBA 格式的图片数据。
首先我们需要在 JavaScript 中编写加载图片的函数,将其写到上一步创建的 HTML 文件里:
函数里面暴露额外的方法:一个为 wasm 里面的图片分配内存的方法 一个释放内存的方法 修改 webp.c
如下:
为 RGBA 的图片分配内存,RGBA 图片一个像素包含 4 个字节,所以代码中需要添加 4 * sizeof(uint8_t)
,malloc
函数返回的指针指向所分配内存的第一块内存单元地址,当这个指针返回给 JavaScript 使用时,会被当做一个简单的数字处理。当通过 cwrap
函数获取暴露给 JavaScript 的对应 C 函数时,可以使用这个指针数字找到复制图片数据的内存开始位置。我们在 HTML 文件中添加额外的代码如下:
和 destroy_buffer
外,还有很多用于编码文件等方面的函数,我们将在后续讲解,除此之外,代码首先加载了一份 image.jpg
的图片,然后调用 C 函数为此图片数据分配内存,并相应的拿到返回的指针传给 WebAssembly 的 Module.HEAP8
,在内存开始位置 p,写入图片的数据,最后会释放分配的内存。函数来完成工作。这个函数接收一个指向图片数据的指针以及它的尺寸,以及每次需要跨越的 stride
步长,这里为 4 个字节(RGBA),一个区间在 0-100 的可选的质量参数。在编码的过程中,WebPEncodeRGBA
会分配一块用于输出数据的内存,我们需要在编码完成之后调用 WebPFree
来释放这块内存。我们打开 webp.c
文件,添加如下处理编码的代码:
函数执行的结果为分配一块输出数据的内存以及返回内存的大小。因为 C 函数无法使用数组作为返回值(除非我们需要进行动态内存分配),所以我们使用一个全局静态数组来获取返回的结果,这可能不是很规范的 C 代码写法,同时它要求 wasm 指针为 32 比特长,但是为了简单起见我们可以暂时容忍这种做法。现在 C 侧的相关逻辑已经编写完毕,可以在 JavaScript 侧调用编码函数,获取图片数据的指针和图片所占用的内存大小,将这份数据保存到 WASM 的缓冲中,然后释放 wasm 在处理图片时所分配的内存,让我们打开 HTML 文件完成上述描述的逻辑:
函数加载了一张本地的 image.jpg
图片,你需要事先准备一张图片放置在 emcc
编译器输出的目录下,也就是我们的 HTML 文件目录下使用。注意:new Uint8Array(someBuffer)
将会在同样的内存块上创建一个新视图,而 new Uint8Array(someTypedArray)
只会复制 someTypedArray
的数据,确保使用复制的数据进行操作,不会修改原内存数据。
当你的图片比较大时,因为 wasm 不能自动扩充内存,如果默认分配的内存无法容纳 input
和 output
图片数据的内存,你可能会遇到如下报错:
但是我们例子中使用的图片比较小,所以只需要单纯的在编译时加上一个过滤参数 -s ALLOW_MEMORY_GROWTH=1
忽略这个报错信息即可:
和 emmake
来封装这些命令,并注入合适的参数来抹平那些有前置依赖的项目,如果使用 emcc 来处理这些有大量前置依赖的项目,命令会变成如下操作:运行项目的 configure
文件将 C/C++ 代码编译器从 gcc/g++
换成 emcc/em++
通过 emmake make
来构建 C/C++ 项目,生成 wasm 对象的 .o
文件 调用 emcc
接收编译的对象文件 .o
文件,然后输出最终的 WASM 和 JS 胶水代码 版本的 Emscripten 编译器,进入之前我们 Clone 到本地的 emsdk 项目运行如下命令:的 ffmpeg 代码:文件:开启 pthreads
支持-O3
表示在编译时优化代码体积,一般可以从 30MB 压缩到 15MBINITIAL_MEMORY
设置为 33554432 (32MB),主要是 Emscripten 可能占用 19MB,所以设置更大的内存容量来避免在编译过程中可分配的内存不足的问题实际使用 emconfigure
来配置 configure
文件,替换 gcc
编译器为 emcc
,以及设置一些必要的操作来处理可能遇到的编译 BUG,最终生成用于编译构建的配置文件 命令来进行处理。目录下创建 wasm
文件夹,用于放置构建之后的文件,然后自定义编译文件输出如下:在编译时设置了 pthread
时,使得程序具备响应式特效-o wasm/dist/ffmpeg-core.js
则将原 ffmpeg
js 文件的输出重命名为 ffmpeg-core.js
,对应的输出 ffmpeg-core.wasm
和 ffmpeg-core.worker.js
-s EXPORTED_FUNCTIONS="[_main, _proxy_main]"
导出 ffmpeg 对应的 C 文件里的 main
函数,proxy_main
则是通过设置 PROXY_TO_PTHREAD
代理 main
函数用于外部使用-s EXTRA_EXPORTED_RUNTIME_METHODS="[FS, cwrap, setValue, writeAsciiToMemory]"
则是导出一些 runtime 的辅助函数,用于导出 C 函数、处理文件系统、指针的操作通过上述编译命令最终输出下面三个文件:
ffmpeg-core.js
ffmpeg-core.wasm
ffmpeg-core.worker.js
目录下创建 ffmpeg.js
文件,在其中写入如下代码:是加载 WebAssembly 模块完成之后执行的逻辑,我们所有相关逻辑需要在这个函数中编写cwrap
则用于导出 C 文件中(fftools/ffmpeg.c
)的 proxy_main
使用,函数的签名为 int main(int argc, char **argv)
,其中 int
对应到 JavaScript 就是 number
,argc 表示参数的个数 ,而 char **argv
是 C 中的指针,表示实际参数的指针数组,也可以映射到 number
接着处理 ffmpeg
的传参兼容逻辑,对于命令行中运行 ffmpeg -hide_banner
,在我们代码里通过函数调用需要 main(2, ["./ffmpeg", "-hide_banner"])
,第一个参数很好解决,那么我们如何传递一个字符串数组呢?这个问题可以分解为两个部分:
我们需要将 JavaScript 的字符串转换成 C 中的字符数组 我们需要将 JavaScript 中的数组转换为 C 中的指针数组 第一部分很简单,因为 Emscripten 提供了一个辅助函数 writeAsciiToMemory
来完成这一工作:
来帮助我们创建这个数组:交互的程序:。为了完成上述的任务,只需要使用到 FS 模块的两个函数 FS.writeFile()
和 FS.readFile()
,对于从文件系统中读取和写入的所有数据都要求是 JavaScript 中的 Uint8Array 类型,所以在消费数据之前有必要约定数据类型。
我们将通过 fs.readFileSync()
方法读取名为 flame.avi
的视频文件,然后使用 FS.writeFile()
将其写入到 Emscripten 文件系统。
方法从 Emscripten 文件系统中读取转码好的视频文件,然后通过 fs.writeFileSync()
将视频写入到本地文件系统。最终我们会收到如下结果:格式到 mp4
格式的转码,接下来我们将在浏览器中使用 ffmpeg 转码视频,并在浏览器中播放。之前我们编译的 ffmpeg 虽然可以将 avi
格式转码到 mp4
,但是这种通过默认编码格式转码的 mp4
的文件无法直接在浏览器中播放,因为浏览器不支持这种编码,所以我们需要使用 libx264
编码器来将 mp4
文件编码成浏览器可播放的编码格式。
首先在 WebAssembly
目录下下载 x264
的编码器源码:
文件,并加入如下内容:编码器之后,就可以在 ffmpeg 的编译脚本中加入打开 x264
的开关,这一次我们在 ffmpeg
文件夹下创建 Bash 脚本用于构建,创建 build.sh
如下::文件夹下创建 index.html
文件,然后添加如下内容:浏览器扩展,就可以使用 Chrome 开发者工具调试 C/C++ 代码了。这里的原理其实就是,Emscripten 在编译时,会生成一种 DWARF 格式的调试文件,这是一种被大多数编译器使用的通用调试文件格式,而 则会解析 DWARF 文件,为 Chrome Devtools 在调试时提供 source map 相关的信息,使得开发者可以在 89+ 版本以上的 Chrome Devtools 上调试 C/C++ 代码。
文件夹,然后创建 temp.c
文件,填充如下内容并保存:时,如果遇到 x >= y
的情况会抛出异常,终止程序执行。在终端切换目录到 temp
目录下执行 emcc
命令进行编译:
参数,告诉 Emscripten 在编译时为代码注入 DWARF 调试信息。现在可以开启一个 HTTP 服务器,可以使用 npx serve .
,然后访问 localhost:5000/temp.html
查看运行效果。
需要确保已经安装了 Chrome 扩展:https://chrome.google.com/webstore/detail/cc++-devtools-support-dwa/pdcpmagijalfljmkmjngeonclgbbannb,以及 Chrome Devtools 升级到 89+ 版本。
为了查看调试效果,需要设置一些内容。
打开 Chrome Devtools 里面的 WebAssembly 调试选项
设置完之后,在工具栏顶部会出现一个 Reload 的蓝色按钮,需要重新加载配置,点击一下就好。
设置调试选项,在遇到异常的地方暂停
刷新浏览器,然后你会发现断点停在了 temp.js
,由 Emscripten 编译生成的 JS 胶水代码,然后顺着调用栈去找,可以查看到 temp.c
并定位到抛出异常的位置:
可以看到,我们成功在 Chrome Devtools 里面查看了 C 代码,并且代码停在了 abort()
处,同时还可以类似我们调试 JS 时一样,查看当前 scope 下的值:
如上述可以查看 x
、y
值,将鼠标浮动到 x
上还可以显示此时的值。
文件夹,然后添加 mandelbrot.cc
文件,并填入如下内容:和 ,这使得我们的代码变得有一点复杂了,我们接下来编译上述代码,来看看 Chrome Devtools 的调试效果如何。通过在编译时带上 -g
标签,告诉 Emscripten 编译器带上调试信息,并寻求 Emscripten 在编译时注入 SDL2 库以及允许库在运行时可以使用任意内存大小:
命令开启一个本地的 Web 服务器,然后访问 http://localhost:5000/mandelbrot.html 可以看到如下效果:打开开发者工具,然后可以搜索到 mandelbrot.cc
文件,我们可以看到如下内容:
我们可以在第一个 for 循环里面的 palette
赋值语句哪一行打一个断点,然后重新刷新网页,我们发现执行逻辑会暂停到我们的断点处,通过查看右侧的 Scope 面板,可以看到一些有意思的内容。
、palette
,还可以展开它们,查看复杂类型里面具体的值:等变量上面,同样可以查看值的类型:文件,并在编译时要求 Emscripten 为我们提供内建的 SDL 相关的库,由于 SDL 库并不是我们从源码编译而来,所以不会带上调试相关的信息,所以我们仅仅在 mandelbrot.cc
里面可以通过查看 C++ 代码的形式来调试,而对于 SDL 相关的内容则只能查看 WebAssembly 相关的代码来进行调试。如我们在 41 行,SDL_SetRenderDrawColor 调用处打上断点,并使用 step in 进入到函数内部:
会变成如下的形式:
我们又回到了原始的 WebAssembly 的调试形式,这也是难以避免的一种情况,因为我们在开发过程中可能会遇到各种第三方库,但是我们并不能保证每个库都能从源码编译而来且带上了类似 DWARF 的调试信息,绝大部分情况下我们无法控制第三方库的行为;而另外一种情况则是有时我们会在生产情况下遇到问题,而生产环境也是没有调试信息的。
上述情况暂时还没有比较好的处理方法,但是开发者工具却改进了上述的调试体验,将所有的代码都打包成单一的 WebAssembly 文件,对应到我们这次就是 mandelbrot.wasm
文件,这样我们再也无需担心其中的某段代码到底来自哪个源文件。
这样的名字,大大提高了栈追踪和反汇编的体验。,但是这只能看到一些独立的字节,无法了解到这些字节对应到的其他数据格式,如 ASCII 格式。但是 Chrome 开发者工具还为我们提供了一些其他更加强大的内存查看形式,当我们右键点击 env.memory
时,可以选择 Reveal in Memory Inspector panel:或者点击 env.memory
旁边的小图标:
可以打开内存面板:
从内存面板里面可以查看以十六进制或 ASCII 的形式查看 WebAssembly 的内存,导航到特定的内存地址,将特定数据解析成各种不同的格式,如十六进制 65 代表的 e 这个 ASCII 字符。
或者 console.time
等 API,因为这些函数调用获得的性能相关的数字通常不能反应真实世界的效果。所以如果需要对代码进行性能分析,你需要使用开发者工具提供的性能面板,性能面板里面会全速运行代码,并且提供不同函数执行时花费时间的明确断点信息:
可以看到上述几个比较典型的时间点如 161ms,或者 461ms 的 LCP 与 FCP ,这些都是能反应真实世界下的性能指标。
或者你可以在加载网页时关闭控制台,这样就不会涉及到调试信息等相关内容的调用,可以确保比较真实的效果,等到页面加载完成,然后再打开控制台查看相关的指标信息。
配置里面设置路径映射,点击扩展的 “选项”:然后添加路径映射,在 old/path 里填入之前的源文件构建时的路径,在 new/path 里填入现在存在本地文件系统上的文件路径:
上述映射的功能和一些 C++ 的调试器如 GDB 的 set substitute-path
以及 LLDB 的 target.source-map
很像。这样开发者工具在查找源文件时,会查看是否在配置的路径映射里有对应的映射,如果源路径无法加载文件,那么开发者工具会尝试从映射路径加载文件,否则会加载失败。
标志来取消优化构建时(通常是带上 -O
参数)对函数进行内联处理的功能,未来开发者工具会修复这个问题。所以针对之前提到的简单 C 程序的编译脚本如下:操作:的文件名,然后在代码加载时,插件会定位到调试文件的位置并将其加载进开发者工具。如果我们想同时进行优化构建,并将调试信息单独拆分,并在之后需要调试时,加载本地的调试文件进行调试,在这种场景下,我们需要重载调试文件存储的地址来帮助插件能够找到这个文件,可以运行如下命令来处理:
,加入 -g
对应的标志:运行我们的脚本,然后打开 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
答疑...
如何将复杂的 CMake 项目编译到 WebAssembly? 在编译复杂的 CMake 项目到 WebAssembly 时如何探索一套通用的最佳实践? 如何和 CMake 项目结合起来进行 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 项目结合使用: 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
❤️ 谢谢支持
以上便是本次分享的全部内容,希望对你有所帮助^_^
喜欢的话别忘了 分享、点赞、收藏 三连哦~。
欢迎关注公众号 ELab团队 收获大厂一手好文章~
我们来自字节跳动,是旗下大力教育前端部门,负责字节跳动教育全线产品前端开发工作。
我们围绕产品品质提升、开发效率、创意与前沿技术等方向沉淀与传播专业知识及案例,为业界贡献经验价值。包括但不限于性能监控、组件库、多端技术、Serverless、可视化搭建、音视频、人工智能、产品设计Blazor WebAssembly + Grpc Web = 未来?
Blazor WebAssembly是什么 首先来说说WebAssembly是什么,WebAssembly是一个可以使C#,Java,Golang等静态强类型编程语言,运行在浏览器中的标准,浏览器厂商基于此标准实现执行引擎。 在实现了WebAssembly标准引擎之后,浏览器中可以执行由其他语言编译
以上是关于为什么说 WebAssembly 是 Web 的未来?的主要内容,如果未能解决你的问题,请参考以下文章
Blazor WebAssembly + Grpc Web = 未来?
为什么说 WebAssembly 属于浏览器之外? Why WebAssembly Belongs Outside the Browser
为什么说 WebAssembly 属于浏览器之外? Why WebAssembly Belongs Outside the Browser
.wast
为扩展命名,然后通过 等工具,将文本格式下的 WASM 转为二进制格式的可执行代码,以 .wasm
为扩展的格式。来看一段 WASM 文本格式下的模块代码:
JS 模块导入了一个函数 imported_func
,将其命名为 $i
,接收参数 i32
然后导出一个名为 exported_func
的函数,可以从 Web App,如 JS 中导入这个函数使用 接着为参数 i32
传入 42,然后调用函数 $i
我们通过 wabt 将上述文本格式转为二进制代码:
将上述代码复制到一个新建的,名为 simple.wat
的文件中保存 使用 进行编译转换 当你安装好 wabt 之后,运行如下命令进行编译:
选项,让内容在命令行输出:走到台前,AssemblyScript 是 TypeScript 的一种变体,为 JavaScript 添加了 , 可以使用 将其编译成 WebAssembly。WebAssembly 类型大致如下:
i32、u32、i64、v128 等
小整数类型:i8、u8 等
变量整数类型:isize、usize 等
Binaryen 会前置将 AssemblyScript 静态编译成强类型的 WebAssembly 二进制,然后才会交给 JS 引擎去执行,所以说虽然 AssemblyScript 带来了一层抽象,但是实际用于生产的代码依然是 WebAssembly,保有 WebAssembly 的性能优势。AssemblyScript 被设计的和 TypeScript 非常相似,提供了一组内建的函数可以直接操作 WebAssembly 以及编译器的特性.
内建函数:
静态类型检查:
function isInteger<T>(value?: T): bool
等
实用函数:
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
等
数学操作
内存操作
控制流
SIMD
Atomics
Inline instructions
然后基于这套内建的函数向上构建一套标准库。
标准库:
Globals
Array
ArrayBuffer
DataView
Date
Error
Map
Math
Number
Set
String
Symbol
TypedArray
如一个典型的 Array 的使用如下:
这样优秀的编译器存在了。可以通过下面这张图直观的阐述 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++ 库,如 的一部分 API。
目前使用 WebAssembly 最大的场景也是这种将 C/C++ 模块编译到 WASM 的方式,比较有名的例子有 之类的大型库或应用。
中加入如下代码:目录,运行:和 a.out.wasm
,后者为编译之后的 wasm 代码,前者为 JS 胶水代码,提供了 WASM 运行的 runtime。可以使用 Node.js 进行快速测试:
,我们成功将 C/C++ 代码运行在了 Node.js 环境。接下来我们尝试一下将代码运行在 Web 环境,修改编译代码如下:
胶水代码main.wasm
WASM 代码main.html
加载胶水代码,执行 WASM 的一些逻辑Emscripten 生成代码有一定的规则,具体可以参考:https://emscripten.org/docs/compiling/Building-Projects.html#emscripten-linker-output-files
如果要在浏览器打开这个 HTML,需要在本地起一个服务器,因为单纯的打开通过 file://
协议访问时,主流浏览器不支持 XHR 请求,只有在 HTTP 服务器下,才能进行 XHR 请求,所以我们运行如下命令来打开网站:
文件,添加如下代码:函数,其他的代码会作为 “死代码” 在编译时被删掉,所以为了使用我们在上面定义的 myFunction
,我们需要在其定义之前加上 EMSCRIPTEN_KEEPALIVE
声明,确保在编译时不会删掉 myFunction
函数相关的代码。我们需要导入 emscripten/emscripten.h
头文件,才能使用 EMSCRIPTEN_KEEPALIVE
声明。
同时我们还需要对编译命令做一下改进如下:
表示在 main
函数运行完之后,程序不退出,依然保持可执行状态,方便后续可调用 myFunction
函数-s "EXTRA_EXPORTED_RUNTIME_METHODS=[\'ccall\']"
则表示导出一个运行时的函数 ccall
,这个函数可以在 JS 中调用 C 程序的函数进行编译之后,我们还需要修改生成的 function.html
文件,加入我们的函数调用逻辑如下:
事件,在回调函数里,我们调用了 myFunction
函数。在命令行中运行 npx serve .
打开浏览器访问 http://localhost:3000/function.html,查看结果如下:
只执行 main
函数:
尝试点击按钮执行 myFunction
函数:
可以看到首先进行 alert 弹框展示,然后打开控制台,可以看到 myFunction
的调用结果,打印 "MyFunction Called"
。
、fclose
来访问你文件系统,但是 JS 是运行在浏览器提供的沙盒环境里,无法直接访问到本地文件系统。所以为了兼容 C/C++ 程序访问文件系统,编译为 WASM 之后依然能够正常运行,Emscripten 会在其 JS 胶水代码里面模拟一个文件系统,并提供和 libc stdio 一致的 API。让我们重新创建一个名为 file.c
的程序,添加如下代码:
访问 file.txt
,然后一行一行的读取文件内容,如果程序执行过程中有任何的出错,就会打印错误。我们在目录下新建 file.txt
文件,并加入如下内容:
参数,提前将文件内容加载进 Emscripten runtime,因为在 C/C++ 等程序上访问文件都是同步操作,而 JS 是基于事件模型的异步操作,且在 Web 中只能通过 XHR 的形式去访问文件(Web Worker、Node.js 可同步访问文件),所以需要提前将文件加载好,确保在代码编译之前,文件已经准备好了,这样 C/C++ 代码可以直接访问到文件。运行如下命令进行代码编译:
,依然是确保 main
逻辑执行完之后,程序不会退出。然后运行我们的本地服务器,访问 http://localhost:3000/file.html,可以查看结果:
上找到它,同时可以了解到它的一些 获取版本的函数,测试版本是否可以正确获取。我们在目录下创建 webp.c
文件,添加如下内容:
就是 libwebp 里面获取当前版本的函数,而我们是通过导入 src/webp/encode.h
头文件来获取这个函数的,为了让编译器在编译时能够找到这个头文件,我们需要在编译的时候将 libwebp 库的头文件地址告诉编译器,并将编译器需要的所有 libwebp 库下的 C 文件传给编译器。让我们运行如下编译命令:
将 libwebp 库的头文件地址告诉编译器libwebp/src/dec,dsp,demux,enc,mux,utils/*.c
将编译器所需的 C 文件传给编译器,这里将 dec,dsp,demux,enc,mux,utils
等目录下的所有 C 文件都传递给了编译器,避免了一个个列出所需文件的繁琐,然后让编译器去自动识别那些没有使用的文件,并将其过滤掉webp.c
是我们编写的 C 函数,用于调用 WebPGetEncoderVersion
获取库版本-O3
代表在编译时进行等级为 3 的优化,包含内联函数、去除无用代码、对代码进行各种压缩优化等而 -s WASM=1
其实是默认的,就是在编译时输出 xx.out.wasm
,这里之所以会设置这个选项主要是针对那些不支持 WASM 的 runtime,可以设置 -s WASM=0
,输出等价的 JS 代码替代 WASM EXTRA_EXPORTED_RUNTIME_METHODS= \'["cwrap"]\'
则是输出 runtime 的函数 cwrap
,类似 ccall
可以在 JS 中调用 C 函数上述的编译输出只有 a.out.js
和 a.out.wasm
,我们还需要建一份 HTML 文档来使用输出的脚本代码,新建 webp.html
,添加如下内容:
的回调里面去执行我们 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。
方法来获取版本号来证实了已经成功编译了 libwebp 库到 wasm,然后可以在 JavaScript 使用它,接下来我们将了解更加复杂的操作,如何使用 libwebp 的编码 API 来转换图片格式。libwebp 的 encoding API 需要接收一个关于 RGB、RGBA、BGR 或 BGRA 的字节数组,幸运的是,Canvas API 有一个 CanvasRenderingContext2D.getImageData
方法,能够返回一个 Uint8ClampedArray
,这个数组包含 RGBA 格式的图片数据。
首先我们需要在 JavaScript 中编写加载图片的函数,将其写到上一步创建的 HTML 文件里:
函数里面暴露额外的方法:一个为 wasm 里面的图片分配内存的方法 一个释放内存的方法 修改 webp.c
如下:
为 RGBA 的图片分配内存,RGBA 图片一个像素包含 4 个字节,所以代码中需要添加 4 * sizeof(uint8_t)
,malloc
函数返回的指针指向所分配内存的第一块内存单元地址,当这个指针返回给 JavaScript 使用时,会被当做一个简单的数字处理。当通过 cwrap
函数获取暴露给 JavaScript 的对应 C 函数时,可以使用这个指针数字找到复制图片数据的内存开始位置。我们在 HTML 文件中添加额外的代码如下:
和 destroy_buffer
外,还有很多用于编码文件等方面的函数,我们将在后续讲解,除此之外,代码首先加载了一份 image.jpg
的图片,然后调用 C 函数为此图片数据分配内存,并相应的拿到返回的指针传给 WebAssembly 的 Module.HEAP8
,在内存开始位置 p,写入图片的数据,最后会释放分配的内存。函数来完成工作。这个函数接收一个指向图片数据的指针以及它的尺寸,以及每次需要跨越的 stride
步长,这里为 4 个字节(RGBA),一个区间在 0-100 的可选的质量参数。在编码的过程中,WebPEncodeRGBA
会分配一块用于输出数据的内存,我们需要在编码完成之后调用 WebPFree
来释放这块内存。我们打开 webp.c
文件,添加如下处理编码的代码:
函数执行的结果为分配一块输出数据的内存以及返回内存的大小。因为 C 函数无法使用数组作为返回值(除非我们需要进行动态内存分配),所以我们使用一个全局静态数组来获取返回的结果,这可能不是很规范的 C 代码写法,同时它要求 wasm 指针为 32 比特长,但是为了简单起见我们可以暂时容忍这种做法。现在 C 侧的相关逻辑已经编写完毕,可以在 JavaScript 侧调用编码函数,获取图片数据的指针和图片所占用的内存大小,将这份数据保存到 WASM 的缓冲中,然后释放 wasm 在处理图片时所分配的内存,让我们打开 HTML 文件完成上述描述的逻辑:
函数加载了一张本地的 image.jpg
图片,你需要事先准备一张图片放置在 emcc
编译器输出的目录下,也就是我们的 HTML 文件目录下使用。注意:new Uint8Array(someBuffer)
将会在同样的内存块上创建一个新视图,而 new Uint8Array(someTypedArray)
只会复制 someTypedArray
的数据,确保使用复制的数据进行操作,不会修改原内存数据。
当你的图片比较大时,因为 wasm 不能自动扩充内存,如果默认分配的内存无法容纳 input
和 output
图片数据的内存,你可能会遇到如下报错:
但是我们例子中使用的图片比较小,所以只需要单纯的在编译时加上一个过滤参数 -s ALLOW_MEMORY_GROWTH=1
忽略这个报错信息即可:
和 emmake
来封装这些命令,并注入合适的参数来抹平那些有前置依赖的项目,如果使用 emcc 来处理这些有大量前置依赖的项目,命令会变成如下操作:运行项目的 configure
文件将 C/C++ 代码编译器从 gcc/g++
换成 emcc/em++
通过 emmake make
来构建 C/C++ 项目,生成 wasm 对象的 .o
文件 调用 emcc
接收编译的对象文件 .o
文件,然后输出最终的 WASM 和 JS 胶水代码 版本的 Emscripten 编译器,进入之前我们 Clone 到本地的 emsdk 项目运行如下命令:的 ffmpeg 代码:文件:开启 pthreads
支持-O3
表示在编译时优化代码体积,一般可以从 30MB 压缩到 15MBINITIAL_MEMORY
设置为 33554432 (32MB),主要是 Emscripten 可能占用 19MB,所以设置更大的内存容量来避免在编译过程中可分配的内存不足的问题实际使用 emconfigure
来配置 configure
文件,替换 gcc
编译器为 emcc
,以及设置一些必要的操作来处理可能遇到的编译 BUG,最终生成用于编译构建的配置文件 命令来进行处理。目录下创建 wasm
文件夹,用于放置构建之后的文件,然后自定义编译文件输出如下:在编译时设置了 pthread
时,使得程序具备响应式特效-o wasm/dist/ffmpeg-core.js
则将原 ffmpeg
js 文件的输出重命名为 ffmpeg-core.js
,对应的输出 ffmpeg-core.wasm
和 ffmpeg-core.worker.js
-s EXPORTED_FUNCTIONS="[_main, _proxy_main]"
导出 ffmpeg 对应的 C 文件里的 main
函数,proxy_main
则是通过设置 PROXY_TO_PTHREAD
代理 main
函数用于外部使用-s EXTRA_EXPORTED_RUNTIME_METHODS="[FS, cwrap, setValue, writeAsciiToMemory]"
则是导出一些 runtime 的辅助函数,用于导出 C 函数、处理文件系统、指针的操作通过上述编译命令最终输出下面三个文件:
ffmpeg-core.js
ffmpeg-core.wasm
ffmpeg-core.worker.js
目录下创建 ffmpeg.js
文件,在其中写入如下代码:是加载 WebAssembly 模块完成之后执行的逻辑,我们所有相关逻辑需要在这个函数中编写cwrap
则用于导出 C 文件中(fftools/ffmpeg.c
)的 proxy_main
使用,函数的签名为 int main(int argc, char **argv)
,其中 int
对应到 JavaScript 就是 number
,argc 表示参数的个数 ,而 char **argv
是 C 中的指针,表示实际参数的指针数组,也可以映射到 number
接着处理 ffmpeg
的传参兼容逻辑,对于命令行中运行 ffmpeg -hide_banner
,在我们代码里通过函数调用需要 main(2, ["./ffmpeg", "-hide_banner"])
,第一个参数很好解决,那么我们如何传递一个字符串数组呢?这个问题可以分解为两个部分:
我们需要将 JavaScript 的字符串转换成 C 中的字符数组 我们需要将 JavaScript 中的数组转换为 C 中的指针数组 第一部分很简单,因为 Emscripten 提供了一个辅助函数 writeAsciiToMemory
来完成这一工作:
来帮助我们创建这个数组:交互的程序:。为了完成上述的任务,只需要使用到 FS 模块的两个函数 FS.writeFile()
和 FS.readFile()
,对于从文件系统中读取和写入的所有数据都要求是 JavaScript 中的 Uint8Array 类型,所以在消费数据之前有必要约定数据类型。
我们将通过 fs.readFileSync()
方法读取名为 flame.avi
的视频文件,然后使用 FS.writeFile()
将其写入到 Emscripten 文件系统。
方法从 Emscripten 文件系统中读取转码好的视频文件,然后通过 fs.writeFileSync()
将视频写入到本地文件系统。最终我们会收到如下结果:格式到 mp4
格式的转码,接下来我们将在浏览器中使用 ffmpeg 转码视频,并在浏览器中播放。之前我们编译的 ffmpeg 虽然可以将 avi
格式转码到 mp4
,但是这种通过默认编码格式转码的 mp4
的文件无法直接在浏览器中播放,因为浏览器不支持这种编码,所以我们需要使用 libx264
编码器来将 mp4
文件编码成浏览器可播放的编码格式。
首先在 WebAssembly
目录下下载 x264
的编码器源码:
文件,并加入如下内容:编码器之后,就可以在 ffmpeg 的编译脚本中加入打开 x264
的开关,这一次我们在 ffmpeg
文件夹下创建 Bash 脚本用于构建,创建 build.sh
如下::文件夹下创建 index.html
文件,然后添加如下内容:浏览器扩展,就可以使用 Chrome 开发者工具调试 C/C++ 代码了。这里的原理其实就是,Emscripten 在编译时,会生成一种 DWARF 格式的调试文件,这是一种被大多数编译器使用的通用调试文件格式,而 则会解析 DWARF 文件,为 Chrome Devtools 在调试时提供 source map 相关的信息,使得开发者可以在 89+ 版本以上的 Chrome Devtools 上调试 C/C++ 代码。
文件夹,然后创建 temp.c
文件,填充如下内容并保存:时,如果遇到 x >= y
的情况会抛出异常,终止程序执行。在终端切换目录到 temp
目录下执行 emcc
命令进行编译:
参数,告诉 Emscripten 在编译时为代码注入 DWARF 调试信息。现在可以开启一个 HTTP 服务器,可以使用 npx serve .
,然后访问 localhost:5000/temp.html
查看运行效果。
需要确保已经安装了 Chrome 扩展:https://chrome.google.com/webstore/detail/cc++-devtools-support-dwa/pdcpmagijalfljmkmjngeonclgbbannb,以及 Chrome Devtools 升级到 89+ 版本。
为了查看调试效果,需要设置一些内容。
打开 Chrome Devtools 里面的 WebAssembly 调试选项
设置完之后,在工具栏顶部会出现一个 Reload 的蓝色按钮,需要重新加载配置,点击一下就好。
设置调试选项,在遇到异常的地方暂停
刷新浏览器,然后你会发现断点停在了 temp.js
,由 Emscripten 编译生成的 JS 胶水代码,然后顺着调用栈去找,可以查看到 temp.c
并定位到抛出异常的位置:
可以看到,我们成功在 Chrome Devtools 里面查看了 C 代码,并且代码停在了 abort()
处,同时还可以类似我们调试 JS 时一样,查看当前 scope 下的值:
如上述可以查看 x
、y
值,将鼠标浮动到 x
上还可以显示此时的值。
文件夹,然后添加 mandelbrot.cc
文件,并填入如下内容:和 ,这使得我们的代码变得有一点复杂了,我们接下来编译上述代码,来看看 Chrome Devtools 的调试效果如何。通过在编译时带上 -g
标签,告诉 Emscripten 编译器带上调试信息,并寻求 Emscripten 在编译时注入 SDL2 库以及允许库在运行时可以使用任意内存大小:
命令开启一个本地的 Web 服务器,然后访问 http://localhost:5000/mandelbrot.html 可以看到如下效果:打开开发者工具,然后可以搜索到 mandelbrot.cc
文件,我们可以看到如下内容:
我们可以在第一个 for 循环里面的 palette
赋值语句哪一行打一个断点,然后重新刷新网页,我们发现执行逻辑会暂停到我们的断点处,通过查看右侧的 Scope 面板,可以看到一些有意思的内容。
、palette
,还可以展开它们,查看复杂类型里面具体的值:等变量上面,同样可以查看值的类型:文件,并在编译时要求 Emscripten 为我们提供内建的 SDL 相关的库,由于 SDL 库并不是我们从源码编译而来,所以不会带上调试相关的信息,所以我们仅仅在 mandelbrot.cc
里面可以通过查看 C++ 代码的形式来调试,而对于 SDL 相关的内容则只能查看 WebAssembly 相关的代码来进行调试。如我们在 41 行,SDL_SetRenderDrawColor 调用处打上断点,并使用 step in 进入到函数内部:
会变成如下的形式:
我们又回到了原始的 WebAssembly 的调试形式,这也是难以避免的一种情况,因为我们在开发过程中可能会遇到各种第三方库,但是我们并不能保证每个库都能从源码编译而来且带上了类似 DWARF 的调试信息,绝大部分情况下我们无法控制第三方库的行为;而另外一种情况则是有时我们会在生产情况下遇到问题,而生产环境也是没有调试信息的。
上述情况暂时还没有比较好的处理方法,但是开发者工具却改进了上述的调试体验,将所有的代码都打包成单一的 WebAssembly 文件,对应到我们这次就是 mandelbrot.wasm
文件,这样我们再也无需担心其中的某段代码到底来自哪个源文件。
这样的名字,大大提高了栈追踪和反汇编的体验。,但是这只能看到一些独立的字节,无法了解到这些字节对应到的其他数据格式,如 ASCII 格式。但是 Chrome 开发者工具还为我们提供了一些其他更加强大的内存查看形式,当我们右键点击 env.memory
时,可以选择 Reveal in Memory Inspector panel:或者点击 env.memory
旁边的小图标:
可以打开内存面板:
从内存面板里面可以查看以十六进制或 ASCII 的形式查看 WebAssembly 的内存,导航到特定的内存地址,将特定数据解析成各种不同的格式,如十六进制 65 代表的 e 这个 ASCII 字符。
或者 console.time
等 API,因为这些函数调用获得的性能相关的数字通常不能反应真实世界的效果。所以如果需要对代码进行性能分析,你需要使用开发者工具提供的性能面板,性能面板里面会全速运行代码,并且提供不同函数执行时花费时间的明确断点信息:
可以看到上述几个比较典型的时间点如 161ms,或者 461ms 的 LCP 与 FCP ,这些都是能反应真实世界下的性能指标。
或者你可以在加载网页时关闭控制台,这样就不会涉及到调试信息等相关内容的调用,可以确保比较真实的效果,等到页面加载完成,然后再打开控制台查看相关的指标信息。
配置里面设置路径映射,点击扩展的 “选项”:然后添加路径映射,在 old/path 里填入之前的源文件构建时的路径,在 new/path 里填入现在存在本地文件系统上的文件路径:
上述映射的功能和一些 C++ 的调试器如 GDB 的 set substitute-path
以及 LLDB 的 target.source-map
很像。这样开发者工具在查找源文件时,会查看是否在配置的路径映射里有对应的映射,如果源路径无法加载文件,那么开发者工具会尝试从映射路径加载文件,否则会加载失败。
标志来取消优化构建时(通常是带上 -O
参数)对函数进行内联处理的功能,未来开发者工具会修复这个问题。所以针对之前提到的简单 C 程序的编译脚本如下:操作:的文件名,然后在代码加载时,插件会定位到调试文件的位置并将其加载进开发者工具。如果我们想同时进行优化构建,并将调试信息单独拆分,并在之后需要调试时,加载本地的调试文件进行调试,在这种场景下,我们需要重载调试文件存储的地址来帮助插件能够找到这个文件,可以运行如下命令来处理:
,加入 -g
对应的标志:运行我们的脚本,然后打开 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
答疑...
如何将复杂的 CMake 项目编译到 WebAssembly? 在编译复杂的 CMake 项目到 WebAssembly 时如何探索一套通用的最佳实践? 如何和 CMake 项目结合起来进行 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 项目结合使用: 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
❤️ 谢谢支持
以上便是本次分享的全部内容,希望对你有所帮助^_^
喜欢的话别忘了 分享、点赞、收藏 三连哦~。
欢迎关注公众号 ELab团队 收获大厂一手好文章~
我们来自字节跳动,是旗下大力教育前端部门,负责字节跳动教育全线产品前端开发工作。
我们围绕产品品质提升、开发效率、创意与前沿技术等方向沉淀与传播专业知识及案例,为业界贡献经验价值。包括但不限于性能监控、组件库、多端技术、Serverless、可视化搭建、音视频、人工智能、产品设计Blazor WebAssembly + Grpc Web = 未来?
Blazor WebAssembly是什么 首先来说说WebAssembly是什么,WebAssembly是一个可以使C#,Java,Golang等静态强类型编程语言,运行在浏览器中的标准,浏览器厂商基于此标准实现执行引擎。 在实现了WebAssembly标准引擎之后,浏览器中可以执行由其他语言编译
以上是关于为什么说 WebAssembly 是 Web 的未来?的主要内容,如果未能解决你的问题,请参考以下文章
Blazor WebAssembly + Grpc Web = 未来?
为什么说 WebAssembly 属于浏览器之外? Why WebAssembly Belongs Outside the Browser
为什么说 WebAssembly 属于浏览器之外? Why WebAssembly Belongs Outside the Browser