WAMR 模块实例上下文 API 与 wasi-threads 交互实战:深入解析 inst-context-threads 示例
2026/9/18 11:29:13 网站建设 项目流程

WAMR 模块实例上下文 API 与 wasi-threads 交互实战:深入解析 inst-context-threads 示例

【免费下载链接】fluent-bitFast and Lightweight Logs, Metrics and Traces processor for Linux, BSD, OSX and Windows项目地址: https://gitcode.com/GitHub_Trending/fl/fluent-bit

导读

本指南以 WAMR(WebAssembly Micro Runtime,版本 2.4.1,随本仓库以lib/wasm-micro-runtime-WAMR-2.4.1方式内嵌)中samples/inst-context-threads示例为主线,完整讲解「模块实例上下文(Module Instance Context)API」与「wasi-threads 多线程」如何协同工作:当 Wasm 应用内部创建多个线程时,宿主原生函数如何把一份上下文数据写入并扩散到该模块实例派生出的所有线程,实现跨线程共享。读完本文,你将掌握wasm_runtime_create_context_keywasm_runtime_set_context_spreadwasm_runtime_get_context等 API 的完整使用流程、WAMR 内部的扩散实现原理,以及如何构建和运行该示例。

示例概览:它到底演示了什么

官方 README(见 lib/wasm-micro-runtime-WAMR-2.4.1/samples/inst-context-threads/README.md)对示例的定位非常凝练:

This sample demonstrates some interactions between module instance context API and wasi-threads.

即:模块实例上下文 API 与 wasi-threads 之间的交互。具体来说,示例回答了一个在多线程 Wasm 场景下非常现实的问题:当一个 Wasm 模块实例内部用pthread_create派生出多个线程后,宿主侧通过上下文 API 写入的数据,能否被该实例的所有线程看到?

示例给出的答案和实现要点是:

  1. Wasm 应用(testapp.c)内部创建pthread线程,线程从宿主导入的get_context读到的初始值应为「未设置」(返回 -1);
  2. 线程内调用宿主导入的set_context(1234),把值写入上下文;
  3. 线程回读确认值为 1234;
  4. 主线程pthread_join之后回读,同样得到 1234——证明通过set_context_spread写入的上下文会扩散到模块实例对应的线程簇(cluster)中的所有执行环境

整个示例目录结构如下:

lib/wasm-micro-runtime-WAMR-2.4.1/samples/inst-context-threads/ ├── CMakeLists.txt # 宿主运行时的构建配置(开启 WASI threads、AOT/解释器) ├── README.md # 官方示例说明(本文的主体) ├── run.sh # 运行脚本:执行 out/inst-context -f out/wasm-apps/testapp.wasm ├── build.sh # 一键构建脚本:编译宿主程序与 wasm 应用 ├── src/ │ ├── main.c # 宿主侧主程序:运行时初始化、注册原生符号、加载/实例化模块 │ ├── native_impl.c # 原生函数实现:set_context / get_context │ └── my_context.h # 自定义上下文结构体与全局声明 └── wasm-apps/ └── testapp.c # Wasm 侧应用:使用 pthread + 导入的上下文 API

宿主侧实现:上下文 API 的完整调用链

宿主程序由src/main.c(见 main.c)与src/native_impl.c(见 native_impl.c)组成。

第一步:注册自定义上下文键

main.cwasm_runtime_full_init成功后,立即创建上下文键:

my_context_key = wasm_runtime_create_context_key(my_context_dtor); if (!my_context_key) { printf("wasm_runtime_create_context_key failed.\n"); return -1; }

要点说明:

  • wasm_runtime_create_context_key的签名是void *wasm_runtime_create_context_key(void (*dtor)(WASMModuleInstanceCommon *inst, void *ctx)),返回值是一个不透明的 key 句柄,后续所有 set/get 操作都以它为索引;
  • 可以同时注册一个析构回调dtor,当模块实例被销毁(deinstantiate)时,WAMR 会调用它清理该 key 关联的上下文数据。示例中的my_context_dtor做了两个断言:
void my_context_dtor(wasm_module_inst_t inst, void *ctx) { printf("%s called\n", __func__); my_dtor_called++; bh_assert(ctx == &my_context); /* 传回的正是全局上下文对象 */ bh_assert(inst == module_inst); /* 且关联的正是当前模块实例 */ }

这验证了析构回调的触发时机与参数正确性,主程序在wasm_runtime_deinstantiate前后分别断言my_dtor_called == 0my_dtor_called == 1(见 main.c),从而严格证明:析构回调只会在实例销毁时被调用一次

第二步:注册原生函数并导出到 Wasm 侧

示例通过NativeSymbol数组把两个 C 函数导出给 Wasm 应用(模块名env):

static NativeSymbol native_symbols[] = { { "set_context", set_context, "(i)", NULL }, { "get_context", get_context, "()i", NULL }, }; init_args.n_native_symbols = sizeof(native_symbols) / sizeof(NativeSymbol); init_args.native_module_name = "env"; init_args.native_symbols = native_symbols;
  • "(i)"表示set_context接收一个i32参数、无返回值;
  • "()i"表示get_context无参数、返回一个i32
  • 符号签名语法与doc/export_native_api.md描述的原生 API 导出规范一致;
  • 数组必须声明为 static(示例中注释明确说明「the array must be static defined since runtime will keep it after registration」),因为运行时在注册后会长期持有该数组。

第三步:原生函数如何读写上下文

native_impl.c中的两个函数是本示例的核心:

void set_context(wasm_exec_env_t exec_env, int32_t n) { wasm_module_inst_t inst = wasm_runtime_get_module_inst(exec_env); printf("%s called on module inst %p\n", __func__, inst); struct my_context *ctx = &my_context; ctx->x = n; wasm_runtime_set_context_spread(inst, my_context_key, ctx); } int32_t get_context(wasm_exec_env_t exec_env) { wasm_module_inst_t inst = wasm_runtime_get_module_inst(exec_env); struct my_context *ctx = wasm_runtime_get_context(inst, my_context_key); if (ctx == NULL) { return -1; /* 尚未设置上下文时的约定返回值 */ } return ctx->x; }

关键点:

  • 通过wasm_runtime_get_module_inst(exec_env)拿到当前执行环境对应的模块实例,这是上下文 API 的「归属对象」——上下文始终挂在模块实例上,而不是全局变量;
  • wasm_runtime_set_context_spread是「扩散」版本:它会把上下文写入该模块实例及其派生线程对应的所有执行环境(见下文的内部实现分析);
  • wasm_runtime_get_context在 key 未被写入时返回NULL,Wasm 侧据此约定-1表示「未设置」;
  • 上下文数据结构定义在 my_context.h 中,非常精简:
struct my_context { int x; }; extern void *my_context_key; extern struct my_context my_context;

这里my_context是宿主侧的一个全局对象,其指针被写入到每个线程的实例上下文中。

Wasm 侧实现:多线程视角验证上下文传播

Wasm 应用 testapp.c 使用pthread直接编写,导入两个函数:

void set_context(int32_t n) __attribute__((import_module("env"))) __attribute__((import_name("set_context"))); int32_t get_context() __attribute__((import_module("env"))) __attribute__((import_name("get_context")));

线程函数start的执行逻辑(带断言验证):

void * start(void *vp) { int32_t v; printf("thread started\n"); /* 新线程初始状态:上下文未设置,应为 -1 */ v = get_context(); assert(v == -1); /* 在线程内写入上下文 */ set_context(1234); /* 线程内回读,应为 1234 */ v = get_context(); assert(v == 1234); return NULL; }

主函数则验证「线程写入后,主线程也能读到」这一传播语义:

int main() { pthread_t t1; int32_t v; /* 初始状态:主线程未设置上下文 */ v = get_context(); assert(v == -1); /* 创建并等待线程执行完毕 */ ret = pthread_create(&t1, NULL, start, NULL); assert(ret == 0); ret = pthread_join(t1, &val); assert(ret == 0); /* 关键断言:上下文已从线程扩散回主线程 */ v = get_context(); assert(v == 1234); printf("success\n"); return 0; }

整个测试流程对应了三个递进式的验证点:

验证点断言说明
初始未设置get_context() == -1上下文键创建后默认无数据,NULL映射为-1
线程内写入与回读写入 1234 后get_context() == 1234set_context_spread至少对本线程执行环境生效
跨线程传播主线程 join 后get_context() == 1234上下文扩散到整个线程簇,主/子线程共享

提示:示例中的 Wasm 代码直接使用pthread_create/pthread_join,这正是 wasi-threads 提案提供的 API 形态——WAMR 通过lib-wasi-threads库将其映射到宿主线程实现(详见 thread_manager.c)。

内部实现原理:上下文如何「扩散」到所有线程

理解wasm_runtime_set_context_spread的底层实现,才能真正明白本示例的价值。该函数的公开声明位于 wasm_runtime_common.h,实现位于 wasm_runtime_common.c:

wasm_runtime_set_context_spread(WASMModuleInstanceCommon *inst, void *key, void *ctx) { wasm_native_set_context_spread(inst, key, ctx); }

wasm_native_set_context_spread(见 wasm_native.c)的关键分支是:

void wasm_native_set_context_spread(WASMModuleInstanceCommon *inst, void *key, void *ctx) { #if WASM_ENABLE_THREAD_MGR != 0 wasm_cluster_set_context(inst, key, ctx); #else wasm_native_set_context(inst, key, ctx); #endif }
  • 开启线程管理器(WASM_ENABLE_THREAD_MGR)时,走wasm_cluster_set_context集群级扩散
  • 未开启线程支持时,退化为只写当前实例(等价于普通wasm_runtime_set_context)。

线程簇(cluster)遍历:实现扩散的核心

wasm_cluster_set_context位于 thread_manager.c:

void wasm_cluster_set_context(WASMModuleInstanceCommon *module_inst, void *key, void *ctx) { WASMExecEnv *exec_env = wasm_clusters_search_exec_env(module_inst); if (exec_env == NULL) { /* Maybe threads have not been started yet. */ wasm_runtime_set_context(module_inst, key, ctx); } else { WASMCluster *cluster; struct inst_set_context_data data; data.key = key; data.ctx = ctx; cluster = wasm_exec_env_get_cluster(exec_env); bh_assert(cluster); os_mutex_lock(&cluster->lock); traverse_list(&cluster->exec_env_list, set_context_visitor, &data); os_mutex_unlock(&cluster->lock); } }

从中可以提炼出以下实现事实:

  1. 运行时为每个 Wasm 模块实例(以及它派生的线程执行环境WASMExecEnv)维护一个线程簇WASMCluster,簇内通过exec_env_list链表挂载所有执行环境;
  2. wasm_cluster_set_context先在集群中查找该实例对应的执行环境:若线程尚未启动(找不到),则退化为单实例写入(wasm_runtime_set_context);
  3. 若线程已经启动,则在持有cluster->lock的情况下遍历整个exec_env_list,通过set_context_visitor对每一个执行环境调用wasm_runtime_set_context写入同一份上下文——这正是「spread(扩散)」语义的来源;
  4. 加锁遍历保证了在多线程并发调用set_context时对上下文列表访问的一致性。

上下文键与析构回调的底层存储

在 wasm_native.c 中,上下文键的实现是一个「句柄即索引」的方案:

  • g_context_dtors[WASM_MAX_INSTANCE_CONTEXTS]是一个全局静态数组,保存每个 key 的析构回调;
  • wasm_native_create_context_key线性扫描数组中第一个空位,把 key 编码为idx + 1(避免 0 冲突),并注册析构回调(未提供时使用dtor_noop空实现);
  • wasm_native_destroy_context_key回收该槽位;
  • wasm_native_set_context/wasm_native_get_context通过context_key_to_idx把 key 还原为索引,然后读写WASMModuleInstanceExtraCommon中的contexts[idx]数组。

也就是说:每个模块实例的扩展公共结构体里都有一个「上下文槽位数组」,key 决定槽位,set/get 决定读写,而 spread 决定写入范围。

构建与运行:从源码到可执行程序

前提条件

  • 构建宿主程序需要 CMake(cmake_minimum_required (VERSION 3.14),见 CMakeLists.txt);
  • 构建 Wasm 应用需要wasi-sdk(20.0 或更高版本,必须带 wasi-threads 支持),因为示例使用了--target=wasm32-wasi-threads-pthread编译选项(见 build.sh);
  • 宿主平台为 Linux/BSD/macOS 等支持 pthread 的系统(Windows 平台下 CMake 工程会被调整为C ASM语言组合,见 CMakeLists.txt)。

构建步骤

执行仓库内的 build.sh:

cd lib/wasm-micro-runtime-WAMR-2.4.1/samples/inst-context-threads ./build.sh

脚本依次完成:

  1. cmake_build/下执行cmake ..make,构建宿主可执行文件inst-context,并拷贝到out/目录;
  2. 进入wasm-apps/目录,用 wasi-sdk 的 clang 编译所有.c文件为.wasm
/opt/wasi-sdk/bin/clang \ --target=wasm32-wasi-threads \ -pthread \ -Wl,--import-memory \ -Wl,--export-memory \ -Wl,--max-memory=655360 \ -o out/wasm-apps/testapp.wasm wasm-apps/testapp.c

其中--import-memory/--export-memory让 Wasm 内存与宿主共享、可供线程间可见,--max-memory=655360预留给多线程栈/堆空间。

CMake 侧的运行时特性开关

宿主程序构建时通过 CMake 显式开启了以下特性(见 CMakeLists.txt):

set (WAMR_BUILD_INTERP 1) # 解释器模式 set (WAMR_BUILD_AOT 1) # AOT 模式 set (WAMR_BUILD_JIT 0) # 关闭 JIT set (WAMR_BUILD_LIBC_BUILTIN 0) # 关闭内置 libc set (WAMR_BUILD_LIB_WASI_THREADS 1) # 开启 wasi-threads 支持(本示例的关键) if (NOT MSVC) set (WAMR_BUILD_LIBC_WASI 1) # 开启 WASI libc endif ()

值得注意:WAMR_BUILD_LIB_WASI_THREADS是让pthread_create等调用真正可用的前提,而运行时为 vmlib 链接了-lpthread,宿主程序最终链接vmlib -lm -ldl -lpthread(Linux 上还会附加-lrt)。

运行与预期输出

运行脚本 run.sh 内容即:

out/inst-context -f out/wasm-apps/testapp.wasm

其中-f指定 Wasm 文件路径(参见main.cprint_usage输出的Options: -f [path of wasm file])。

按执行顺序,预期输出大致为(各printf由原生函数与 Wasm 侧代码共同产生):

thread started confirming the initial state on thread get_context called on module inst 0x... confirming the context on thread set_context called on module inst 0x... ... confirming the context propagated from the thread on main get_context called on module inst 0x... success

程序以退出码 0 结束的前提是所有assert全部通过,包括:

  • 新线程初始状态下get_context()返回 -1;
  • 线程内set_context(1234)后回读为 1234;
  • 主线程pthread_join后回读仍为 1234(上下文已从线程扩散到主线程);
  • 实例销毁时my_context_dtor恰好被调用一次。

与同系列示例的对照:单实例版本的差异

本仓库还提供了一个不含线程的姊妹示例 samples/inst-context(同样包含src/main.csrc/native_impl.c,并使用了相同的上下文 API)。两者的核心差异在于:

  • inst-context:仅演示单个模块实例上的上下文 set/get 基本语义,不存在线程,因此即使使用wasm_runtime_set_context_spread,也只会落到单实例写入路径(wasm_clusters_search_exec_env找不到已启动线程时会走退化分支);
  • inst-context-threads:在 wasi-threads 场景下验证扩散语义,证明set_context_spread会沿线程簇把上下文同步给所有执行环境。

如果你需要对比「不扩散」与「扩散」两种 API 的行为差异,可以同时阅读这两个示例的native_impl.c,观察wasm_runtime_set_context(仅写当前实例)与wasm_runtime_set_context_spread(写整个集群)在源码层面的调用差异。

小结与工程启示

通过inst-context-threads示例,可以总结出模块实例上下文 API 的完整工程用法:

  1. 创建上下文键wasm_runtime_create_context_key(dtor)在运行时初始化后调用一次,注册自定义上下文槽位与析构回调;
  2. 注册原生符号:通过NativeSymbol数组 +init_args.n_native_symbols把宿主函数导出到env模块,签名语法遵循原生 API 导出规范;
  3. 写入与读取:原生函数内用wasm_runtime_get_module_inst(exec_env)定位实例,再调用wasm_runtime_set_context_spread/wasm_runtime_get_context读写上下文;
  4. 析构清理wasm_runtime_destroy_context_key在运行时销毁前回收 key,模块实例销毁时 dtor 精确触发一次,可用于释放宿主侧资源;
  5. 多线程语义set_context_spread在多线程场景下通过线程簇遍历(wasm_cluster_set_context加锁遍历exec_env_list)实现跨线程上下文同步,这是它区别于普通set_context的本质所在。

对于在 Wasm 中承载多线程业务、同时又需要宿主侧按线程簇注入配置或状态的场景(例如日志上下文、租户标识、请求级元数据的跨线程传播),这一套 API 提供了规范且线程安全的宿主侧解决方案,而本示例的完整代码正是最直接的可复现参考。

【免费下载链接】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),仅供参考

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

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

立即咨询