Wasmtime是Bytecode Alliance维护的高性能WebAssembly运行时,核心使用Rust实现,但它提供了稳定的C API,因此C++应用程序可以方便地嵌入Wasmtime来执行.wasm模块。要在C++宿主中调用Wasm函数,需要理解wasm_engine_t、wasm_store_t、wasmtime_module_t等句柄的生命周期,并按顺序完成引擎初始化、字节码加载、实例化、函数查找和调用。本文从环境准备开始,展现一个最小可运行的C++示例,并逐步讨论参数传递、错误处理和内存管理。

一、准备Wasmtime的C API环境
Wasmtime官方发布版中会包含头文件和静态库或动态库。头文件通常有两个:wasm.h是WebAssembly C API标准定义,wasmtime.h是Wasmtime扩展API。下载对应平台的压缩包后,将include目录加入编译器搜索路径,并链接libwasmtime库。Linux下常见的链接选项为-lwasmtime -lpthread -ldl -lm,Windows下则需要链接wasmtime.lib并确保运行时DLL在PATH中。
如果使用CMake,可以在CMakeLists.txt中配置include_directories和target_link_libraries。手动编译一个单文件程序时,可以使用类似下面的命令:g++ -std=c++17 main.cpp -I/path/to/wasmtime/include -L/path/to/wasmtime/lib -lwasmtime -lpthread -ldl -lm -o main。验证环境时,可以先调用wasm_engine_new并检查返回值不为空,这是后续所有操作的前提。
#include <wasm.h>
#include <wasmtime.h>
#include <stdio.h>
int main() {
wasm_engine_t* engine = wasm_engine_new();
if (!engine) {
printf("Failed to create engine\n");
return 1;
}
printf("Wasmtime engine created\n");
wasm_engine_delete(engine);
return 0;
}
二、加载Wasm模块并完成实例化
WebAssembly模块通常以二进制格式存储在.wasm文件中。Wasmtime C API提供了wasm_byte_vec_t来容纳字节序列。先读取文件内容到wasm_byte_vec_t,再通过wasmtime_module_new把字节编译成模块。需要注意的是,wasm_byte_vec_t需要初始化和释放,wasm_byte_vec_delete会释放内部缓冲区。
实例化模块前,需要创建一个wasm_store_t存储上下文,它关联着引擎和运行时状态。多个模块可以共享同一个存储,但不同存储之间的对象不能直接相互引用。调用wasmtime_instance_new时还要传入一个链接器wasmtime_linker_t,如果Wasm模块只使用自身函数而不导入宿主函数,可以传入一个空的链接器。下面的代码演示了从文件读取模块并实例化的完整过程,同时包含了基本的错误打印。
#include <wasm.h>
#include <wasmtime.h>
#include <stdio.h>
#include <stdlib.h>
int main() {
wasm_engine_t* engine = wasm_engine_new();
wasm_store_t* store = wasm_store_new(engine);
wasm_byte_vec_t wat_bytes;
wasm_byte_vec_new(&wat_bytes, 0, NULL);
FILE* file = fopen("module.wasm", "rb");
if (!file) return 1;
fseek(file, 0, SEEK_END);
long size = ftell(file);
fseek(file, 0, SEEK_SET);
wasm_byte_vec_new_uninitialized(&wat_bytes, size);
fread(wat_bytes.data, 1, size, file);
fclose(file);
wasmtime_module_t* module = NULL;
wasmtime_error_t* error = wasmtime_module_new(engine, &wat_bytes, &module);
if (error) {
wasm_name_t message;
wasmtime_error_message(error, &message);
printf("module error: %.*s\n", (int)message.size, message.data);
wasm_byte_vec_delete(&message);
wasmtime_error_delete(error);
return 1;
}
wasmtime_linker_t* linker = wasmtime_linker_new(engine);
wasmtime_instance_t instance;
error = wasmtime_instance_new(store, module, NULL, 0, &instance, NULL);
if (error) {
printf("instantiate error\n");
return 1;
}
printf("module instantiated\n");
wasmtime_linker_delete(linker);
wasmtime_module_delete(module);
wasm_byte_vec_delete(&wat_bytes);
wasm_store_delete(store);
wasm_engine_delete(engine);
return 0;
}
上面的示例中wasmtime_instance_new的参数imports和num_imports可以传NULL和0,因为模块没有外部导入。如果模块需要宿主函数,则应先通过wasmtime_linker_define注册函数,再把导入数组传入实例化接口。实例化成功后,instance是一个值类型对象,生命周期由store管理,不需要单独删除。
三、查找导出函数并执行调用
Wasm模块的导出项可以是函数、全局变量、内存或表。要调用函数,先通过wasmtime_instance_export_get获取导出项,并判断类型是否为WASM_EXTERN_FUNC。导出项在Wasmtime C API中表示为wasmtime_extern_t,其中kind字段标识具体类型。函数对象可以从of.func中取得。
调用函数前,需要构造参数数组和结果数组。C API使用wasm_val_t结构表示值,它的kind成员可以是WASM_I32、WASM_I64、WASM_F32、WASM_F64等。对于整数参数,直接设置val.kind和val.of.i32即可。调用wasmtime_func_call后,结果会写入预先分配的结果数组。下面演示调用一个名为add的导出函数,它接受两个i32参数并返回它们的和。
// 假设 store 和 instance 已经创建好
wasmtime_extern_t item;
bool ok = wasmtime_instance_export_get(store, &instance, "add", 3, &item);
if (!ok || item.kind != WASMTIME_EXTERN_FUNC) {
printf("export not found\n");
return 1;
}
wasmtime_func_t func = item.of.func;
wasm_val_t params[2];
params[0].kind = WASM_I32;
params[0].of.i32 = 20;
params[1].kind = WASM_I32;
params[1].of.i32 = 22;
wasm_val_t results[1];
wasmtime_error_t* error = wasmtime_func_call(store, &func, params, 2, results, 1, NULL);
if (error) {
printf("call failed\n");
return 1;
}
printf("20 + 22 = %d\n", results[0].of.i32);
对于返回值为空的函数,结果数组可以传NULL,结果个数传0。参数数组和结果数组都会由wasmtime_func_call内部读取或写入,调用结束后如果值是引用类型,需要注意手动释放。基础数字类型不需要清理。
四、处理字符串参数与内存操作
Wasm函数签名中的字符串通常不是一等公民,而是通过线性内存中的地址和长度来表示。宿主端要把字符串传给Wasm函数,需要先获取模块导出的内存对象,分配或找到可用地址,然后用memcpy把字节写入内存,最后把地址和长度作为i32参数传递。对于返回字符串,则要从结果参数中读取地址和长度,再从内存中复制出来。
Wasmtime C API中可以使用wasmtime_extern_t获取WASMTIME_EXTERN_MEMORY类型的导出项,再通过wasmtime_memory_data拿到宿主可访问的指针。写入前要注意内存是否足够,必要时可以在Wasm侧调用分配函数或设计一个静态缓冲区。由于WebAssembly内存是线性的,宿主进程中的地址与Wasm内部地址一致,因此这种操作比较直观,但要避免越界和生命周期问题。
五、用RAII封装降低资源管理复杂度
上面的C API演示中,每个对象都有对应的delete函数,手动管理容易遗漏。在C++中,可以使用std::unique_ptr配合自定义删除器自动释放引擎、存储、模块等句柄。例如定义using EnginePtr = std::unique_ptr<wasm_engine_t, decltype(&wasm_engine_delete)>;,之后在作用域结束时自动调用删除函数。
对于值类型对象如wasmtime_instance_t,它们由store统一管理,不需要单独删除,但要注意store必须比实例、函数、内存等对象活得久。跨线程使用同一个store需要加锁,Wasmtime本身并不保证线程安全,建议每个线程创建独立的store与实例。
完整的C++封装还可以提供load_module、call_function等便捷方法,把错误统一转成异常或std::expected,让业务代码更清晰。掌握这些基础后,你便能在C++服务中安全地嵌入Wasmtime,将Wasm模块作为插件或沙箱化逻辑运行。
WasmtimeC++嵌入WebAssemblyWasm函数调用修改时间:2026-10-05 05:19:55