WAMR Reference Types 详解:用 externref / funcref 打通 Wasm 与宿主环境的对象互操作
2026/9/18 14:21:30 网站建设 项目流程

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时尤其要牢记这一点。

这条约束的深层含义:

  1. externref在宿主侧就是一个整数宽度的句柄:WAMR 不做解析、不做引用计数语义上的深拷贝,仅把该整数原样搬运进/出 Wasm;
  2. 跨平台移植性风险:如果你的宿主对象指针在 64 位平台是 8 字节,那么所有涉及externref的 NativeSymbol 签名、wasm_runtime_call_wasm*的参数组装都必须按 8 字节对齐处理,否则会出现截断;
  3. 调用约定:在 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_indirect

4.1 Wasm 侧:hello.wat

hello.wat 完整展示了externreffuncref的两种使用场景:

(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 $t2elem段直接把导入的原生函数引用预填充进表,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_tof.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_awasm_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_initwasm_runtime_loadwasm_runtime_instantiatewasm_runtime_create_exec_envwasm_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 的最佳实践与注意事项

综合官方文档约束与示例实现,可总结以下实践要点:

  1. 始终以uintptr_t承载 externref:无论是原生函数形参、wasm_val_t.of.foreign还是wasm_runtime_call_wasm的 argv 拆分,都要以sizeof(uintptr_t)为准,勿硬编码 4 或 8;
  2. Wasm 侧用 table 长期持有引用externref想跨多个 Wasm 函数存活时,存入externref表(如示例中的table $t1)比局部变量更可靠;
  3. 调用前校验results[0].kind == WASM_EXTERNREF:类型不匹配时 WAMR 可能返回失败,显式校验可避免误读联合体;
  4. 注意wasm_runtime_call_wasm的 argv 拆包:8 字节句柄要像示例那样用 union 拆成两个 32 位分量,这是最容易出错的环节;
  5. 对象生命周期由宿主负责:WAMR 对externref不解引用、不管理其内存,宿主需自行保证句柄在 Wasm 使用期间有效。若需要运行时管理外部对象映射,可参考 wasm_export.h 提供的wasm_externref_obj2ref/wasm_externref_ref2obj/wasm_externref_retain/wasm_externref_set_cleanup等扩展 API。

六、总结

  • reference-types 提案引入的funcrefexternref,使 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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询