WAMR Reference Types 详解:用 externref / funcref 打通 Wasm 与宿主环境的对象互操作
【免费下载链接】fluent-bitFast and Lightweight Logs, Metrics and Traces processor for Linux, BSD, OSX and Windows项目地址: https://gitcode.com/GitHub_Trending/fl/fluent-bit
导读
WebAssembly 的 reference-types 中内置的 2.4.1 版本)的实现为主线,讲解 reference-types 提案在 WAMR 中的落地方式、externref的不透明对象语义、宿主侧与 Wasm 侧双向传递 host 对象的三种调用接口,并结合 samples/ref-types 官方示例给出可运行的完整代码解读与编译方法。读完本文,你将掌握如何让原生方法把宿主对象作为externref传入/传出 Wasm 应用,并理解其在 32/64 位平台上的位宽约束。
一、什么是 reference-types 提案:funcref 与 externref
reference-types 提案为 WebAssembly 增加了两种新的值类型:
| 类型 | 语义 | 典型用途 |
|---|---|---|
funcref | 指向 Wasm 函数的引用,可存放在 table 中,配合call_indirect实现动态分发 | 替代早期仅能存函数指针的anyfunc表,作为回调、多态调度的载体 |
externref | 指向宿主(host)环境中任意对象的引用 | 让 Wasm 直接持有并传递宿主对象(如 C 语言中的结构体指针、句柄),无需序列化拷贝 |
在 Wasm 沙箱模型中,模块默认只能访问自身线性内存,与宿主交互通常依赖“拷贝数据进/出内存 + 传递指针整数”的方式。externref改变了这一格局:宿主对象的引用可以直接以类型安全的方式进入 Wasm 值栈、局部变量、全局变量甚至 table 中,宿主侧无需复制对象内容,Wasm 侧也无需感知对象内部布局。
注:reference-types 提案的原始规范位于外部 WebAssembly 官方仓库,本文仅介绍 WAMR 对该提案的实现与使用方式。
二、WAMR 对 reference-types 的实现概述
WAMR 完整实现了 reference-types 提案。其核心设计是:
- 允许原生方法将宿主对象作为
externref参数传给 Wasm 应用; - 允许原生方法从 Wasm 应用接收
externref形式返回的宿主对象; - 内部实现上,WAMR不会解析或解引用
externref,对它而言这是一个不透明(opaque)类型——它只负责搬运这个引用值,不关心它指向什么。
这一设计与官方文档描述一致:官方文档 lib/wasm-micro-runtime-WAMR-2.4.1/doc/ref_types.md 明确说明 WAMR 已实现 reference-types 提案,并将externref定义为不透明类型。
2.1 在源码中的类型表示
从 WAMR 导出头文件 core/iwasm/include/wasm_export.h 可以看到其类型建模:
enum wasm_valkind_enum { WASM_I32, WASM_I64, WASM_F32, WASM_F64, WASM_V128, WASM_EXTERNREF = 128, /* externref 值类型 */ WASM_FUNCREF, /* funcref 值类型 */ };而通用值结构wasm_val_t的联合体中专门为引用类型预留了字段(wasm_export.h):
typedef struct wasm_val_t { wasm_valkind_t kind; uint8_t _paddings[7]; union { int32_t i32; int64_t i64; float f32; double f64; /* represent a foreign object, aka externref in .wat */ uintptr_t foreign; /* externref 在宿主侧的真实载体 */ struct wasm_ref_t *ref; } of; } wasm_val_t;注意foreign字段的类型是uintptr_t,这正是本文第 3 节位宽约束的根源。
2.2 构建开关与提案支持状态
reference-types 功能受编译宏WAMR_BUILD_REF_TYPES控制:
- 在 doc/build_wamr.md 中说明:
WAMR_BUILD_REF_TYPES=1/0,默认开启; - 在 doc/stability_wasm_proposals.md 的提案支持状态表中,Reference Types 的“实现状态”为Yes;
- 官方示例 samples/ref-types/CMakeLists.txt 中显式设置了
set(WAMR_BUILD_REF_TYPES 1)来确保该示例启用此特性。
因此,使用默认参数构建的 WAMR 已内置 reference-types 支持,无需额外开关。
三、关键约束:externref 的位宽 = uintptr_t
官方文档给出了使用externref在原生方法中必须遵守的核心限制:
宿主对象必须是
uintptr_t变量的值。也就是说,在 64 位机器上占用8 字节,在 32 位机器上占用4 字节。在调用wasm_runtime_call_wasm时尤其要牢记这一点。
这条约束的深层含义:
externref在宿主侧就是一个整数宽度的句柄:WAMR 不做解析、不做引用计数语义上的深拷贝,仅把该整数原样搬运进/出 Wasm;- 跨平台移植性风险:如果你的宿主对象指针在 64 位平台是 8 字节,那么所有涉及
externref的 NativeSymbol 签名、wasm_runtime_call_wasm*的参数组装都必须按 8 字节对齐处理,否则会出现截断; - 调用约定:在 NativeSymbol 签名串中,
externref参数用小写字母'r'表示(见 wasm_export.h 的签名规则说明):
'i': i32 类型 'I': i64 类型 'f': f32 类型 'F': f64 类型 'r': externref 类型,宿主侧应传 uintptr_t '*': 指针参数(WASM 中为 i32),运行时自动做边界检查 '$': 字符串参数(WASM 中为 i32),运行时自动做边界检查正是因为'r'对应uintptr_t,官方文档才特别强调:在 32 位平台上externref只有 4 字节,绝不能在 64 位假设下硬编码宽度。
四、官方示例逐行解读:ref-types
官方文档最后指向了 samples/ref-types 示例。该示例包含三个文件:
samples/ref-types/ ├── CMakeLists.txt # 构建脚本:启用 REF_TYPES 并编译 hello.wasm └── src/ ├── hello.c # 宿主侧 C 代码:注册原生函数、调用 Wasm └── hello.wat # Wasm 侧 WAT 代码:使用 externref 表与 call_indirect4.1 Wasm 侧:hello.wat
hello.wat 完整展示了externref与funcref的两种使用场景:
(module ;; 函数类型:参数为 (i32, externref),返回 i32 (type $t0 (func (param i32 externref) (result i32))) ;; 导入两个宿主原生函数(来自 "env" 模块) (import "env" "native-cmp-externref" (func $native-cmp-externref (param externref externref) (result i32))) (import "env" "native-chk-externref" (func $native-chk-externref (param i32 externref) (result i32))) ;; externref 表:容量 8,用于存放宿主对象引用 (table $t1 8 8 externref) ;; funcref 表:用 elem 段预填充两个原生函数引用 (table $t2 funcref (elem $native-cmp-externref $native-chk-externref)) ;; 把 externref 写入 $t1 表的第 i 个槽位 (func (export "set-externref") (param $i i32) (param $r externref) (table.set $t1 (local.get $i) (local.get $r))) ;; 从 $t1 表第 i 个槽位取出 externref (func (export "get-externref") (param $i i32) (result externref) (table.get $t1 (local.get $i))) ;; 取出表中对象并交给宿主原生函数比较 (func (export "cmp-externref") (param $i i32) (param $r externref) (result i32) (table.get $t1 (local.get $i)) (local.get $r) (call $native-cmp-externref)) ;; 通过 funcref 表做间接调用(call_indirect) (func (export "chk-externref") (param $i i32) (param $r externref) (result i32) (call_indirect $t2 (type $t0) (local.get $i) (local.get $r) (i32.const 1))) )这段 WAT 的关键点:
externref表(table $t1):Wasm 模块内部用table.set/table.get存储和读取宿主对象引用,这是externref能跨函数存活的基础设施;funcref表(table $t2):elem段直接把导入的原生函数引用预填充进表,call_indirect按索引动态调用——这正是 reference-types 提案统一后的 table 元素模型;chk-externref的调用细节:call_indirect $t2 (type $t0)要求被调函数签名为(i32, externref) -> i32,由于$t2中存的是$native-cmp-externref(签名为(externref, externref) -> i32)和$native-chk-externref(签名为(i32, externref) -> i32),后者匹配$t0,故传入索引1即调用native-chk-externref。
4.2 宿主侧:hello.c
hello.c 是完整的嵌入示例,展示了“注册原生函数 → 加载模块 → 实例化 → 双向传递 externref → 校验一致性”的完整链路。
① 注册带 externref 参数的原生函数
static uintptr_t global_objects[10] = { 0 }; int32 local_cmp_externref(wasm_exec_env_t exec_env, uintptr_t externref_a, uintptr_t externref_b) { return externref_a == externref_b; /* 仅比较句柄值,不解引用 */ } int32 local_chk_externref(wasm_exec_env_t exec_env, int32 index, uintptr_t externref) { return externref == global_objects[index]; } static NativeSymbol native_symbols[] = { { "native-cmp-externref", local_cmp_externref, "(rr)i", NULL }, { "native-chk-externref", local_chk_externref, "(ir)i", NULL }, };签名串"(rr)i"表示两个externref参数返回i32;"(ir)i"表示i32 + externref参数返回i32。原生函数直接以uintptr_t接收这些句柄,只做相等比较——体现了“不透明类型”的语义。
② 宿主 → Wasm:把 externref 写入 Wasm 表
wasm_set_externref使用wasm_runtime_call_wasm的经典 argv 数组方式调用 Wasm 导出的set-externref:
union { uintptr_t val; uint32 parts[2]; } u; uint32 argv[3] = { 0 }; u.val = externref; /* 把 uintptr_t 拆成两个 32 位分量 */ argv[0] = index; argv[1] = u.parts[0]; argv[2] = u.parts[1]; if (!wasm_runtime_call_wasm(exec_env, wasm_set_externref_ptr, 2, argv)) { const char *exception; if ((exception = wasm_runtime_get_exception(inst))) { printf("Exception: %s\n", exception); } return false; }注意这里用联合体把uintptr_t拆成parts[0]/parts[1]两个 32 位分量,这正是官方文档提醒“在 64 位机器上 externref 占 8 字节”的实际落地:wasm_runtime_call_wasm的 argv 以 32 位为单位传递,8 字节句柄必须手动拆分。
③ Wasm → 宿主:接收 externref 返回值
wasm_get_externref使用变参版本wasm_runtime_call_wasm_v,并通过wasm_val_t的of.foreign字段取回句柄:
wasm_val_t results[1] = { 0 }; if (!wasm_runtime_call_wasm_v(exec_env, wasm_get_externref_ptr, 1, results, 1, index)) { ... return false; } if (WASM_EXTERNREF != results[0].kind) { /* 校验返回值类型 */ return false; } *ret_externref = results[0].of.foreign; /* uintptr_t 句柄 */ return true;④ 类型化参数版本:wasm_runtime_call_wasm_a
wasm_cmp_externref使用wasm_val_t数组显式构造参数,是最“类型安全”的调用方式:
wasm_val_t arguments[2] = { { .kind = WASM_I32, .of.i32 = index }, { .kind = WASM_EXTERNREF, .of.foreign = externref }, }; if (!wasm_runtime_call_wasm_a(exec_env, wasm_cmp_externref_ptr, 1, results, 2, arguments)) { ... }由此可以总结 WAMR 提供的三种调用入口(均声明于 wasm_export.h):
| 接口 | 参数传递方式 | externref 支持方式 |
|---|---|---|
wasm_runtime_call_wasm | 扁平uint32 argv[]数组 | 需自行按 8/4 字节拆分uintptr_t |
wasm_runtime_call_wasm_v | 变参列表 | 结果用wasm_val_t.of.foreign接收 |
wasm_runtime_call_wasm_a | wasm_val_t数组(携带 kind) | 参数/结果都用WASM_EXTERNREF+of.foreign |
⑤ 一致性校验主流程
set_and_cmp串起“写入、读回、双向比较”的闭环,模拟了真实场景中对象句柄往返 Wasm 的流程:
static bool set_and_cmp(wasm_exec_env_t exec_env, wasm_module_inst_t inst, int32 i, uintptr_t externref) { int32 cmp_result = 0; uintptr_t wasm_externref = 0; wasm_set_externref(exec_env, inst, i, externref); /* 宿主 → Wasm 表 */ local_set_externref(i, externref); /* 宿主侧记录一份 */ wasm_get_externref(exec_env, inst, i, &wasm_externref); /* Wasm 表 → 宿主 */ if (!local_chk_externref(exec_env, i, wasm_externref)) { printf("#%d, In host language world Wasm Externref 0x%lx Vs. Native " "Externref 0x%lx FAILED\n", i, wasm_externref, externref); return false; } if (!wasm_cmp_externref(exec_env, inst, i, global_objects[i], &cmp_result) || !cmp_result) { printf("#%d, In Wasm world Native Externref 0x%lx Vs, Wasm Externref " "FAILED\n", i, global_objects[i]); return false; } return true; }main中分别用NULL(句柄值 0)和三个基于0x123456789abc的大数测试该闭环:
const uint64 big_number = 0x123456789abc; if (!set_and_cmp(exec_env, wasm_module_inst, 0, 0) || !set_and_cmp(exec_env, wasm_module_inst, 1, big_number + 1) || !set_and_cmp(exec_env, wasm_module_inst, 2, big_number + 2) || !set_and_cmp(exec_env, wasm_module_inst, 3, big_number + 3)) { goto fail; } printf("GREAT! PASS ALL CHKs\n");其中0x123456789abc是超过 32 位表示范围的值,专门用于验证 64 位平台上 8 字节externref的拆分/重组逻辑——若某处只按 32 位处理,该测试会直接失败。
其余main流程(wasm_runtime_full_init→wasm_runtime_load→wasm_runtime_instantiate→wasm_runtime_create_exec_env→wasm_runtime_lookup_function→ 调用 → 逐级销毁)是 WAMR 嵌入的标准生命周期,可对照 embed_wamr.md 进一步学习。
4.3 构建与运行
CMakeLists.txt 负责将hello.wat编译为hello.wasm(需要wat2wasm,来自 wabt 工具链;旧版 wabt 还需追加--enable-reference-types标志),再与hello.c一起链接vmlib。参考流程:
# 1. 准备 wabt(提供 wat2wasm) # 例如:sudo apt-get install wabt # 2. 配置并构建示例(在 WAMR 源码目录下执行) cd lib/wasm-micro-runtime-WAMR-2.4.1 cmake -S samples/ref-types -B build/ref-types -DWAMR_BUILD_REF_TYPES=1 cmake --build build/ref-types # 3. 运行(hello.wasm 由构建产物生成,位于 build 目录) cd build/ref-types && ./hello # 预期输出:GREAT! PASS ALL CHKs五、使用 externref 的最佳实践与注意事项
综合官方文档约束与示例实现,可总结以下实践要点:
- 始终以
uintptr_t承载 externref:无论是原生函数形参、wasm_val_t.of.foreign还是wasm_runtime_call_wasm的 argv 拆分,都要以sizeof(uintptr_t)为准,勿硬编码 4 或 8; - Wasm 侧用 table 长期持有引用:
externref想跨多个 Wasm 函数存活时,存入externref表(如示例中的table $t1)比局部变量更可靠; - 调用前校验
results[0].kind == WASM_EXTERNREF:类型不匹配时 WAMR 可能返回失败,显式校验可避免误读联合体; - 注意
wasm_runtime_call_wasm的 argv 拆包:8 字节句柄要像示例那样用 union 拆成两个 32 位分量,这是最容易出错的环节; - 对象生命周期由宿主负责:WAMR 对
externref不解引用、不管理其内存,宿主需自行保证句柄在 Wasm 使用期间有效。若需要运行时管理外部对象映射,可参考 wasm_export.h 提供的wasm_externref_obj2ref/wasm_externref_ref2obj/wasm_externref_retain/wasm_externref_set_cleanup等扩展 API。
六、总结
- reference-types 提案引入的
funcref与externref,使 Wasm 与宿主之间的引用级互操作成为可能; - WAMR 完整实现了该提案,
WAMR_BUILD_REF_TYPES默认开启,externref在内部被当作不透明值原样搬运; - 宿主侧通过
'r'签名注册原生函数,并借助wasm_runtime_call_wasm/_v/_a三种入口与 Wasm 双向传递uintptr_t句柄; - 位宽约束(64 位 8 字节 / 32 位 4 字节)是所有 externref 代码必须严格遵守的边界条件;
- 可运行的完整参考实现见 samples/ref-types,WAT 与 C 源码分别展示了 Wasm 侧表格存储与宿主侧句柄往返的完整闭环。
【免费下载链接】fluent-bitFast and Lightweight Logs, Metrics and Traces processor for Linux, BSD, OSX and Windows项目地址: https://gitcode.com/GitHub_Trending/fl/fluent-bit
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考