ffi 原生动态库

ffi 原生动态库

ffi 原生动态库

功能

ffi 让 BT 脚本直接调用本机动态库中的 C ABI 函数,例如 Windows 的 .dll、Linux 的 .so 和 macOS 的 .dylib

受支持目标的 BT 默认构建已经内置 FFI,无需安装额外组件。Linux 官方包中的静态 musl bt 为保持跨发行版兼容而不包含 FFI;包内按 GNU 目标构建的 bt_app 仍包含 FFI。使用前可通过 BT.has('ffi') 判断当前二进制是否启用 FFI,然后准备与当前操作系统、CPU 架构匹配的动态库,并从动态库文档或 C 头文件中确认要调用函数的导出名称和完整原型。

FFI 适合在 CLI 脚本和桌面 App 的长期 VM 中调用已有原生能力。它不能在 Web 请求脚本中使用。

快速开始

下面的 Windows 示例加载 user32.dll,声明 MessageBoxW 的完整签名,调用函数后关闭动态库:

完整签名的格式是:

示例中的 i32(ptr, wstr, wstr, u32) 对应 C 原型中的 32 位整数返回值、一个指针、两个 Windows 宽字符串和一个 32 位无符号整数。

语法

推荐的使用顺序是:加载动态库、用完整签名声明函数、调用函数、处理返回值、关闭 Buffer 和动态库。

ffi.load

加载动态库并返回一个 FfiLibrary 对象。

参数

参数类型必填默认值有效范围或可选值说明
pathString裸库名或动态库文件路径裸库名交给操作系统查找;带目录的路径支持当前源码目录、@ 项目根和绝对路径。不能为空或包含 NUL。
schemaObject0..=128 个函数声明允许调用的函数及其签名。传入后只能调用 schema 中列出的精确导出名称;空对象表示不允许调用任何函数。

schema 字段

字段类型必填默认值有效范围或可选值说明
精确导出名称String每个要调用的函数必填非空、无 NUL、最多 255 个 UTF-8 字节字段名区分大小写。BT 不会自动添加、删除或替换 AW 后缀。
函数声明String完整签名或单独的返回类型,最多 512 个 UTF-8 字节完整签名最多声明 16 个参数。建议始终使用完整签名。

支持的签名类型

类型可用位置BT 参数BT 返回值说明
void仅返回类型empty原生函数没有返回值。
i8、i16、i32、i64参数、返回Int、BoolInt有符号整数;参数必须在声明宽度的范围内,Bool 转为 01
u8、u16、u32、u64参数、返回非负 Int、BoolInt无符号整数;超出声明范围会报错。u64 返回值不能超过 BT Int 上限。
isize、usize参数、返回IntInt指针宽度整数;当前支持的平台均按 64 位检查。
f32、f64参数、返回Int、FloatFloat浮点数;必须明确选择 f32 或 f64。
ptr参数、返回null、FfiPointer、FfiBufferFfiPointer、nullFfiBuffer 可直接作为稳定地址传入;原生空指针返回 null
cstr参数、返回String、nullString、nullUTF-8 NUL 结尾字符串。返回时立即复制为 BT String,非法 UTF-8 返回 null
wstr参数、返回String、nullString、null仅 Windows;UTF-16 NUL 结尾字符串。返回时立即复制为 BT String,非法 UTF-16 返回 null

返回值

类型说明
FfiLibrary已加载的动态库对象,通过 library.导出名称(...) 调用原生函数。

声明和调用函数

完整签名

完整签名会明确参数数量、每个参数的 ABI 类型和返回类型,是最安全、最容易检查的调用方式。快速开始中的 MessageBoxW: 'i32(ptr, wstr, wstr, u32)' 就是完整签名;无参数函数写成 cstr(),无返回值函数写成 void(),调用后分别得到 String 和 empty

函数的参数数量、类型和整数范围必须与 schema 一致。可检测到的类型错误、越界、内部 NUL、资源已关闭、权限不足或符号不存在,都会在进入原生函数前报错。

只声明返回类型

只写返回类型时,BT 固定返回类型,并在第一次调用时按有限规则推断参数。下面的 Windows 示例把返回值声明为 ptr,并根据函数名的 W 后缀把 String 参数作为 wstr

省略 schema

省略 schema 时可以调用合法的精确导出名称,但返回类型固定为 i32。只有函数真实原型完全符合下列规则时才能使用:

BT 参数推断类型条件
Inti32必须在 i32 范围内。
nullptr作为普通空指针,不推断字符串类型。
FfiPointer、FfiBufferptr资源必须仍然有效。
Stringwstr仅 Windows,并且导出名称以大写 W 结尾。
ASCII Stringcstr 指针仅 Windows,并且导出名称以大写 A 结尾。

Bool、Float、empty、Bytes、Array、Object 等不能自动推断,必须使用完整签名。首次调用成功后,该函数的参数数量和推断类型会被固定,后续调用必须保持一致。

如果原生返回值不是 i32,或者函数使用 Bool、Float、64 位整数、字符串等参数,不要省略 schema。

FfiPointer

原生函数声明为 ptr 时,空地址返回 null,非空地址返回 FfiPointer

FfiPointer 只能用于判空、比较,或作为 ptr 参数传回 FFI。BT 不允许读取、写入、转换为整数或进行指针算术。它依赖来源动态库或 FfiBuffer;对应资源关闭后,Pointer 会立即失效。

如果原生函数返回需要释放的指针,应将返回类型声明为 ptr,再调用该动态库提供的释放函数。不要把需要释放的地址声明为 cstrwstr

ffi.buffer

创建固定长度、零填充、地址稳定且至少按 16 字节对齐的可写内存,并返回 FfiBuffer。它适合传给需要由原生函数写入数据的 ptr 参数。

参数

参数类型必填默认值有效范围或可选值说明
sizeInt1..=BT_BYTES_LIMITBuffer 的可见字节长度;创建后不能扩容。

返回值

类型说明
FfiBufferBT 托管的可写原生内存,可直接传给声明为 ptr 的参数。

方法

方法参数必填默认值有效范围返回值说明
len()Int返回 Buffer 的可见字节长度。
ptr(offset)offset: Int00..=len()FfiPointer返回指定偏移的地址;len() 位置只能作为末尾指针传递。
write(data, offset)data: Bytes;offset: Intdata 是;offset 否offset 为 0写入范围不能越界Int把 Bytes 写入 Buffer,返回写入字节数。
to_bytes(offset, length)offset: Int;length: Intoffset 为 0;length 为剩余长度读取范围不能越界Bytes把指定范围复制为普通 Bytes。
to_string(offset)offset: Int00..len()String、null读取 NUL 结尾 UTF-8;非法 UTF-8 返回 null,找不到 NUL 时报错。
to_wstring(offset)offset: Int0仅 Windows;offset 必须按 2 字节对齐String、null读取 NUL 结尾 UTF-16;非法 UTF-16 返回 null

下面的示例让原生函数写入 UTF-8 文本:

ffi.close

关闭 FfiLibraryFfiBuffer。不再使用资源时应主动关闭,常驻进程尤其需要及时释放。

参数

参数类型必填默认值有效范围或可选值说明
valueFfiLibrary、FfiBufferffi.load()ffi.buffer() 返回的资源FfiPointer 不能单独关闭。

返回值

类型说明
Bool第一次成功关闭返回 true;资源已经关闭时返回 false

关闭动态库后,不能再调用其中的函数;关闭 Buffer 后,不能再访问其方法。由这些资源产生的 FfiPointer 也会同时失效。

能力和资源状态

字段类型说明
enabledBool当前解释器是否包含 FFI。
open_librariesInt当前打开的动态库数量。
buffersInt当前存活的 FfiBuffer 数量。
buffer_bytesInt当前 FfiBuffer 实际占用的总字节数。

同一进程最多同时打开 32 个动态库、存活 256 个 FfiBuffer,Buffer 总量最多 64 MiB。单个 schema 最多声明 128 个函数,单个完整签名最多包含 16 个参数;省略 schema 时,单个动态库最多使用 256 个不同函数。

适用平台

  • Windows x64
  • Linux x64 GNU(x86_64-unknown-linux-gnu,不包含 musl)
  • macOS Intel
  • macOS Apple Silicon

所有平台都使用目标系统默认的 C ABI。wstr 和根据 AW 后缀推断字符串的能力仅适用于 Windows。

注意事项

  • 优先使用完整签名,并逐项对照动态库官方文档或 C 头文件。错误的参数类型、返回类型或调用约定可能直接导致进程崩溃。
  • FFI 不是沙箱。动态库中的崩溃、非法内存访问和其他原生错误无法由 BT 安全捕获;不可信或需要超时隔离的动态库应放到独立子进程中调用。
  • 原生调用是同步阻塞操作。不要在要求异步非阻塞的执行路径中调用耗时函数。
  • 不支持结构体、联合体、可变参数、callback 和指针算术,也不适合要求 UI 主线程、COM apartment、原生线程回调或异步保存指针的 API。
  • String 传给 cstrwstr 时只在本次同步调用期间有效;原生函数不得保存该地址。FfiBuffer 也不能被原生函数保存到调用结束后异步使用。
  • cstrwstr 返回只适合原生文档保证可读且以 NUL 结尾的字符串。BT 会立即复制文本,但不会替原生库释放地址。
  • Windows x64 的 BOOL 通常声明为 i32,句柄声明为 ptrW API 的字符串声明为 wstr
  • ffiBT_PERMISSION_ALLOWBT_PERMISSION_DENY 中的 ffi 权限控制;ffi.close() 始终允许执行,确保权限收紧后仍能释放已有资源。
  • 自行分发包含 FFI 的 BT 可执行文件时,必须同时分发项目根目录中的 THIRD_PARTY_LICENSES_FFI.txt,保留 libffi、libffi-sys、C libffi 和 libloading 的版权与许可文本。
  • Windows 完整示例位于 examples/ffi-user32/