wasm2c 实战指南:用 WABT 把 WebAssembly 模块转换为可移植的 C 源码
【免费下载链接】wabtThe WebAssembly Binary Toolkit项目地址: https://gitcode.com/GitHub_Trending/wa/wabt
wasm2c是 WebAssembly Binary Toolkit(WABT)中的核心工具之一:它读取一个 WebAssembly 二进制模块(.wasm),并将其翻译为功能等价、可直接编译运行的 C 源码与头文件。本文以 WABT 仓库中的 wasm2c/README.md 为骨架,结合 src/tools/wasm2c.cc 入口实现、wasm2c/wasm-rt.h 运行时头文件与 wasm2c/examples 真实示例,完整讲解从.wat编写、.wasm编译到.c落地、链接运行的端到端流程,并深入剖析生成代码的结构、嵌入方(embedder)必须实现的运行时符号、异常处理支持、多实例化以及 Segue 段寄存器优化等进阶主题。读完本文,你将能独立完成“Wasm 模块 → C 代码 → 本地可执行程序”的完整转换,并理解 wasm2c 运行时契约的每个细节。
wasm2c 是什么
wasm2c接收一个 WebAssembly 模块,生成与之等价的 C 源码和头文件。生成代码的目标标准为 C99;如果模块使用了 Wasm 线程/原子操作(threads/atomics),则生成代码面向 C11 标准。这种转换使 Wasm 模块可以被嵌入到任何支持 C 编译器的宿主程序中,无需解释器或 JIT 运行时,编译产物直接以机器码形式运行。
最基本的用法:
# 解析二进制文件 test.wasm,写出 test.c 和 test.h $ wasm2c test.wasm -o test.c # 解析 test.wasm,写出 test.c 和 test.h,但忽略二进制中的调试名字段(如有) $ wasm2c test.wasm --no-debug-names -o test.c两个命令都会同时产生test.c与test.h两个文件:-o指定 C 源文件路径,头文件路径自动由去除扩展名后拼接.h得到(该逻辑见 src/tools/wasm2c.cc)。
命令行选项全解
结合 src/tools/wasm2c.cc 的参数解析与 man/wasm2c.1 手册页,wasm2c支持的完整选项如下:
| 选项 | 说明 |
|---|---|
-h, --help | 打印帮助信息 |
--version | 打印版本信息 |
-v, --verbose | 输出更多调试信息,可多次使用 |
-o, --output=FILENAME | 生成的 C 源文件路径,默认输出到 stdout |
--num-outputs=NUM | 生成的 C 源文件数量(分片输出,见下文) |
-n, --module-name=MODNAME | 生成的 C 符号统一前缀;默认取 names section 中的模块名,若无则取输入文件名 |
--no-debug-names | 忽略二进制文件中的调试名 |
--disable-exceptions | 禁用实验性异常处理(exception handling) |
--disable-mutable-globals | 禁用导入/导出可变全局变量 |
--disable-saturating-float-to-int | 禁用饱和浮点转整数指令 |
--disable-sign-extension | 禁用符号扩展指令 |
--disable-simd | 禁用 SIMD 支持 |
--enable-threads | 启用线程支持(Wasm threads/atomics) |
--enable-function-references | 启用类型化函数引用 |
--disable-multi-value | 禁用多返回值 |
--disable-tail-call | 禁用尾调用 |
--disable-bulk-memory | 禁用批量内存操作 |
--disable-reference-types | 禁用引用类型(externref) |
--disable-annotations | 禁用自定义注解语法 |
--enable-code-metadata | 启用代码元数据 |
--enable-gc | 启用垃圾回收 |
--disable-memory64 | 禁用 64 位内存 |
--disable-multi-memory | 禁用多内存 |
--disable-extended-const | 禁用扩展常量表达式 |
--disable-relaxed-simd | 禁用 Relaxed SIMD |
--enable-custom-page-sizes | 启用自定义页大小 |
--enable-compact-imports | 启用紧凑导入段 |
--enable-wide-arithmetic | 启用宽算术 |
--enable-all | 启用全部特性 |
几个值得注意的细节(均可在源码中验证):
- 模块名决定符号前缀:
wasm2c.cc中,若未显式指定-n,则依次回退使用 names section 中的模块名、输入文件名(去扩展名)。所有生成的导出符号都共享这一前缀。 - 多文件输出:
--num-outputs=NUM可将生成代码分片写入<basename>_0.c、<basename>_1.c…,同时额外生成<basename>-impl.h(见 src/tools/wasm2c.cc),适合超大模块并行编译。 - 特性白名单校验:
wasm2c.cc维护了supported_features列表(multi-memory、multi-value、sign-extension、saturating-float-to-int、exceptions、memory64、extended-const、simd、threads、tail-call、custom-page-sizes、compact-imports,见 src/tools/wasm2c.cc),若启用了列表之外的功能会直接报错退出。 - 内部流水线:
Wasm2cMain依次执行ReadBinaryIr(二进制 → IR)、ValidateModule(校验)、GenerateNames/ApplyNames(生成与套用名字),最后调用WriteC写出 C 代码(见 src/tools/wasm2c.cc)。
教程:.wat -> .wasm -> .c
以一个经典的阶乘函数为例,将它保存为fac.wat:
(memory $mem 1) (func (export "fac") (param $x i32) (result i32) (if (result i32) (i32.eq (local.get $x) (i32.const 0)) (then (i32.const 1)) (else (i32.mul (local.get $x) (call 0 (i32.sub (local.get $x) (i32.const 1)))) ) ) )先用wat2wasm把文本格式编译为二进制格式:
$ wat2wasm fac.wat -o fac.wasm再用wasm2c把二进制转换为 C 源文件与头文件:
$ wasm2c fac.wasm -o fac.c这一步生成两个文件:fac.c与fac.h。仓库 wasm2c/examples/fac 下保存了这一整套流程的完整产物(fac.wat、fac.wasm、fac.c、fac.h、main.c和 Makefile),其中 Makefile 展示了自动化构建方式:
fac.wasm: fac.wat ../../../bin/wat2wasm ../../../bin/wat2wasm $< -o $@ fac.c: fac.wasm ../../../bin/wasm2c ../../../bin/wasm2c $< -o $@ --disable-simd使用生成的模块
为实际使用fac模块,新建main.c,包含fac.h、初始化模块实例并调用fac。
wasm2c依据fac.wasm生成若干 C 符号:
w2c_fac:表示fac模块一个实例的类型;wasm2c_fac_instantiate/wasm2c_fac_free:构造与释放w2c_fac实例的函数;w2c_fac_fac:模块导出的fac函数本身,作用于w2c_fac实例。
所有导出符号共享同一个模块 ID(fac),默认取自模块 names section 或输入文件名,可通过-n/--module-name覆盖。
#include <stdio.h> #include <stdlib.h> #include "fac.h" int main(int argc, char** argv) { /* Make sure there is at least one command-line argument. */ if (argc < 2) { printf("Invalid argument. Expected '%s NUMBER'\n", argv[0]); return 1; } /* Convert the argument from a string to an int. We'll implicitly cast the int to a `u32`, which is what `fac` expects. */ u32 x = atoi(argv[1]); /* Initialize the Wasm runtime. */ wasm_rt_init(); /* Declare an instance of the `fac` module. */ w2c_fac fac; /* Construct the module instance. */ wasm2c_fac_instantiate(&fac); /* Call `fac`, using the mangled name. */ u32 result = w2c_fac_fac(&fac, x); /* Print the result. */ printf("fac(%u) -> %u\n", x, result); /* Free the fac module. */ wasm2c_fac_free(&fac); /* Free the Wasm runtime state. */ wasm_rt_free(); return 0; }这段代码展示了嵌入方(宿主程序)的标准生命周期:先wasm_rt_init()初始化运行时,声明并instantiate模块实例,调用导出函数,最后依次wasm2c_fac_free与wasm_rt_free释放资源。
编译 wasm2c 输出
编译可执行程序需要main.c、生成的fac.c,再加上运行时实现文件wasm-rt-impl.c和wasm-rt-mem-impl.c(其中实现了fac.c/fac.h用到的各类wasm_rt_*函数):
$ cc -o fac main.c fac.c wasm2c/wasm-rt-impl.c wasm2c/wasm-rt-mem-impl.c -Iwasm2c -lm示例的 Makefile 中对应规则为fac: main.o fac.o ../../wasm-rt-impl.o,并链接-lm(数学库)。
关于编译优化的合规性提示
wasm2c 依赖 C 编译器的某些行为来维持与 WebAssembly 规范的严格一致性,尤其涉及两点:将“signaling”NaN 转换为“quiet”NaN 浮点值;无限递归必须产生 trap。因此在使用优化编译(如-O2/-O3)时,需要禁用部分优化以保持合规:
- GCC 11 上,追加
-fno-optimize-sibling-calls -frounding-math -fsignaling-nans即可; - clang 14 上,追加
-fno-optimize-sibling-calls -frounding-math即可。
编译并运行验证:
$ ./fac 1 fac(1) -> 1 $ ./fac 5 fac(5) -> 120 $ ./fac 10 fac(10) -> 3628800开启额外的健全性检查
wasm2c提供了宏WASM_RT_SANITY_CHECKS:一旦定义,生成的 wasm2c 代码会启用额外的健全性检查。注意这会带来较高的性能开销,因此仅建议在 debug 构建中使用(wasm2c/wasm-rt.h 中默认值为 0)。
开启 Segue 优化(Linux x86_64 专属)
wasm2c在条件允许时可以使用 “Segue” 优化:借助 x86 段寄存器保存 Wasm 线性内存的位置,从而加速内存访问。其启用前提是:使用 clang 编译 wasm2c 输出、运行在 x86_64 Linux 上、定义了宏WASM_RT_ALLOW_SEGUE,并向 clang 传递-mfsgsbase标志。从 wasm2c/wasm-rt.h 的WASM_RT_USE_SEGUE推导逻辑可见完整约束:
- 模块形态限制:模块必须恰好使用一块非共享、默认页大小、32 位、被导入或导出的内存;
- 编译器限制:不能用 GCC 编译。Segue 需要
(rd|wr)gsbase内建函数、“address namespaces”指针访问以及对自定义 “address namespaces” 指针的 memcpy 支持,GCC 不满足 memcpy 要求(故目前仅 clang 9+ 可用); - 平台限制:不能在 Windows 上使用,因为 Windows 在上下文切换时不会恢复段寄存器;
- 其余条件:非大端(
!WABT_BIG_ENDIAN)、非 Android、运行于 Linux 或 FreeBSD。
wasm2c生成的代码会在调用进入 wasm2c 生成的模块时自动设置未使用的段寄存器(x86_64 Linux 上为%gs),在调用外部模块后恢复。普通 C 编写的宿主函数无需改动即可继续工作——C 代码不会修改空闲的%gs段寄存器;但任何用汇编编写、会破坏空闲段寄存器的宿主函数,必须在交还控制权给 wasm2c 生成代码之前恢复该寄存器的值。
进一步的优化:如果宿主程序不把%gs段寄存器用于其他任何目的(多数程序如此),可以定义宏WASM_RT_SEGUE_FREE_SEGMENT,允许 wasm2c 无条件覆写%gs而无需恢复旧值。WASM_RT_USE_SEGUE生效时,运行时还需要提供wasm_rt_fsgsbase_inst_supported变量以及wasm_rt_syscall_set_segue_base/wasm_rt_syscall_get_segue_base函数(见 wasm2c/wasm-rt.h)。
可以用 Dhrystone 基准对比 Segue 开启前后的性能差异:
cd wasm2c/benchmarks/segue && make实际生成的 fac.c 顶部也展示了 Segue 相关的条件编译:只有当WASM_RT_USE_SEGUE && IS_SINGLE_UNSHARED_MEMORY时,才启用WASM_RT_USE_SEGUE_FOR_THIS_MODULE,并通过__builtin_ia32_rdgsbase64/wrgsbase64或系统调用读写段基址。
查看生成的头文件 fac.h
生成的 fac.h 大致如下:
/* Automatically generated by wasm2c */ #ifndef FAC_H_GENERATED_ #define FAC_H_GENERATED_ ... #include "wasm-rt.h" ... #ifndef WASM_RT_CORE_TYPES_DEFINED #define WASM_RT_CORE_TYPES_DEFINED ... #endif #ifdef __cplusplus extern "C" { #endif typedef struct w2c_fac { char dummy_member; } w2c_fac; void wasm2c_fac_instantiate(w2c_fac*); void wasm2c_fac_free(w2c_fac*); wasm_rt_func_type_t wasm2c_fac_get_func_type(uint32_t param_count, uint32_t result_count, ...); /* export: 'fac' */ u32 w2c_fac_fac(w2c_fac*, u32); #ifdef __cplusplus } #endif #endif /* FAC_H_GENERATED_ */逐段解读:最外层的#ifndef是头文件的标准防重复包含样板;WASM_RT_CORE_TYPES_DEFINED段包含所有 WebAssembly 模块都需要的类型定义(实际生成文件中是u8/s8/u16/s16/u32/s32/u64/s64/f32/f64这些定宽类型别名);extern "C"保证该头文件被 C++ 包含时符号不被名字修饰(name mangling)。fac模块没有全局变量、内存或表,因此w2c_fac结构体近乎为空,只有一个dummy_member。
wasm-rt.h 中的核心类型
头文件包含的 wasm2c/wasm-rt.h 定义了大量与 WebAssembly 相关的运行时类型。首先是wasm_rt_trap_t枚举,用于说明 trap 发生的原因:
typedef enum { WASM_RT_TRAP_NONE, WASM_RT_TRAP_OOB, WASM_RT_TRAP_INT_OVERFLOW, WASM_RT_TRAP_DIV_BY_ZERO, WASM_RT_TRAP_INVALID_CONVERSION, WASM_RT_TRAP_UNREACHABLE, WASM_RT_TRAP_CALL_INDIRECT, WASM_RT_TRAP_UNCAUGHT_EXCEPTION, WASM_RT_TRAP_EXHAUSTION, } wasm_rt_trap_t;源码 wasm2c/wasm-rt.h 中的完整版本还包含WASM_RT_TRAP_NULL_REF(空引用)与WASM_RT_TRAP_UNALIGNED(非对齐原子操作);在 macOS 等启用信号处理栈检测溢出的平台上,WASM_RT_TRAP_EXHAUSTION会与WASM_RT_TRAP_OOB合并为同一个值(WASM_RT_MERGED_OOB_AND_EXHAUSTION_TRAPS)。
其次是wasm_rt_type_t枚举,用于描述函数签名。README 列出六种 WebAssembly 值类型:
typedef enum { WASM_RT_I32, WASM_RT_I64, WASM_RT_F32, WASM_RT_F64, WASM_RT_FUNCREF, WASM_RT_EXTERNREF, } wasm_rt_type_t;当前 wasm2c/wasm-rt.h 的实现还追加了WASM_RT_V128(SIMD 向量)与WASM_RT_EXNREF(异常引用)。
接着是wasm_rt_function_ptr_t——通用函数回调的签名。由于 Wasm 表可以容纳任意签名的函数,需要将其统一转换为规范形式:
typedef void (*wasm_rt_function_ptr_t)(void);接下来是函数引用(funcref)的定义。在 WebAssembly 1.0 中这是所有表元素的类型;如今 funcref 也可以作为普通值使用,表也可以声明为 externref 类型。结构体中wasm_rt_func_type_t是一个不透明的 256 位 ID,可通过Z_[modname]_get_func_type函数查询(callback示例对此有演示);module_instance指向函数所属模块实例的指针,调用该函数时会被传入:
typedef struct { wasm_rt_func_type_t func_type; wasm_rt_function_ptr_t func; void* module_instance; } wasm_rt_funcref_t;(当前源码 wasm2c/wasm-rt.h 中func_type实际以const char*实现,并新增了func_tailcallee成员用于尾调用优化。)
然后是内存实例的定义。data指向size字节的线性内存;size是内存实例当前大小(字节),pages是当前大小(页数),page_size是页大小(默认 65536 字节);max_pages是模块指定的最大页数或内存索引类型所允许的上限(is64为 true 表示内存可增长到 2^64 字节,false 表示限制在 2^32 字节):
typedef struct { uint8_t* data; uint32_t page_size; uint64_t pages, max_pages; uint64_t size; bool is64; } wasm_rt_memory_t;当前源码 wasm2c/wasm-rt.h 还增加了data_end字段,用于大端平台上地址翻转访问及守卫页(guard page)布局。紧跟着的是共享内存实例的定义,它与普通内存类似,但可被多个 Wasm 实例使用,因此对操作施加了最低限度的内存序约束;共享内存定义多出一个成员mem_lock,用于内存增长操作时的线程安全锁:
typedef struct { _Atomic volatile uint8_t* data; uint64_t pages, max_pages; uint64_t size; bool is64; mtx_t mem_lock; } wasm_rt_shared_memory_t;源码中的mem_lock实际类型为WASM_RT_MUTEX(Windows 上为CRITICAL_SECTION,其他平台为pthread_mutex_t),且仅在 C11 可用(WASM_RT_C11_AVAILABLE)时定义(见 wasm2c/wasm-rt.h)。
最后是表实例的定义。data指向size个元素;与内存实例类似,size是表的当前大小,max_size是最大大小,若无上限则为0xffffffff:
typedef struct { wasm_rt_funcref_t* data; uint32_t max_size; uint32_t size; } wasm_rt_funcref_table_t;源码还提供了对应的wasm_rt_externref_table_t(元素类型为wasm_rt_externref_t,即void*,见 wasm2c/wasm-rt.h)。
嵌入方必须定义的符号
wasm-rt.h中还有一组函数声明,必须在这些 C 源码被使用之前由嵌入方(即你)实现。这些函数的 C 实现定义在 wasm2c/wasm-rt-impl.h 与 wasm2c/wasm-rt-impl.c 中:
void wasm_rt_init(void); bool wasm_rt_is_initialized(void); void wasm_rt_free(void); void wasm_rt_trap(wasm_rt_trap_t) __attribute__((noreturn)); const char* wasm_rt_strerror(wasm_rt_trap_t trap); void wasm_rt_allocate_memory(wasm_rt_memory_t*, uint32_t initial_pages, uint32_t max_pages, bool is64, uint32_t page_size); uint32_t wasm_rt_grow_memory(wasm_rt_memory_t*, uint32_t pages); void wasm_rt_free_memory(wasm_rt_memory_t*); void wasm_rt_allocate_memory_shared(wasm_rt_shared_memory_t*, uint32_t initial_pages, uint32_t max_pages, bool is64, uint32_t page_size); uint32_t wasm_rt_grow_memory_shared(wasm_rt_shared_memory_t*, uint32_t pages); void wasm_rt_free_memory_shared(wasm_rt_shared_memory_t*); void wasm_rt_allocate_funcref_table(wasm_rt_table_t*, uint32_t elements, uint32_t max_elements); void wasm_rt_allocate_externref_table(wasm_rt_externref_table_t*, uint32_t elements, uint32_t max_elements); void wasm_rt_free_funcref_table(wasm_rt_table_t*); void wasm_rt_free_externref_table(wasm_rt_table_t*); uint32_t wasm_rt_call_stack_depth; /* on platforms that don't use the signal handler to detect exhaustion */ void wasm_rt_init_thread(void); void wasm_rt_free_thread(void);(当前源码 wasm2c/wasm-rt.h 中内存相关函数的initial_pages/max_pages参数已改为uint64_t,返回值也相应为uint64_t,以支持 memory64。)
各符号语义如下:
wasm_rt_init:必须在做任何其他事情之前调用,用于初始化运行时;wasm_rt_free释放所有全局状态;wasm_rt_is_initialized用于确认运行时已初始化。wasm_rt_trap:模块发生 trap 时调用的函数。可能的实现方式有抛出 C++ 异常,或直接中止程序执行。wasm2c 自带的默认运行时使用longjmp展开栈。宿主可以通过定义WASM_RT_TRAP_HANDLER覆盖对longjmp的调用,指向自定义 trap 处理函数(签名须为void handler(wasm_rt_trap_t)),例如-DWASM_RT_TRAP_HANDLER=my_trap_handler。wasm_rt_allocate_memory:初始化内存实例,至少分配给定初始页数所需空间,每页大小为page_size(除非使用 custom-page-sizes 特性,否则必须为WASM_DEFAULT_PAGE_SIZE,即 64 KiB),内存必须清零;is64参数指示内存以 i32 还是 i64 地址索引。wasm_rt_grow_memory:按给定页数增长内存实例。若内存不足或新页数超过最大页数,则必须返回0xffffffff表示失败;成功时返回内存实例之前的页数。宿主可通过定义WASM_RT_GROW_FAILED_HANDLER指定失败回调(签名为void handler(void)),例如-DWASM_RT_GROW_FAILED_HANDLER=my_growfail_handler。wasm_rt_free_memory:释放内存实例。wasm_rt_allocate_memory_shared:初始化可被不同 Wasm 线程共享的内存实例,其余行为与wasm_rt_allocate_memory类似;wasm_rt_grow_memory_shared按页增长共享内存,其余类似wasm_rt_grow_memory;wasm_rt_free_memory_shared释放共享内存实例。wasm_rt_allocate_funcref_table与wasm_rt_allocate_externref_table:初始化对应类型的表实例,至少分配给定初始元素数的空间,元素必须清零;对应的wasm_rt_free_*_table释放表实例。wasm_rt_call_stack_depth:当前调用栈深度。由于它在模块间共享,只能由嵌入方定义一次,且仅在不使用信号处理来检测栈溢出的平台上使用。wasm_rt_init_thread/wasm_rt_free_thread:初始化/释放除调用wasm_rt_init的线程之外的其他线程的运行时状态,示例见 wasm2c/examples/threads。
异常处理(exceptions)的运行时支持
若 wasm2c 以异常支持模式运行,还需定义若干附加符号(若想避免,可用--disable-exceptions)。它们定义在 wasm2c/wasm-rt-exceptions.h,其 C 实现位于 wasm2c/wasm-rt-exceptions-impl.c:
void wasm_rt_load_exception(const char* tag, uint32_t size, const void* values); WASM_RT_NO_RETURN void wasm_rt_throw(void); WASM_RT_UNWIND_TARGET WASM_RT_UNWIND_TARGET* wasm_rt_get_unwind_target(void); void wasm_rt_set_unwind_target(WASM_RT_UNWIND_TARGET* target); uint32_t wasm_rt_exception_tag(void); uint32_t wasm_rt_exception_size(void); void* wasm_rt_exception(void); wasm_rt_try(target)各符号语义:
wasm_rt_load_exception:将活动异常(active exception)设置为给定的 tag、大小和内容。wasm_rt_throw:抛出活动异常。WASM_RT_UNWIND_TARGET:异常被抛出并捕获时的 unwind target 类型。wasm_rt_get_unwind_target:获取异常抛出时的当前 unwind target;wasm_rt_set_unwind_target设置之。- 三个访问函数
wasm_rt_exception_tag、wasm_rt_exception_size、wasm_rt_exception分别返回活动异常的 tag、大小与内容。 wasm_rt_try(target):宏,将当前调用环境捕获为 unwind target 并存入target(须为WASM_RT_UNWIND_TARGET类型)。在 wasm2c/wasm-rt.h 中它被实现为WASM_RT_SETJMP_EXN(target)。
导出符号(Exported symbols)
最后,fac.h定义了模块实例类型(对fac而言基本为空)以及模块提供的导出符号。本例中唯一的导出是fac函数:
typedef struct w2c_fac { char dummy_member; } w2c_fac; void wasm2c_fac_instantiate(w2c_fac*); void wasm2c_fac_free(w2c_fac*); wasm_rt_func_type_t wasm2c_fac_get_func_type(uint32_t param_count, uint32_t result_count, ...); /* export: 'fac' */ u32 w2c_fac_fac(w2c_fac*, u32);wasm2c_fac_instantiate(w2c_fac*)创建模块实例,在使用实例前必须先调用;wasm2c_fac_free(w2c_fac*)释放实例。wasm2c_fac_get_func_type用于在运行时查询函数类型 ID。它是可变参数函数:前两个参数给出参数个数与结果个数,后续参数为上述wasm_rt_type_t枚举中的类型。callback示例(wasm2c/examples/callback)演示了如何借此在运行时向 WebAssembly 模块动态传入宿主函数。
处理其他类型的导入与导出
导出函数通过在头文件中声明带前缀的等价函数来处理。若模块导入函数,wasm2c会在输出头文件中声明该函数,由宿主函数负责定义实现。
其他类型的导出(全局变量、内存、表)处理方式不同:它们属于模块实例的一部分,每个实例可以拥有各自的导出。对于这些情况,wasm2c提供接受模块实例为参数、返回对应导出的函数。例如若fac导出一块内存:
(export "mem" (memory $mem))则wasm2c会在头文件中声明如下函数:
/* export: 'mem' */ wasm_rt_memory_t* w2c_fac_mem(w2c_fac* instance);其定义形式为:
/* export: 'mem' */ wasm_rt_memory_t* w2c_fac_mem(w2c_fac* instance) { return &instance->w2c_mem; }宿主程序通过该访问器即可直接读写模块实例的线性内存,这正是rot13示例中宿主与 Wasm 交换数据的基础。
快速查看 fac.c 的内部实现
fac.c的内容属于模块内部实现,但了解其工作原理很有帮助。文件开头数百行定义了实现各种 WebAssembly 指令所需的宏(如 fac.c 中的MEM_ADDR、TRAP(x)、FUNC_PROLOGUE/FUNC_EPILOGUE——后者在WASM_RT_STACK_DEPTH_COUNT模式下通过wasm_rt_call_stack_depth计数检测栈耗尽并TRAP(EXHAUSTION))。其后是各类初始化函数(init、free、init_func_types、init_globals、init_memory、init_table、init_exports),本例中它们大多为空,因为模块没有使用全局变量、内存或表。
最有趣的部分是fac函数本身的定义:
static u32 w2c_fac_fac_0(w2c_fac* instance, u32 var_p0) { FUNC_PROLOGUE; u32 var_i0, var_i1, var_i2; var_i0 = var_p0; var_i1 = 0u; var_i0 = var_i0 == var_i1; if (var_i0) { var_i0 = 1u; } else { var_i0 = var_p0; var_i1 = var_p0; var_i2 = 1u; var_i1 -= var_i2; var_i1 = w2c_fac_fac_0(instance, var_i1); var_i0 *= var_i1; } FUNC_EPILOGUE; return var_i0; }对照原始 WebAssembly 文本的扁平格式(flat format),可以看到输出与输入存在一一映射关系:
(func $fac (param $x i32) (result i32) local.get $x i32.const 0 i32.eq if (result i32) i32.const 1 else local.get $x local.get $x i32.const 1 i32.sub call 0 i32.mul end)它看起来与前面书写的阶乘函数不同,是因为这里用了“扁平格式”而非“折叠格式”(folded format)。可以用wat-desugar在两种格式间转换验证:
$ wat-desugar fac-flat.wat --fold -o fac-folded.wat(module (func (;0;) (param i32) (result i32) (if (result i32) ;; label = @1 (i32.eq (local.get 0) (i32.const 0)) (then (i32.const 1)) (else (i32.mul (local.get 0) (call 0 (i32.sub (local.get 0) (i32.const 1))))))) (export "fac" (func 0)) (type (;0;) (func (param i32) (result i32))))格式与变量/函数名虽不同,但结构完全一致。
创建模块的多个实例
由于执行上下文信息(如内存)被封装在模块实例结构体中,且该结构体的指针贯穿所有函数调用,因此同一模块的多个实例可以在同一地址空间内并存互不干扰。
以rot13示例(wasm2c/examples/rot13)的main函数变体为例:通过声明两组上下文信息,两个rot13实例即可在同一地址空间内实例化:
#include <assert.h> #include <stdio.h> #include <stdlib.h> #include "rot13.h" /* Define structure to hold the imports */ typedef struct w2c_host { wasm_rt_memory_t memory; char* input; } w2c_host; /* Accessor to access the memory member of the host */ wasm_rt_memory_t* w2c_host_mem(w2c_host* instance) { return &instance->memory; } int main(int argc, char** argv) { /* Make sure there is at least one command-line argument. */ if (argc < 2) { printf("Invalid argument. Expected '%s WORD...'\n", argv[0]); return 1; } /* Initialize the Wasm runtime. */ wasm_rt_init(); /* Create two `host` instances to store the memory and current string */ w2c_host host_1, host_2; wasm_rt_allocate_memory(&host_1.memory, 1, 1, false, WASM_DEFAULT_PAGE_SIZE); wasm_rt_allocate_memory(&host_2.memory, 1, 1, false, WASM_DEFAULT_PAGE_SIZE); /* Construct the `rot13` module instances */ w2c_rot13 rot13_1, rot13_2; wasm2c_rot13_instantiate(&rot13_1, &host_1); wasm2c_rot13_instantiate(&rot13_2, &host_2); /* Call `rot13` on the first two arguments. */ assert(argc > 2); host_1.input = argv[1]; w2c_rot13_rot13(&rot13_1); host_2.input = argv[2]; w2c_rot13_rot13(&rot13_2); /* Free the rot13 instances. */ wasm2c_rot13_free(&rot13_1); wasm2c_rot13_free(&rot13_2); /* Free the Wasm runtime state. */ wasm_rt_free(); return 0; } /* Fill the wasm buffer with the input to be rot13'd. * * params: * instance: An instance of the w2c_host structure * ptr: The wasm memory address of the buffer to fill data. * size: The size of the buffer in wasm memory. * result: * The number of bytes filled into the buffer. (Must be <= size). */ u32 w2c_host_fill_buf(w2c_host* instance, u32 ptr, u32 size) { for (size_t i = 0; i < size; ++i) { if (instance->input[i] == 0) { return i; } instance->memory.data[ptr + i] = instance->input[i]; } return size; } /* Called when the wasm buffer has been rot13'd. * * params: * w2c_host: An instance of the w2c_host structure * ptr: The wasm memory address of the buffer. * size: The size of the buffer in wasm memory. */ void w2c_host_buf_done(w2c_host* instance, u32 ptr, u32 size) { /* The output buffer is not necessarily null-terminated, so use the %*.s * printf format to limit the number of characters printed. */ printf("%s -> %.*s\n", instance->input, (int)size, &instance->memory.data[ptr]); }这段代码还示范了rot13模块如何通过导入宿主函数w2c_host_fill_buf(把命令行输入写入 Wasm 内存)与w2c_host_buf_done(把转换结果读出打印)与宿主交互,其中宿主上下文w2c_host通过wasm2c_rot13_instantiate(&rot13_1, &host_1)传入模块实例,充分体现了“实例即状态容器”的设计。
小结
至此,从命令行基础用法、.wat → .wasm → .c的完整转换流程,到生成头文件中w2c_*符号契约、wasm-rt.h的 trap/类型/内存/表数据结构、嵌入方必须实现的运行时 API、异常支持符号、Segue 优化以及多实例化模式,本文已完整覆盖 wasm2c/README.md 的全部内容,并对照 src/tools/wasm2c.cc、wasm2c/wasm-rt.h、wasm2c/wasm-rt-impl.c、wasm2c/wasm-rt-exceptions-impl.c 与 wasm2c/examples 系列示例做了源码级验证。进一步实践可参考 wasm2c/examples/fac、wasm2c/examples/rot13、wasm2c/examples/callback 与 wasm2c/examples/threads 四个完整示例工程,以及test/wasm2c/目录下 260 余个测试用例。
【免费下载链接】wabtThe WebAssembly Binary Toolkit项目地址: https://gitcode.com/GitHub_Trending/wa/wabt
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考