ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

WAMR Reference Types 详解:用 externref / funcref 打通 Wasm 与宿主环境的对象互操作

WAMR Reference Types 详解:用 externref / funcref 打通 Wasm 与宿主环境的对象互操作 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 与 externrefreference-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.htypedef 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_TYPES1/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.wathello.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 $t1Wasm 模块内部用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.chello.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-externrefunion { 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_awasm_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_twasm_runtime_call_wasm_v变参列表结果用wasm_val_t.of.foreign接收wasm_runtime_call_wasm_awasm_val_t数组携带 kind参数/结果都用WASM_EXTERNREFof.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_TYPES1 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 或 8Wasm 侧用 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-typesWAT 与 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),仅供参考
返回列表