# 扩展运行机制 ## 功能 扩展机制把“脚本能看到的 API”和“扩展内部如何执行”分开。脚本只通过 `bindings.json` 里声明的入口、对象和方法调用扩展;BT 宿主负责加载 `.bts` 包、校验描述文件、建立注册表,并把调用分发给对应 Runner。 ## 语法 扩展入口调用语法和普通函数一致: ```bt object = calc(1) value = object.add(2).value() // 输出:3 print value ``` `calc` 来自 `bindings.json` 的公开入口声明,`add` 和 `value` 来自对象方法声明。 ## 参数 扩展调用的参数由 bindings 决定。宿主会按 bindings 的参数数量、类型和 role 处理脚本传入的值,再把调用交给对应 Runner。 ## 返回值 扩展调用可以返回原始值,也可以返回扩展对象句柄。对象句柄只表示“某个扩展模块中的某个对象”,真实状态由扩展后端保存。 脚本可以用 `type(object)` 查看扩展对象在 `bindings.json` 中声明的对象类型名: ```bt 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` 风格可以对应不同后端。脚本侧调用保持一致: ```bt 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 自身提供取消边界。