扩展运行机制
扩展运行机制
功能
扩展机制把“脚本能看到的 API”和“扩展内部如何执行”分开。脚本只通过 bindings.json 里声明的入口、对象和方法调用扩展;BT 宿主负责加载 .bts 包、校验描述文件、建立注册表,并把调用分发给对应 Runner。
语法
扩展入口调用语法和普通函数一致:
object = calc(1) value = object.add(2).value() // 输出:3 print value
calc 来自 bindings.json 的公开入口声明,add 和 value 来自对象方法声明。
参数
扩展调用的参数由 bindings 决定。宿主会按 bindings 的参数数量、类型和 role 处理脚本传入的值,再把调用交给对应 Runner。
返回值
扩展调用可以返回原始值,也可以返回扩展对象句柄。对象句柄只表示“某个扩展模块中的某个对象”,真实状态由扩展后端保存。
脚本可以用 type(object) 查看扩展对象在 bindings.json 中声明的对象类型名:
object = calc(1) // 输出:Calc print type(object)
加载流程
项目启动时,BT 会按以下流程处理扩展:
1. 查找项目根目录下的 extensions/。
2. 按文件名顺序读取目录中的 .bts 文件。
3. 校验 .bts 后缀、zip 条目路径、条目数量、单文件大小和总解压大小。
4. 读取并校验 manifest.json。
5. 读取并校验 bindings.json。
6. 根据 manifest.kind 和 manifest.runtime.mode 初始化纯 BT Runner、线程本地 WASM Runner 或项目级 ExtensionService。
7. 建立扩展注册表,检查公开入口名是否冲突。
8. 把公开入口注入到当前 VM 的用户全局环境。
加载失败时,项目启动会失败并输出中文错误;BT 不会加载半成功的扩展包。
调用流程
脚本调用扩展入口时:
宿主会按入口名 calc 找到 bindings 中的函数声明,检查参数数量和类型,再把调用交给扩展所属 Runner 或 shared 服务。返回值如果是普通值,就直接回到脚本;如果是扩展对象,脚本拿到的是一个受宿主管理的对象句柄。
脚本调用对象方法时,宿主会先根据对象句柄找到扩展模块和对象类型,再查找方法名。WASM 方法调用时,接收者对象句柄会作为第一个参数传给 WASM;纯 BT 方法调用时,宿主会在内部 VM 中把方法调用到对应对象上。
runtime.mode=shared 的 WASM 扩展会把参数先编码成 BtValueBinary 字节,再投递到项目级 ExtensionService 的有界 worker 队列。worker 线程只接收调用 ID、返回类型、调用标签、参数字节和回复通道,返回结果字节后由调用线程解码,避免把 VM 内部 Value 跨线程传递。
shared 扩展返回对象时,worker 返回的是本地对象 ID。宿主会把它登记为脚本可见的 host_object_id,并记录 host_object_id -> worker_id + local_object_id + type_id。后续对象方法调用会先查这张表,把接收者改回 worker 本地对象 ID,并投递到创建该对象的 worker。lifecycle=dispose 的方法成功返回后,宿主会移除这条路由。
shared worker 使用 Wasmtime epoch interrupt 处理 call_timeout_ms。调用超时时,调用线程会标记目标 worker 并推进模块 Engine 的 epoch;只有被标记的 worker 会在 epoch callback 中返回超时 trap。该 worker 随后清理超时标记、丢弃旧 Store/Instance 并重建运行态,其他 worker 会继续执行。
如果 WASM 模块导出 bts_init、bts_shutdown 或 bts_stats,Runner 会在 worker 初始化、正常退出或统计读取路径调用它们;未导出这些函数的旧扩展保持兼容。
kind 和 abi
kind 是后端类型,决定入口文件由谁执行。abi 是调用协议版本,决定宿主和后端如何交换参数、返回值和对象句柄。
| kind | abi | Runner |
|---|---|---|
bt | bts-bt-1 | 纯 BT Runner,复用 BT Parser、Compiler 和 VM。 |
wasm | bts-wasi-1 | 线程本地 WASM Runner 或 shared ExtensionService,使用 WASI P1 和 BtValueBinary。 |
这两个字段不能随意组合。kind=bt 必须使用 bts-bt-1,kind=wasm 必须使用 bts-wasi-1。
代码示例
同一个 bindings.json 风格可以对应不同后端。脚本侧调用保持一致:
value = calc(4).add(6).value() // 输出:10 print value
差别只在扩展包内部:纯 BT 扩展由 src/lib.bt 实现;WASM 扩展由 module.wasm 实现。
注意事项
- 公开入口会成为全局常量,脚本不能重新赋值。
-
env('calc')可以读取扩展入口,has_env('calc')返回true。 -
envs('calc')和has_envs('calc')只表示系统函数和系统常量,不会把扩展入口当作系统能力。 - 扩展入口名不能和 BT 系统环境名称冲突,也不能和其他扩展入口重复。
-
type()返回扩展对象的脚本可见类型名,例如Calc;真实方法路由仍由宿主根据模块 ID、类型 ID 和对象 ID 完成。 - 扩展对象不应跨越不支持对象句柄的边界,例如
task()快照。 - shared WASM 使用宿主级对象句柄表路由对象方法;旧句柄或已释放句柄会返回已失效错误。
- shared WASM 超时中断只保证不会让 worker 永久占用服务容量;如果 WASM 正在执行长期阻塞 hostcall,仍需要后续 host import 自身提供取消边界。