# WASM 扩展 ## 功能 WASM 扩展使用 WASM/WASI 模块作为后端入口,适合用 Rust 编写高性能逻辑、复杂状态管理或需要更清晰 ABI 边界的扩展。它的 `manifest.kind` 为 `wasm`,`manifest.abi` 为 `bts-wasi-1`。 BT 提供 `bt-extension-sdk` Rust SDK,帮助扩展作者处理 BtValueBinary 编解码、WASM 线性内存分配释放、调用 ID 分发和扩展对象句柄。 ## 语法 WASM 扩展通常使用 `module.wasm` 作为入口: ```json { "kind": "wasm", "abi": "bts-wasi-1", "entry": "module.wasm" } ``` WASM 模块必须导出: | 导出 | 说明 | | ------ | ------ | | `memory` | WASM 线性内存。 | | `bts_alloc(len) -> ptr` | 给宿主写入参数申请内存。 | | `bts_call(id, args_ptr, args_len) -> packed_ptr_len` | 根据调用 ID 执行业务逻辑并返回结果缓冲区。 | | `bts_free(ptr, len)` | 释放宿主传回的参数或返回值内存。 | 模块可以额外导出这些函数: | 导出 | 说明 | | ------ | ------ | | `bts_set_module_id(module_id)` | 宿主实例化模块后调用,SDK 用该模块 ID 创建扩展对象句柄。 | | `bts_init(config_ptr, config_len) -> packed_ptr_len` | worker 初始化后调用,参数是宿主整理后的 runtime 配置对象。 | | `bts_shutdown() -> packed_ptr_len` | worker 正常退出前调用,用于释放扩展内部连接或缓存。 | | `bts_stats() -> packed_ptr_len` | 返回扩展侧统计对象,供宿主后续调试接口汇总。 | 生命周期导出是可选的。旧 WASM 扩展不导出这些函数时仍可正常运行;如果导出,签名必须和表格一致。 ## 参数 WASM 扩展收到的参数来自 BtValueBinary 编码后的数组。入口函数参数按 bindings 顺序传入;对象方法参数会在最前面额外收到接收者 `ExtObject` 句柄。 ## 返回值 WASM handler 通过 SDK 返回 `BtValue`。返回原始类型时必须和 bindings 的 `returns` 匹配;返回对象类型时必须返回当前模块创建的同类型 `ExtObject`。 ## Rust SDK 流程 创建 WASM 扩展: ```text bt ext new calc_sdk --kind wasm cd calc_sdk rustup target add wasm32-wasip1 cargo build --target wasm32-wasip1 --release copy target\wasm32-wasip1\release\calc_sdk.wasm module.wasm bt ext build . -o calc_sdk.bts ``` 脚手架中的核心结构类似: ```rust bt_extension! { 1 => entry_create, 2 => entry_add, 3 => entry_value, 4 => entry_close, } ``` 这里的数字必须和 `bindings.json` 里的函数或方法 `id` 一致。 如果扩展需要生命周期导出,可以使用 SDK 提供的独立 helper 宏: ```rust bt_extension_init!(init_worker); bt_extension_shutdown!(shutdown_worker); bt_extension_stats!(stats_worker); ``` `bt_extension!` 仍只负责 `bts_alloc`、`bts_free`、`bts_set_module_id` 和 `bts_call`,不会强制旧扩展导出生命周期函数。 ## 对象句柄 WASM 扩展不能把 Rust 对象直接交给 BT 脚本。正确做法是: 1. 扩展内部用 `ObjectStore` 保存真实状态。 2. 返回 `ExtObject` 句柄给 BT。 3. 脚本调用对象方法时,宿主把这个句柄作为第一个参数传给 WASM handler。 4. handler 根据对象 ID 找到扩展内部状态,再执行方法。 脚本侧仍然是普通链式调用: ```bt value = calc(3).add(7).value() // 输出:10 print value ``` 脚本中的 `type()` 会返回 bindings 对象名,而不是低层 ABI 标签: ```bt object = calc(3) // 输出:Calc print type(object) ``` ## BtValueBinary `bts-wasi-1` 使用 BtValueBinary 在宿主和 WASM 之间传输值。它会保留 `empty` 与 `null` 的区别,并支持普通值、Bytes、数组、对象和扩展对象句柄。 不支持通过 ABI 传输函数、类实例、正则、标准库对象、任务、定时器、迭代器或循环引用数组/对象。 ## shared runtime WASM 扩展默认使用 `runtime.mode: "thread_local"`,运行态会按调用线程缓存。需要跨请求复用昂贵初始化状态、连接或缓存时,可以在 `manifest.json` 中声明 `runtime.mode: "shared"`。 shared WASM 扩展会使用项目级 `ExtensionService`: 1. 调用线程按 bindings 处理参数和路径 role。 2. 调用线程把参数编码成 BtValueBinary 字节。 3. 服务把调用 ID、返回类型、调用标签、参数字节和回复通道投递到有界 worker 队列。 4. worker 内持有独立 WASM Store/Instance,调用 `bts_call`。 5. worker 返回结果字节,调用线程解码成 BT 值。 6. 如果返回扩展对象,宿主把 worker 本地对象 ID 改写成脚本可见 `host_object_id`;对象方法调用时再改写回创建 worker 的本地对象 ID。 当前阶段支持 shared 入口函数和对象方法返回原始值或扩展对象。`lifecycle: "dispose"` 的 `close()` / `dispose()` 方法成功后,宿主会移除对象路由,旧句柄再次调用会报已失效。 shared worker 会启用 Wasmtime epoch interrupt。`call_timeout_ms` 到期时,调用线程会标记目标 worker 并触发 epoch 检查;该 worker 捕获超时 trap 后会丢弃当前 WASM Store/Instance 并重建,后续调用不会被永久占用的 worker 卡住。 ## 注意事项 - WASM 模块包含 start section 会被拒绝。 - `thread_local` WASM Runner 会延迟实例化模块,并在线程本地缓存实例。 - `shared` WASM 扩展使用独立有界 worker 队列;队列满或服务关闭时会返回中文错误,不会落回线程本地 Runner。 - `shared` WASM 扩展多 worker 下会使用宿主级对象路由,避免不同 worker 内相同本地对象 ID 互相冲突。 - `shared` WASM 扩展调用超时会统计 timeout,并中断目标 worker 的 WASM 执行;其他 worker 不会因为同一个超时调用被强制重建。 - 可选生命周期导出只在存在时调用;不导出 `bts_init`、`bts_shutdown`、`bts_stats` 的旧扩展保持兼容。 - 普通 `object` 返回类型可以用 `empty` 表示无结果,例如 SQLite `one()` 查不到行;具体扩展对象返回类型仍必须返回同模块、同类型对象句柄。 - WASM 返回对象类型时,必须返回同模块、同类型的扩展对象句柄。 - SDK 内部错误信息仍可使用 `ext_object` 这类 ABI 分类;脚本层 `type()` 返回的是 `bindings.json` 声明的对象类型名。 - 长期持有状态的 WASM 对象应提供 `close()` 或 `dispose()`,并在 handler 中释放扩展内部状态。 - `bt ext new --kind wasm` 生成的是 Rust SDK 项目骨架;仍需要先编译 WASM 并复制为 `module.wasm` 后再打包。