适用范围

本文用于检查 Kotlin wasmWasi 产物,也适用于 Rust、C/C++、Go 等工具链生成的核心 Wasm 模块。前提是模块使用的 WASI 版本、导入接口和 WebAssembly 提案均受目标 Wasmtime 运行时支持。

文中的示例路径来自一个 Kotlin/WASI 工程:

1
./wasi-failure/build/compileSync/wasmWasi/main/productionLibrary/optimized/kotlinwasi-wasmtime-issue-wasi-failure.wasm

执行前必须替换以下内容:

项目 含义
输入 .wasm 路径 当前构建产物的实际路径
输出文件名 用于区分模块、配置或实验组的名称
--target 部署设备的目标三元组;在本机测试时可省略

aarch64-linux-android 表示 AArch64 Android 目标,不表示输入 Wasm 文件的架构。.wasm 是 Wasm 字节码,.cwasm 是 Wasmtime 为指定目标生成的预编译产物。后者不能在架构、Wasmtime 版本或配置不匹配的环境中直接使用。

工具与版本确认

本文涉及以下工具:

  • Wasmtime:运行、AOT 编译和反汇编 Wasm 模块。
  • wasm-tools:验证、打印、检查节区和处理 Component Model 产物。
  • WIT:Component Model 的接口定义语言。
  • Kotlin/wit-bindgen:Kotlin 官方组织维护的 wit-bindgen 分支,可用于了解 WIT 绑定与组件化流程。

先记录工具版本,并查看当前 CLI 实际支持的选项。Wasmtime 的配置项会随版本演进,不能仅根据其他机器上的命令判断参数可用性。

1
2
3
4
5
6
wasmtime --version
wasm-tools --version

wasmtime compile -W help
wasmtime compile -O help
wasmtime compile -C help

创建独立输出目录,避免分析文件与构建产物混在一起:

1
mkdir -p ./output

构建与结构验证

对于 Kotlin/WASI,先执行对应模块的 Gradle 编译任务。模块名应替换为当前工程的实际名称。

1
./gradlew :wasi-failure:compileProductionLibraryKotlinWasmWasiOptimize

随后使用 wasm-tools validate 验证二进制格式与所需提案。命令成功时不输出内容,退出码为 0;失败时会报告字节偏移和验证原因。

1
2
3
wasm-tools validate \
--features=gc,function-references,exceptions,-simd,-relaxed-simd \
./wasi-failure/build/compileSync/wasmWasi/main/productionLibrary/optimized/kotlinwasi-wasmtime-issue-wasi-failure.wasm

该命令验证的是 WebAssembly 二进制及特性集合,不能证明模块能够被目标宿主实例化。运行期还必须满足 WASI、宿主自定义导入和 Wasmtime 配置要求。

若只需确认文件可以被当前版本的 wasm-tools 解析,可使用更宽松的检查:

1
wasm-tools validate --features=all ./path/to/module.wasm

Wasmtime 运行测试

WASI Command 模块

命令型模块通常导出 _startwasmtime run 会连接标准 WASI 接口并执行入口:

1
wasmtime run ./path/to/module.wasm

运行参数必须放在 Wasm 文件之后,才会作为 WASI argv 传给模块:

1
wasmtime run ./path/to/module.wasm --input ./data.json

目录预打开属于 Wasmtime 参数,必须放在 Wasm 文件之前:

1
wasmtime run --dir ./data ./path/to/module.wasm

Reactor 或库模块

Kotlin productionLibrary 一类产物可能是库或 Reactor,而不是可直接执行的 WASI Command。此类模块通常需要宿主先调用 _initialize,再调用业务导出。可用 CLI 做基础探测:

1
wasmtime run --invoke _initialize ./path/to/module.wasm

对于仅含数值参数和返回值的核心 Wasm 导出,CLI 也可以调用指定函数:

1
wasmtime run --invoke add ./path/to/module.wasm 1 2

--invoke 的核心模块参数表示仍处于不稳定状态。Kotlin 的字符串、对象、GC 引用或复杂 ABI 不应依赖 CLI 调用验证,应由实际 Android 或原生宿主通过对应的 Wasmtime 嵌入 API 验证。

Wasmtime AOT 编译

下面的命令保留了 Kotlin/WASI + Android 场景中常用的配置。可以直接复制,然后只替换输入路径、输出路径和 --target

1
2
3
4
5
6
7
8
9
10
11
12
wasmtime compile ./wasi-failure/build/compileSync/wasmWasi/main/productionLibrary/optimized/kotlinwasi-wasmtime-issue-wasi-failure.wasm -o ./output/failure-plugin.cwasm \
--target aarch64-linux-android \
-W gc=y \
-W function-references=y \
-W exceptions=y \
-W simd=n \
-W relaxed-simd=n \
-O static-memory-guard-size=0 \
-O dynamic-memory-guard-size=0 \
-O signals-based-traps=n \
-O opt-level=2 \
-C cranelift-debug-verifier=no

配置含义如下:

参数 作用
--target aarch64-linux-android .cwasm 生成为 Android AArch64 机器码。部署目标必须与之匹配。
-W gc=yfunction-references=yexceptions=y 启用 Kotlin/Wasm 常见的 GC、函数引用和异常提案支持。目标宿主也必须启用相同能力。
-W simd=nrelaxed-simd=n 禁用 SIMD 与 Relaxed SIMD。它们不会转换已有 SIMD 指令;若输入模块确实依赖这些指令,编译会失败。
-O signals-based-traps=n 不使用宿主信号处理 Wasm 陷阱,适合需要避免与 Android 运行时信号处理冲突的嵌入场景。
-O opt-level=2 使用 Wasmtime 当前 CLI 支持的二级优化。
-C cranelift-debug-verifier=no 关闭 Cranelift 内部调试验证器,减少编译时开销;它不替代 wasm-tools validate

在 Wasmtime v47 中,static-memory-guard-sizedynamic-memory-guard-size 仍可用,但已经标记为弃用。新版本应将上面两行替换为以下一行,不要同时保留三项:

1
-C memory-guard-size=0 \

交叉编译成功仅表示当前 Wasmtime 可以为该目标生成 .cwasmaarch64-linux-android 产物不能在 x86_64 开发机上运行。应在相同 Android ABI、相同或兼容 Wasmtime 版本及相同特性配置的宿主中加载。

对于本机 AOT 回归测试,省略 --target 以生成本机产物,再运行该产物:

1
2
wasmtime compile ./path/to/module.wasm -o ./output/module.host.cwasm
wasmtime run ./output/module.host.cwasm

若模块依赖 GC、异常或其他非默认提案,第二组命令也必须带上与目标宿主一致的 -W-O-C 配置。

查看 AOT 原生代码

wasmtime objdump 可以查看 .cwasm 中生成的目标机器码。它适合定位某个 Wasm 函数产生的原生指令,或比较不同优化配置的结果。

1
wasmtime objdump ./output/failure-plugin.cwasm > ./output/failure-plugin.native.txt

需要确认某个目标的 Cranelift 可用设置时,使用:

1
wasmtime settings --target aarch64-linux-android

使用 wasm-tools 预览与分析

输出 WAT 文本

wasm-tools print 将二进制 Wasm 转为 WAT。--name-unnamed 会为未命名项生成稳定名称,便于阅读和差异比较。

1
2
3
wasm-tools print --name-unnamed \
./wasi-failure/build/compileSync/wasmWasi/main/productionLibrary/optimized/kotlinwasi-wasmtime-issue-wasi-failure.wasm \
-o ./output/failure-plugin.wat

大型 Kotlin/Wasm 模块的函数体通常很长。只检查导入、导出、类型和函数轮廓时,可输出骨架:

1
2
3
wasm-tools print --skeleton --name-unnamed \
./path/to/module.wasm \
-o ./output/module.skeleton.wat

查看节区、元数据和二进制偏移

objdump 给出每个标准节和自定义节的偏移、字节数及条目数量。它用于判断体积增长来自 codedataname、DWARF 或其他自定义节。

1
wasm-tools objdump ./path/to/module.wasm -o ./output/module.sections.txt

metadata show 可查看模块或组件的名称、producers、嵌套模块及大小比例。JSON 输出便于保存或进一步处理。

1
2
wasm-tools metadata show --json ./path/to/module.wasm \
-o ./output/module.metadata.json

需要精确对应二进制偏移与结构时,使用 dump

1
wasm-tools dump ./path/to/module.wasm -o ./output/module.bytes.txt

WIT 与 Component Model

WIT 描述 Component Model 的导入、导出与类型。它不是普通核心 Wasm 模块的替代品,而是组件 ABI 的接口契约。

当输入文件已经是 Component Model 二进制时,可以从其接口中还原 WIT:

1
wasm-tools component wit ./path/to/component.wasm

Kotlin/wit-bindgen 是 Kotlin 官方组织维护的上游 wit-bindgen 分支。它关注 WIT 绑定生成与组件化流程;直接由 Kotlin wasmWasi 生成的核心模块是否可以组件化,取决于 Kotlin 版本、WASI 版本和目标组件适配器,不能仅凭文件扩展名判断。

两个模块的完整对比

以下命令用于比较成功与失败模块。使用 > 覆盖旧结果,避免多次执行时将过期内容追加到同一份报告。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
mkdir -p ./output

wasm-tools validate --features=gc,function-references,exceptions,-simd,-relaxed-simd \
./wasi-success/build/compileSync/wasmWasi/main/productionLibrary/optimized/kotlinwasi-wasmtime-issue-wasi-success.wasm

wasm-tools validate --features=gc,function-references,exceptions,-simd,-relaxed-simd \
./wasi-failure/build/compileSync/wasmWasi/main/productionLibrary/optimized/kotlinwasi-wasmtime-issue-wasi-failure.wasm

wasm-tools print --name-unnamed \
./wasi-success/build/compileSync/wasmWasi/main/productionLibrary/optimized/kotlinwasi-wasmtime-issue-wasi-success.wasm \
-o ./output/success-plugin.wat

wasm-tools print --name-unnamed \
./wasi-failure/build/compileSync/wasmWasi/main/productionLibrary/optimized/kotlinwasi-wasmtime-issue-wasi-failure.wasm \
-o ./output/failure-plugin.wat

wasm-tools objdump \
./wasi-success/build/compileSync/wasmWasi/main/productionLibrary/optimized/kotlinwasi-wasmtime-issue-wasi-success.wasm \
-o ./output/success-plugin.sections.txt

wasm-tools objdump \
./wasi-failure/build/compileSync/wasmWasi/main/productionLibrary/optimized/kotlinwasi-wasmtime-issue-wasi-failure.wasm \
-o ./output/failure-plugin.sections.txt

wc -c \
./wasi-success/build/compileSync/wasmWasi/main/productionLibrary/optimized/kotlinwasi-wasmtime-issue-wasi-success.wasm \
./wasi-failure/build/compileSync/wasmWasi/main/productionLibrary/optimized/kotlinwasi-wasmtime-issue-wasi-failure.wasm

success_bytes=$(wc -c < ./wasi-success/build/compileSync/wasmWasi/main/productionLibrary/optimized/kotlinwasi-wasmtime-issue-wasi-success.wasm)
failure_bytes=$(wc -c < ./wasi-failure/build/compileSync/wasmWasi/main/productionLibrary/optimized/kotlinwasi-wasmtime-issue-wasi-failure.wasm)
printf 'failure - success: %+d bytes\n' "$((failure_bytes - success_bytes))"

diff -u ./output/success-plugin.wat ./output/failure-plugin.wat > ./output/wat.diff.txt
diff -u ./output/success-plugin.sections.txt ./output/failure-plugin.sections.txt > ./output/sections.diff.txt

diff 发现差异时返回退出码 1,这是预期结果,不代表分析失败。若命令在 CI 中执行,应单独处理该退出码,避免将“文件不同”误判为任务失败。

建议按以下顺序阅读结果:

  1. 先比较 wc -c 的总字节数,确认差异规模。
  2. 再查看 sections.diff.txt,定位增长的节区。
  3. 最后检查 wat.diff.txt,分析导入、导出、类型、全局变量和函数体变化。
  4. 若 WAT 仍不足以定位问题,以 module.bytes.txt 中的偏移配合编译器报错和 wasmtime objdump 继续分析。

常见结论

现象 首先检查的内容
wasm-tools validate 失败 报错偏移、工具版本和提案集合。
wasmtime compile 不支持某条指令 模块所需提案与 -W 配置,以及 Wasmtime 版本。
.cwasm 无法加载 Wasmtime 版本、目标三元组、CPU 特性和运行时配置是否与编译时一致。
CLI 无法实例化模块 WASI 版本、非 WASI 导入、预打开目录和宿主能力。
调用业务导出后发生初始化错误 模块是否是 Reactor;宿主是否已调用 _initialize 并完成 WASI 配置。

参考资料