WAMR 多模块(Multi-Module)依赖加载机制完全指南:从 import/export 到动态链接实战
【免费下载链接】fluent-bitFast and Lightweight Logs, Metrics and Traces processor for Linux, BSD, OSX and Windows项目地址: https://gitcode.com/GitHub_Trending/fl/fluent-bit
导读
WASM 模块可以通过 import 机制依赖其他模块,WAMR(WebAssembly Micro Runtime)提供了一整套多模块注册、按需加载与跨模块函数调用的 API。本文以 WAMR 官方文档 multi_module.md 为骨架,结合仓库内 multi-module 示例工程 的完整源码,系统讲解多模块的依赖声明、Command/Reactor 编程模型、构建开关、宿主侧回调实现以及跨模块查找函数的方法,让你能够从零搭建一个"主模块 + 多个子模块"的 WASM 应用。
一、WASM 多模块模型:import 与 export 的依赖关系
WASM 模块可以像传统动态库一样相互依赖:一个模块通过import引入其他模块的函数(functions)、全局变量(globals)、内存(memories)和表(tables),同时通过export将这些实体暴露给其他模块使用。WAMR 会在加载主模块时,根据其 import section递归加载所有依赖的子模块。
;; 伪代码示意:主模块从 "mB" 导入函数 B1 (import "mB" "B1" (func $B1 (result i32)))关键前提(文档明确说明):
- WAMR 目前仅实现 load-time(加载期)动态链接,即所有模块在实例化前一次性解析完依赖关系,不支持运行期的动态加载/dlopen 语义;
- 多模块的加载顺序完全由主模块 import section 中声明的依赖关系驱动,WAMR 内部按依赖树递归完成。
依赖的递归加载是在 wasm_loader.c 与 wasm_runtime.c 等核心文件中实现的,二者均受
WASM_ENABLE_MULTI_MODULE宏控制。
WASI Command/Reactor 模型
WAMR 的多模块设计遵循WASI Command/Reactor 模型,它将模块划分为两类角色:
| 角色 | 说明 | 必备导出 |
|---|---|---|
| Command(命令) | 主模块,程序入口,依赖 reactor 提供的导出 | 必须导出_start() |
| Reactor(反应器) | 子模块/库模块,被 Command 或其他 reactor 调用 | 必须导出_initialize() |
当编译时开启WASM_ENABLE_LIBC_WASI,且任意模块导入了 WASI API(形如(import "wasi_snapshot_preview1" "XXX"))时,必须遵守上述 WASI Application ABI 限制:
- 主模块(command)应包含
_start(); - 子模块(reactor)应包含
_initialize(); - command 与 reactor 都必须导出
memory——因为跨模块函数调用共享同一线性内存空间,被调用方必须能访问调用方的内存导出。
二、多模块相关 API 全景
1. 注册模块:wasm_runtime_register_module
bool wasm_runtime_register_module(const char *module_name, wasm_module_t module, char *error_buf, uint32_t error_buf_size);用于以指定的module_name将一个模块注册到运行时。它的典型场景是主模块:主模块由wasm_runtime_load()直接加载,没有机会向运行时告知自己的模块名;而它又可能被其他模块 import(例如示例中的 mC 注册后即可被查找),因此需要显式注册。而子模块的名字来自主模块 import section 中的声明,WAMR 会据此从文件系统或流中读取对应的.wasm文件,并在内部自动注册,无需宿主手动注册。
在 samples/multi-module/src/main.c 中可以看到,mC 作为主模块通过
wasm_runtime_register_module("mC", module, ...)注册,随后wasm_runtime_find_module_registered("mC")能返回与module相同的指针。
2. 查找已注册模块:wasm_runtime_find_module_registered
wasm_module_t wasm_runtime_find_module_registered( const char *module_name);用于校验某个module_name是否已被注册,若已注册则返回对应模块句柄,否则返回 NULL。示例 main.c 用它验证 mA、mB 被自动注册、mC 注册前后行为差异(见 main.c#L133-L157)。
3. 模块读取器与销毁器回调
typedef bool (*module_reader)(const char *module_name, uint8_t **p_buffer, uint32_t *p_size); typedef void (*module_destroyer)(uint8_t *buffer, uint32_t size); void wasm_runtime_set_module_reader(const module_reader reader, const module_destroyer destroyer);WAMR 的设计哲学是:文件系统与存储完全交给宿主(embedding environment)负责。宿主只需把子模块的二进制内容交给运行时,运行时不关心这些字节来自文件、网络流还是内存。因此:
module_reader:在动态加载子模块时被调用,宿主需要根据module_name读出二进制内容到*p_buffer,并返回实际大小*p_size;module_destroyer:在卸载子模块时被调用,宿主负责释放reader分配的内存;- 两个回调都必须由开发者自行实现,
wasm_runtime_set_module_reader在运行时初始化后、加载主模块前调用。
4. 调用子模块导出函数:wasm_runtime_lookup_function
wasm_function_inst_t wasm_runtime_lookup_function(wasm_module_inst_t const module_inst, const char *name);多模块模式下,该 API 支持两种函数名格式:
| 名称格式 | 查找范围 |
|---|---|
function_name(仅函数名) | 默认在父模块(主模块)中查找 |
$submodule_name$function_name | 在指定子模块中查找导出函数,例如$mA$A1 |
其中signature参数可传 NULL(由运行时自行解析函数签名)。
三、完整实战:mA / mB / mC 三模块依赖链
仓库中的 samples/multi-module 提供了完整可运行的示例,其依赖关系如下:
图:示例中三个模块的 import/export 依赖关系(原图见 doc/pics/multi_module_pic1.png)。
依赖拓扑为:
- mA(reactor):只导出 A1,不依赖任何模块;
- mB(reactor):从 mA 导入 A1,导出 B1、B2;
- mC(command,主模块):从 mA 导入 A1、从 mB 导入 B1、B2,导出 C1~C4,并拥有
main()。
1. 用attribute声明 import/export
C/C++ 侧通过两种__attribute__声明跨模块依赖与导出:
// 声明依赖:当前模块从 MODULE_NAME 导入 FUNCTION_NAME __attribute__((import_module("MODULE_NAME"))) __attribute__((import_name("FUNCTION_NAME"))) extern int FUNCTION_NAME(); // 声明导出:将函数暴露给其他模块 __attribute__((export_name("FUNCTION_NAME"))) int FUNCTION_NAME() { ... }以仓库源码 mA.c 为例:
/* mA.c —— 纯 reactor,不需要 main() */ __attribute__((export_name("A1"))) int A1() { return 11; } int A2() { return 12; /* 未加 export_name,不对外导出 */ }mB.c 演示了"导入 + 再导出"的链条:
/* mB.c —— 从 mA 导入 A1,导出 B1、B2 */ __attribute__((import_module("mA"))) __attribute__((import_name("A1"))) extern int A1(); __attribute__((export_name("B1"))) int B1() { return 21; } __attribute__((export_name("B2"))) int B2() { return A1(); /* 内部调用导入函数 */ }mC.c 则是主模块,同时从 mA、mB 导入多个函数:
/* mC.c —— command,拥有 main() */ #include <stdio.h> __attribute__((import_module("mA"))) __attribute__((import_name("A1"))) extern int A1(); __attribute__((import_module("mB"))) __attribute__((import_name("B1"))) extern int B1(); __attribute__((import_module("mB"))) __attribute__((import_name("B2"))) extern int B2(); __attribute__((export_name("C1"))) int C1() { return 31; } __attribute__((export_name("C2"))) int C2() { return B1(); } __attribute__((export_name("C3"))) int C3() { return A1(); } __attribute__((export_name("C4"))) int C4() { return B2(); } int C5() { return C1() + C2() + C3() + 35; } /* 未导出 */ int main() { printf("%u\n", C5()); return EXIT_SUCCESS; }注意 C5 没有加
export_name属性。示例 main.c 中wasm_application_execute_func(module_inst, "C5", ...)因此调用失败,这正是对"未导出函数不可跨模块访问"的刻意验证。
2. 编译选项:command vs reactor
使用 WASI SDK 的 clang 分别编译:
# 生成 command(主模块),默认 exec-model $ /path/to/wasi-sdk/bin/clang -o command.wasm main_module.c # 生成 reactor(子模块) $ /path/to/wasi-sdk/bin/clang -mexec-model=reactor -o reactor.wasm submodule.c仓库的 wasm-apps/CMakeLists.txt 通过封装函数精确控制每种角色的编译方式:
compile_with_clang(mA.c OFF) # reactor compile_with_clang(mB.c OFF) # reactor compile_with_clang(mC.c ON) # command compile_with_clang(mD.cpp ON) # command(C++ 示例) compile_with_clang(mE.cpp OFF) # reactor(C++ 示例)其中compile_with_clang内部通过target_link_options(... PRIVATE -mexec-model=reactor)(见 CMakeLists.txt#L62-L67)为 reactor 追加链接选项。因此在本例中:mA、mB 是 reactor(子模块),mC 是 command(主模块)。
C++ 示例 mD/mE 还额外演示了__attribute__((constructor))/__attribute__((destructor))构造析构函数在模块生命周期中的执行顺序,以及import_module与import_name的合并写法(见 mD.cpp 与 mE.cpp)。
3. 构建 vmlib:开启 WAMR_BUILD_MULTI_MODULE
要启用多模块支持,构建 WAMR vmlib(核心库)时必须开启WAMR_BUILD_MULTI_MODULE选项。根据 build_wamr.md 中的说明,该选项默认关闭:
WAMR_BUILD_MULTI_MODULE=1/0, default to disable if not set该开关在 config_common.cmake 中转换为WASM_ENABLE_MULTI_MODULE宏,最终写入 core/config.h,并贯穿解释器(interpreter)、AOT 与 fast-jit 各执行模式。示例 README.md 还指出:多模块在解释器与 AOT 模式下均受支持,运行模式由主模块的类型(.wasm或.aot)决定。
4. 宿主侧代码:回调 + 内存池 + 加载调用
参照 main.c,宿主侧共分三步。
第一步:实现两个回调,负责按模块名加载/释放 WASM 二进制。
static char *module_search_path = "."; static bool module_reader_callback(package_type_t module_type, const char *module_name, uint8 **p_buffer, uint32 *p_size) { char *file_format = NULL; #if WASM_ENABLE_INTERP != 0 if (module_type == Wasm_Module_Bytecode) file_format = ".wasm"; #endif #if WASM_ENABLE_AOT != 0 if (module_type == Wasm_Module_AoT) file_format = ".aot"; #endif bh_assert(file_format != NULL); const char *format = "%s/%s%s"; int sz = strlen(module_search_path) + strlen("/") + strlen(module_name) + strlen(file_format) + 1; char *wasm_file_name = wasm_runtime_malloc(sz); if (!wasm_file_name) { return false; } snprintf(wasm_file_name, sz, format, module_search_path, module_name, file_format); *p_buffer = (uint8_t *)bh_read_file_to_buffer(wasm_file_name, p_size); wasm_runtime_free(wasm_file_name); return *p_buffer != NULL; } static void module_destroyer_callback(uint8 *buffer, uint32 size) { if (!buffer) { return; } wasm_runtime_free(buffer); buffer = NULL; }注意:示例中的 reader 会根据运行模式(解释器Wasm_Module_Bytecode/ AOTWasm_Module_AoT)自动追加.wasm或.aot后缀,这正是 README 中"如果构建了 wamrc,还会生成 aot 文件"的配套逻辑。
第二步:创建大缓冲区,让 WAMR 的所有分配都来自该内存池。
static char sandbox_memory_space[10 * 1024 * 1024] = { 0 };在初始化参数中指定Alloc_With_Pool分配模式:
init_args.mem_alloc_type = Alloc_With_Pool; init_args.mem_alloc_option.pool.heap_buf = sandbox_memory_space; init_args.mem_alloc_option.pool.heap_size = sizeof(sandbox_memory_space);第三步:组装完整流程——初始化、注册 reader、加载主模块、递归加载子模块、实例化、跨模块调用。
/* 1. 初始化运行时 */ if (!wasm_runtime_full_init(&init_args)) { printf("Init runtime environment failed.\n"); goto EXIT; } /* 2. 注册 reader/destroyer(必须启用 MULTI_MODULE) */ wasm_runtime_set_module_reader(module_reader_callback, module_destroyer_callback); /* 3. 读取主模块 mC 的二进制 */ file_buf = (uint8 *)bh_read_file_to_buffer(wasm_file, &file_buf_size); /* 4. 加载 mC,WAMR 依据 import section 自动加载 mA、mB */ module = wasm_runtime_load(file_buf, file_buf_size, error_buf, sizeof(error_buf)); /* 5. 实例化 */ module_inst = wasm_runtime_instantiate(module, stack_size, heap_size, error_buf, sizeof(error_buf)); /* 6. 跨模块调用:C2 -> mB.B1(),C3 -> mA.A1(),C4 -> mB.B2() -> mA.A1() */ wasm_application_execute_func(module_inst, "C1", 0, args); /* 返回 0x1f */ wasm_application_execute_func(module_inst, "C2", 0, args); /* 返回 0x15 */ wasm_application_execute_func(module_inst, "C3", 0, args); /* 返回 0xb */ wasm_application_execute_func(module_inst, "C4", 0, args); /* 返回 0xb */运行示例(见 README.md):
$ mkdir build && cd build $ cmake .. && make $ ./multi_module mC.wasm # 解释器模式 $ ./multi_module mC.aot # AOT 模式(需预先用 wamrc 编译)运行时输出验证了依赖链的正确性:C2 经 mB.B1 返回 21(0x15),C3 直接调用 mA.A1 返回 11(0xb),C4 通过 mB.B2 间接调用 mA.A1 同样返回 11(0xb);而 C5 因未导出,调用以失败告终,与代码注释预期完全一致。
四、小结与适用边界
| 要点 | 结论 |
|---|---|
| 加载机制 | load-time 动态链接,WAMR 按主模块 import section 递归加载依赖 |
| 编程模型 | WASI Command(主,_start())/ Reactor(子,_initialize()),二者均须导出 memory |
| 构建开关 | WAMR_BUILD_MULTI_MODULE=1(默认关闭),运行时宏为WASM_ENABLE_MULTI_MODULE |
| 宿主职责 | 必须实现 module_reader / module_destroyer 回调,并负责文件系统访问 |
| 跨模块调用 | $submodule_name$function_name格式定位子模块导出函数 |
| 模式支持 | 解释器与 AOT 均支持,模式由主模块类型决定 |
多模块能力让 WAMR 在资源受限的嵌入式场景下也能实现"库式"代码复用与模块化拆分——宿主只需关注文件的读写,运行时的依赖解析、注册与跨模块寻址全部由 WAMR 内部完成。若需深入验证源码行为,可继续阅读 wasm_loader.c 中load_from_sections对 import 段与子模块的解析逻辑,以及 wasm_runtime_common.c 中 reader/destroyer 与模块注册表的实现。
【免费下载链接】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),仅供参考