news 2026/9/26 10:02:38

Wasm3 的 WAPM 打包与自托管运行:在 WebAssembly 上运行 WebAssembly 解释器(wasm3 / wasm3-strace 实战指南)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Wasm3 的 WAPM 打包与自托管运行:在 WebAssembly 上运行 WebAssembly 解释器(wasm3 / wasm3-strace 实战指南)
  • 解释器
  • 嵌入式
  • 语言运行时

【免费下载链接】wasm3

🚀 A fast WebAssembly interpreter and the most universal WASM runtime

项目地址:https://gitcode.com/gh_mirrors/wa/wasm3
点击查看免费下载

本篇技术指南围绕仓库 extra/wapm-package/ 下的 WAPM(WebAssembly Package Manager)包展开,讲解如何把 wasm3 解释器自身编译成 WASI 格式的 WebAssembly 模块并打包分发,使其可以在 WebAssembly.sh 等宿主环境中运行任意 WASM 文件,甚至实现“wasm3 内嵌 wasm3”的自托管运行。同时,文章将深入剖析随包发布的wasm3-strace工具:它能在不要求目标 WASM 做任何特殊配合的前提下,输出结构化的函数调用跟踪与 trap 时的 wasm 回溯(backtrace)。读完本文,你将掌握--repl交互模式、--func定向调用、WASI 程序运行、栈空间调优,以及一套从命令实操到源码级注入原理的完整分析方法。

Wasm3 与 WAPM 包:为什么要把解释器编译成 WASM

Wasm3 是一个用 C 实现的快速 WebAssembly 解释器。它的独特之处在于:解释器本身可以被编译为一个 WASI 格式的 WebAssembly 模块,并以 WAPM 包的形式发布——这意味着你可以在一个 WASM 运行时里再运行一个 WASM 运行时,即“在 WebAssembly 上运行 WebAssembly”。

包的定义位于 extra/wapm-package/wapm.toml,其完整内容如下:

[package] name = "vshymanskyy/wasm3" version = "0.5.0" description = "🚀 A fast WebAssembly interpreter. You can finally run WebAssembly on WebAssembly 😆" readme = "README.md" repository = "https://github.com/wasm3/wasm3" homepage = "https://github.com/wasm3/wasm3" [[module]] name = "wasm3" source = "build/wasm3-wasi.wasm" abi = "wasi" [[module]] name = "wasm3-strace" source = "build/wasm3-strace.wasm" abi = "wasi" [[command]] name = "wasm3" module = "wasm3" [[command]] name = "wasm3-strace" module = "wasm3-strace"

这个清单揭示了包的三个关键设计:

  • 两个 WASI 模块:wasm3(对应build/wasm3-wasi.wasm)是标准解释器;wasm3-strace(对应build/wasm3-strace.wasm)是开启跟踪插桩的跟踪版。两者都声明abi = "wasi",即按 WASI 命令行应用(command)的方式导出_start、依赖 WASI 的fd_*、args_*等系统调用接口。
  • 两个命令快捷方式:[[command]]把命令名wasm3/wasm3-strace分别映射到对应模块,这样用户在 WAPM 兼容环境中可以直接敲wasm3 ...而不是手动指定模块。
  • 分发即产物:source指向构建出的build/*.wasm,说明这些是 CI/发布流程编译生成的二进制产物,而非仓库内源码。对应源码侧的 WASI 支持由 source/m3_api_wasi.c 与 source/m3_api_tracer.c 提供(后者仅在d_m3HasTracer编译开关下生效)。

前置准备:获取要运行的 WASM 文件

进入支持 WASI 的宿主环境(例如 WebAssembly.sh)后,先用curl拉取一个测试用的 wasm 文件。原文档使用fib32.wasm作为示例:

$ curl https://raw.githubusercontent.com/wasm3/wasm3/main/test/lang/fib32.wasm -o /tmp/fib32.wasm $ ls -l /tmp/fib32.wasm ---------- 1 somebody somegroup 62 1970-01-19 05:45 /tmp/fib32.wasm

该文件仅 62 字节,是一个计算斐波那契数的极简模块。除了远程获取,这份测试资产也直接内置于当前仓库:test/lang/fib32.wasm(其可读源码形式见 test/lang/fib32.wat,另有 64 位变体 test/lang/fib64.wasm)。在本地非沙箱环境中,完全可以直接以仓库相对路径代替 curl 下载:

$ wasm3 --repl test/lang/fib32.wasm

交互模式:wasm3 --repl

wasm3支持交互式解释器(REPL)模式,加载模块后逐行输入函数调用:

$ wasm3 --repl /tmp/fib32.wasm wasm3> fib 20 Result: 6765 wasm3> fib 30 Result: 832040 wasm3> ^C $

REPL 的提示符为wasm3>,输入fib 20即调用模块导出的fib函数并传入参数20,运行后以Result: 6765打印返回值。从源码结构看,这一交互循环实现在 platforms/app/main.c:程序反复读取一行命令、按空白切分参数,然后分发执行。

REPL 内部还内置了一批冒号开头的管理命令(见 platforms/app/main.c),了解它们有助于深度使用:

命令作用
:init重新初始化运行时(可指定栈大小)
:load <file>加载一个 wasm 模块
:load-hex <size>从标准输入按十六进制字节加载模块
:invoke <func> [args...]调用模块导出函数(供 spec 测试使用,浮点按位模式处理)
:invoke-in <module> <func> [args...]调用指定模块内的导出函数
:get-global/:set-global读写模块全局变量
:register <name>/:name <module>为模块注册/命名,供后续模块 import
:compile强制编译(默认按需惰性编译)
:dump将最近加载模块的内存导出为wasm3_dump.bin
:version/:exit打印版本 / 退出会话

函数调用的核心路径是repl_call(platforms/app/main.c):它先用m3_FindFunction在运行时中查找函数,再用m3_GetArgCount校验实参个数(少了报not enough arguments,多了报too many arguments),最后通过m3_CallArgv执行并借助m3_GetResults读取返回值。

定向调用:wasm3 --func

如果只想运行模块中的某个具体函数,可以跳过 REPL,用--func直接指定:

$ wasm3 --func fib /tmp/fib32.wasm 30 Result: 832040

这里--func fib指定调用导出的fib函数,30是紧随其后的实参。默认入口函数是_start(platforms/app/main.c 的print_usage中写明default: _start),所以对普通模块直接wasm3 file.wasm就会运行其_start。--func亦可用-f简写(见参数解析分支 platforms/app/main.c)。在 REPL 中输入不带冒号的命令也会走同一条repl_call路径。

运行 WASI 程序

wasm3编译时若开启 WASI 支持,就能直接运行依赖系统接口的 WASI 应用:

$ curl https://raw.githubusercontent.com/wasm3/wasm3/main/test/wasi/simple/test.wasm -o /tmp/test.wasm $ wasm3 /tmp/test.wasm

仓库中的对应测试程序是 test/wasi/simple/test.c(编译命令见 test/wasi/simple/README.md,其产物即 test/wasi/simple/test.wasm)。它的_start通过fd_write向 stdout 输出,并由一个会主动触发 trap 的test_trap函数用于跟踪演示(见后文)。

从底层看,WASI 支持是在模块加载完成后通过link_all链接进来的(platforms/app/main.c):m3_LinkLibC链接 libc 扩展,m3_LinkWASI链接 WASI 接口;当运行_start时,repl_call会先把 WASI 上下文中的argc/argv设置为命令行参数(并将 wasm 文件名作为argv[0]),再执行调用(platforms/app/main.c)。程序若以proc_exit结束,运行时返回m3Err_trapWasiExit,CLI 会读取wasi_ctx->exit_code作为进程退出码。

自托管:在 wasm3 里运行 wasm3

由于 wasm3 本身也被编译成了 WASI 模块,因此可以“套娃”——用外层 wasm3 解释器运行内层的 wasm3-wasi.wasm,再由它运行第三个 WASM 文件:

$ curl https://registry-cdn.wapm.io/contents/vshymanskyy/wasm3/0.5.0/build/wasm3-wasi.wasm -o /tmp/wasm3.wasm $ wasm3 --stack-size 100000 /tmp/wasm3.wasm /tmp/test.wasm

这里的关键是--stack-size 100000:外层解释器默认栈大小为 512KB(见 platforms/app/main.c 的unsigned argStackSize = 512 * 1024;),但嵌套运行一个解释器(其内部又要为第三个模块维护另一套栈)会明显加深调用链,因此需要显式调大栈空间。理解这一点的意义在于:--stack-size并非模块内存大小,而是解释器为执行 wasm 调用所预留的宿主侧栈空间,遇到深度递归或嵌套运行时若出现栈相关错误,首先应当尝试增大该参数。

该 CLI 支持的其余选项(均可在运行wasm3 --help时看到,源码见 platforms/app/main.c)包括:--compile(关闭惰性编译)、--validate-only/--no-validate(仅校验 / 跳过校验)、--spec-repl(面向 spec 测试的 REPL)、--dump-on-trap(trap 时保存转储)、--gas-meter/--gas-limit(燃料计量与上限)、--snapshot/--resume/--interrupt(执行快照的保存、恢复与暂停点)。

wasm3-strace:结构化函数调用跟踪

随包发布的wasm3-strace可以在不需要目标 WASM 文件做任何特殊配合的前提下,输出任意 WASM 文件执行的结构化跟踪。例如跟踪纯函数调用:

$ wasm3-strace --repl /tmp/fib32.wasm wasm3> fib 3 fib (i32: 3) { fib (i32: 1) { } = 1 fib (i32: 2) { fib (i32: 0) { } = 0 fib (i32: 1) { } = 1 } = 1 } = 2 Result: 2

输出以缩进括号树呈现每个调用的入参(如i32: 3)与返回值(如} = 2),可以直观观察fib 3的两次递归分支。仓库中保存了更多参考输出,例如 test/strace/fib32.txt 展示了fib 6的完整调用树,test/strace/host-call.txt 则展示了宿主原生函数调用被标记为<native>的情形。

WASI 应用的跟踪同样支持,且 trap 发生时还会附带完整的 wasm 回溯:

$ wasm3-strace /tmp/test.wasm trap _start () { __wasm_call_ctors () { __wasilibc_populate_preopens () { wasi_snapshot_preview1!fd_prestat_get(3, 65528) { <native> } = 0 malloc (i32: 2) { dlmalloc (i32: 2) { sbrk (i32: 0) { } = 131072 } = 70016 } = 70016 ... strcmp (i32: 70127, i32: 32) { } = 0 test_trap () { a () { b () { c () { } !trap = [trap] unreachable ... ==== wasm backtrace: 0: 0x000c59 - .unnamed!c 1: 0x000c5e - .unnamed!b 2: 0x000c68 - .unnamed!a 3: 0x000c72 - .unnamed!test_trap 4: 0x000d2c - .unnamed!main 5: 0x0037c9 - .unnamed!__main_void 6: 0x000edb - .unnamed!__original_main 7: 0x0002f3 - .unnamed!_start Error: [trap] unreachable

这段输出信息量很大:

  • 跟踪树把 WASI 启动序列(_start→__wasm_call_ctors→__wasilibc_populate_preopens)与宿主调用(wasi_snapshot_preview1!fd_prestat_get ... { <native> } = 0)完整记录了下来,WASI 系统调用使用模块名!函数名前缀区分;
  • 当c内触发unreachable指令时,以!trap = [trap] unreachable标记陷阱发生位置及原因;
  • ==== wasm backtrace:之后按调用顺序列出 8 层帧,每帧给出 wasm 模块内偏移(如0x000c59)与函数名。此处函数名显示为.unnamed!c,是因为该构建未携带 name section;若模块带名称段(如 test/wasi/simple/test.wasm 用--strip-debug保留名称段的方式编译),回溯会显示真实函数名——参考 test/strace/trap.txt 中的table64-bounds.wasm!call_max。

跟踪的实现原理:编译期插桩与钩子注入

“不需要目标模块特殊配合”并非魔法,而是源自跟踪版运行时在编译阶段向每个函数体插入的追踪钩子。其核心实现在 source/m3_api_tracer.c(整体受d_m3HasTracer编译开关控制):

  • 钩子集合:m3_LinkTracer(source/m3_api_tracer.c)把一组env模块下的追踪函数链接进被加载的模块,包括函数进入/退出/循环的log_exec_enter/log_exec_exit/log_exec_loop、内存读写钩子load_ptr/store_ptr/load_val_i32…store_val_f64,以及局部变量读写钩子get_i32…set_f64;
  • 原始事件流:这些钩子把事件写成id;func、id;val这类分号分隔行,输出到wasm3_trace.csv(首次找到任一追踪函数时打开,见 source/m3_api_tracer.c);
  • 容错设计:SuppressLookupFailure会吞掉“函数查找失败”(m3Err_functionLookupFailed),即模块没有引用这些钩子时静默跳过,这正是“无需目标模块特殊支持”的机制——只有模块被插桩后才会消费这些导入;
  • 跟踪构建:wasm3-strace.wasm对应以-Dd_m3EnableStrace=2 -Dd_m3RecordBacktraces=1编译的版本。测试脚本 test/run-strace-test.py 明确说明该构建“以独立 release 二进制形式发布”,且因需要保留帧信息会关闭尾调用优化;
  • 回溯来源:trap 时m3_GetBacktrace从运行时取出调用帧链表,CLI 逐帧打印模块偏移与函数名(platforms/app/main.c),最终错误信息经m3_GetErrorInfo补充[trap] unreachable等具体原因。

跟踪输出的自动化验证

跟踪功能并非仅供演示,仓库自带一套回归测试 test/run-strace-test.py,把上述命令行的 stderr 输出与 test/strace/ 下的参考文件逐行比对:

测试用例模块参考文件
嵌套调用、参数与返回值test/lang/fib32.wasm(--func fib 6)test/strace/fib32.txt
未命名函数的调用test/regression/github-477.wattest/strace/unnamed-func.txt
宿主原生函数调用test/regression/wasi-memory-export-index0.wattest/strace/host-call.txt
trap 及其发生帧test/regression/table64-bounds.wattest/strace/trap.txt
深调用链上的 trap 回溯test/wasi/simple/test.wasm(trap)test/strace/trap-nested.txt

测试可用--exec指定被测试的解释器(例如直接指定wasm3-strace.wasm或让外层宿主运行它),用--update重新录制参考输出。这说明wasm3-strace的输出格式是稳定契约,可作为调试与回归的基准。

小结

通过 extra/wapm-package/ 中的包定义,wasm3 完成了一次有趣的“自举”:作为 WASI 模块发布的解释器既可以在 WebAssembly.sh 等环境中直接运行任意 WASM(--repl交互、--func定向调用、WASI 程序、--stack-size调栈),又能被嵌套进另一个解释器实现自托管;而wasm3-strace借助编译期插桩与env钩子注入,为任意模块提供函数级调用树跟踪和 trap 回溯,并有 test/strace/ 下的参考输出与 test/run-strace-test.py 提供可复现的验证手段。这套“命令实操 + 源码原理 + 回归测试”的组合,既适合作为 WASM 运行时的上手实验,也是理解解释器插桩与 WASI 应用调试的良好范例。

  • 解释器
  • 嵌入式
  • 语言运行时

【免费下载链接】wasm3

🚀 A fast WebAssembly interpreter and the most universal WASM runtime

项目地址:https://gitcode.com/gh_mirrors/wa/wasm3
点击查看免费下载

相关推荐

上一篇:Vuelidate与TypeScript集成指南:构建类型安全的表单验证系统
下一篇:5分钟上手dat.gui:如何在网页中快速构建交互式参数调节器

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/26 9:57:34

VS Code 高效开发必备插件推荐:用 TaoToken 统一 Key 打通 AI 编码链路

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/26 9:57:00

Atlas 300V部署YOLO实战:从ONNX到OM的完整指南

如果你最近在调研边缘AI推理硬件&#xff0c;Atlas这张卡一定绕不开。尤其是Atlas 300V 24G&#xff0c;讨论的人不少&#xff0c;但很多话题停留在"是不是运算加速卡"这个层面。我的回答很直接&#xff1a;它确实是运算加速卡&#xff0c;专门为AI推理设计的&#x…

作者头像 李华