最近在尝试将一些 C/C++、Lua 项目挪到 Web 上跑, 接触到了 emscripten. 这里会介绍下在 Visual Studio Code 与微信开发者工具中调试使用 emscripten 基于 C 生成的 WASM 代码 (WebAssembly) 的一些方法.
Emscripten 与 WebAssebmly
WebAssembly 是一种新的编码方式, 可以在现代的 Web 浏览器中运行——它是一种低级的类汇编语言, 具有紧凑的二进制格式, 可以接近原生的性能运行, 并为诸如 C/C++、C# 和 Rust 等语言提供编译目标, 以便它们可以在 Web 上运行. 它也被设计为可以与 JavaScript 共存, 允许两者一起工作. –来自 MDN
Emscripten 基于大名鼎鼎的 LLVM 提供了 C/C++ 生态下的编译工具链, 可以很方便的将 C/C++ 项目编译到 WASM, 然后放到 JS 环境 (Web、微信小程序/游戏、nodejs 等) 执行.
有很多著名的 C/C++ 生态下的工具通过它移植到了现代浏览器 (chrome、firefox 等) 中执行.
Emscripten 官方提供了 emsdk 可以很方便的我们管理多个版本的编译工具链.
vscode 中调试 WebAssembly 的基本方法
现在在 vscode 中调试 WebAssembly 还是很方便的, 巨硬 (Microsoft) 在 2023 年就开发好了一个 vscode 插件去做支持, 见: WebAssembly DWARF Debugging.
请确保你已经安装并启用了该扩展插件. 同时安装好了 Emscripten 相关编译工具链, 这里我们使用的版本如下:
1 | ❯ emcc -v |
这里我们已简单的单个 fib.c
文件的调试为例, fib.c
的内容如下:
1 |
|
我们先通过 emcc 将 C 代码编译出 WASM, 如下:
1 | emcc -v fib.c -o fib.html |
上述命令执行完成后, 会生成三个文件: fib.wasm
、fib.html
、fib.js
, 如果我们通过浏览器访问 fib.html
, 可以在浏览器的调试控制台 (F12) 看到对应的斐波那契数的输出.
fib.html
是 emscripten 生成的演示页面, 背后会调用fib.js
胶水层代码, 加载生成的 WEbAssembly 并执行对应 C 代码中的 main 函数. 具体原理可以查看源码并了解相关知识去理解.
如果本地环境安装有 Node.js. 那么我们也可以通过 node 执行胶水层代码 fib.js
, 结果如下:
1 | ❯ node fib.js |
接下来我们将演示通过 vscode 的 debugger 调试器在 C 文件和 JS 文件中打断点调试生成的 WASM & 胶水层 JS 代码, 实现单步调试.
nodejs 中调试演示
这里我们使用如下 launch.json
配置去调试 fib.js
:
1 | { |
vscode 下的 C 代码断点调试需要依赖 DWARF 调试信息 (注: 如果没有调试信息, 我们只能调试生成的 js 代码, 而不能直接在 C 中打断点), 我们使用 emcc 的 -g 编译参数, 让生成的 wasm 带上调试信息. 我们先通过如下命令编译 C 文件:
1 | emcc -g -v fib.c -o fib.html |
在 run()
函数处打一个断点, 然后在 fib.c
中 main 函数的两个 printf 中各打一个断点, 使用 F5 启动调试器即可开始调试. 演示 (GIF 加载可能稍久):
https://pub-a8b9801c20ad491b964fc0e49c81cdb7.r2.dev/debug_in_nodejs.gif
连接到浏览器进行调试
区别于上一小节中提到的 Node.js 环境下的调试方法, vscode 会负责启动 node 执行 fib.js
. 这里介绍的 vscode 结合浏览器的调试方法, WASM 和 JS 代码将由浏览器负责执行, 我们使用 vscode 的 task 让 vscode 帮我们启动浏览器.
我们使用的 vscode launch.json
调试配置如下:
1 | { |
"preLaunchTask": "StartHTTPServer"
说明会在调试开始前, 先执行一个名为 StartHTTPServer
的 vscode task. task 的配置同样可以放置于 .vscode 目录的 tasks.json
中
tasks.json
配置如下, 这里会使用到微软提供的插件 Live Preview, 它会帮我们起一个 HTTP Server 去托管 HTML 文件 (fib.html
):
1 | { |
我们延用之前打断点的位置: 在 run()
函数处打一个断点, 然后在 fib.c
中 main 函数的两个 printf 中各打一个断点. 启动配置好的调试配置.
注: 编译命令仍然是:
emcc -g -v fib.c -o fib.html
演示 (GIF 加载可能稍久):
https://pub-a8b9801c20ad491b964fc0e49c81cdb7.r2.dev/debug_in_chrome.gif
F5 启动调试后, 会有一个 chrome 浏览器调试窗口被拉起, 在 vscode 编译器可以观察到, 断点能正常执行. 于此同时, 我们也可以在浏览器开发者工具的 Debugger 中观察到断点的执行.
如果你细心观察可以看到, 调试器执行到 C 文件时, 区别于 vscode 编辑器会跳转到对应的 C 代码行, chrome 浏览器开发者工具跳转的却是 wasm 文本格式代码, 这个问题我们可以在编译的时候生成 wasm 文件的 source-map 去解决, 编译命令如下:
1 | 确保生成的 source-map 文件 fib.wasm.map 能在 --source-map-base 指定的 HTTP Server 中找到 |
此后, 重新启动调试器, 我们也可以在浏览器的开发者工具中观察到随着调试的执行, 可以正确跳到被打断点的对应 C 代码行, 而不是对应的 wasm 文本表示格式中的代码行, 浏览器会自动读取 source-map 文件找到对应代码文件的位置.
演示 (GIF 加载可能稍久):
https://pub-a8b9801c20ad491b964fc0e49c81cdb7.r2.dev/debug_in_chrome_with_sourcemap.gif
微信开发者工具中的调试
现在有很多的基于 C/C++ 写的游戏移植到了微信平台上, 基于上文浏览器的调试方法, 我们可以在微信开发者工具中达到类似的效果, 在 C 中打断点, 进行小程序/小游戏项目的调试. 我创建了一个小型项目, 可以将其导入 微信的开发者工具 进行尝试. 快速尝试下:
1 | git clone https://github.com/yeshan333/emcc_playground.git |
微信开发者工具打开 debug-blogpost 目录, 打开调试器, 在 Sources -> Page –> localhost:3000 出能看到对应的 C 文件, 并且可以使用 debugger 打断点:
演示(Windows + 微信开发者工具预发布版 RC Build (1.06.2412031) + Wechat Lib:3.7.2, 2024.12.23 10:35:40): (GIF 加载可能稍久)
https://pub-a8b9801c20ad491b964fc0e49c81cdb7.r2.dev/debug_in_wechatdev.gif
注意
Node.js 环境目前尚未支持读取 WebAssembly 的 source-map, 编译出的 wasm 即便带了 DWARF 调试信息, 堆栈只能看到符号, 看不到 C 符号对应的源文件, 例如有这样
一个 C++ 文件 unalign.cc
:
1 | // https://github.com/3dgen/cppwasm-book/blob/master/wasm-in-action-book-examples/ch5/02/unaligned.cc |
使用 emcc 编译它, 命令如下:
1 | emcc -g -v unalign.cc -o unalign.html -gsource-map --source-map-base=http://localhost:3000/ -sSAFE_HEAP=1 |
然后使用 Node.js 执行编译出来的胶水文件, 会得到类似下面的结果, 有一个内存对齐错误, 堆栈上可以看到问题出现在 throw_unalign_err
. 但我们看不到符号对应的源文件.
1 | ❯ node --enable-source-maps unalign.js |
但是如果是在 Web 浏览器环境(chrome)中, 我们能看到符号所在的 C 源文件, 打开 unalign.html
, 我们能看到如下堆栈: