# 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` 的完整签名，调用函数后关闭动态库：

```bt
user32 = ffi.load('user32.dll', {
    MessageBoxW: 'i32(ptr, wstr, wstr, u32)'
})

button = user32.MessageBoxW(
    null,
    'BT 已成功调用 user32.dll',
    'BT FFI',
    64
)

// 点击“确定”后输出：1
print button

ffi.close(user32)
```

完整签名的格式是：

```text
返回类型(参数类型, 参数类型, ...)
```

示例中的 `i32(ptr, wstr, wstr, u32)` 对应 C 原型中的 32 位整数返回值、一个指针、两个 Windows 宽字符串和一个 32 位无符号整数。

## 语法

```bt
library = ffi.load(path)
library = ffi.load(path, schema)
result = library.ExactExportName(...args)

buffer = ffi.buffer(size)

closed = ffi.close(library)
closed = ffi.close(buffer)
```

推荐的使用顺序是：加载动态库、用完整签名声明函数、调用函数、处理返回值、关闭 Buffer 和动态库。

## ffi.load

加载动态库并返回一个 `FfiLibrary` 对象。

### 参数

| 参数 | 类型 | 必填 | 默认值 | 有效范围或可选值 | 说明 |
| ------ | ------ | ------ | ------ | ------ | ------ |
| path | String | 是 | 无 | 裸库名或动态库文件路径 | 裸库名交给操作系统查找；带目录的路径支持当前源码目录、`@` 项目根和绝对路径。不能为空或包含 NUL。 |
| schema | Object | 否 | 无 | `0..=128` 个函数 | 声明允许调用的函数及其签名。传入后只能调用 schema 中列出的精确导出名称；空对象表示不允许调用任何函数。 |

### schema 字段

| 字段 | 类型 | 必填 | 默认值 | 有效范围或可选值 | 说明 |
| ------ | ------ | ------ | ------ | ------ | ------ |
| 精确导出名称 | String | 每个要调用的函数必填 | 无 | 非空、无 NUL、最多 255 个 UTF-8 字节 | 字段名区分大小写。BT 不会自动添加、删除或替换 `A`、`W` 后缀。 |
| 函数声明 | String | 是 | 无 | 完整签名或单独的返回类型，最多 512 个 UTF-8 字节 | 完整签名最多声明 16 个参数。建议始终使用完整签名。 |

### 支持的签名类型

| 类型 | 可用位置 | BT 参数 | BT 返回值 | 说明 |
| ------ | ------ | ------ | ------ | ------ |
| void | 仅返回类型 | 无 | empty | 原生函数没有返回值。 |
| i8、i16、i32、i64 | 参数、返回 | Int、Bool | Int | 有符号整数；参数必须在声明宽度的范围内，Bool 转为 `0` 或 `1`。 |
| u8、u16、u32、u64 | 参数、返回 | 非负 Int、Bool | Int | 无符号整数；超出声明范围会报错。u64 返回值不能超过 BT Int 上限。 |
| isize、usize | 参数、返回 | Int | Int | 指针宽度整数；当前支持的平台均按 64 位检查。 |
| f32、f64 | 参数、返回 | Int、Float | Float | 浮点数；必须明确选择 f32 或 f64。 |
| ptr | 参数、返回 | `null`、FfiPointer、FfiBuffer | FfiPointer、`null` | FfiBuffer 可直接作为稳定地址传入；原生空指针返回 `null`。 |
| cstr | 参数、返回 | String、`null` | String、`null` | UTF-8 NUL 结尾字符串。返回时立即复制为 BT String，非法 UTF-8 返回 `null`。 |
| wstr | 参数、返回 | String、`null` | String、`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`：

```bt
user32 = ffi.load('user32.dll', {
    FindWindowW: 'ptr'
})
window = user32.FindWindowW(null, 'BT FFI')

// 输出：null，或匹配窗口的 FfiPointer
print window

ffi.close(user32)
```

### 省略 schema

省略 schema 时可以调用合法的精确导出名称，但返回类型固定为 `i32`。只有函数真实原型完全符合下列规则时才能使用：

| BT 参数 | 推断类型 | 条件 |
| ------ | ------ | ------ |
| Int | i32 | 必须在 i32 范围内。 |
| `null` | ptr | 作为普通空指针，不推断字符串类型。 |
| FfiPointer、FfiBuffer | ptr | 资源必须仍然有效。 |
| String | wstr | 仅 Windows，并且导出名称以大写 `W` 结尾。 |
| ASCII String | cstr 指针 | 仅 Windows，并且导出名称以大写 `A` 结尾。 |

Bool、Float、empty、Bytes、Array、Object 等不能自动推断，必须使用完整签名。首次调用成功后，该函数的参数数量和推断类型会被固定，后续调用必须保持一致。

```bt
user32 = ffi.load('user32.dll')
screen_width = user32.GetSystemMetrics(0)

// 输出：当前屏幕宽度，数值大于 0
print screen_width

ffi.close(user32)
```

如果原生返回值不是 i32，或者函数使用 Bool、Float、64 位整数、字符串等参数，不要省略 schema。

## FfiPointer

原生函数声明为 `ptr` 时，空地址返回 `null`，非空地址返回 `FfiPointer`。

FfiPointer 只能用于判空、比较，或作为 `ptr` 参数传回 FFI。BT 不允许读取、写入、转换为整数或进行指针算术。它依赖来源动态库或 FfiBuffer；对应资源关闭后，Pointer 会立即失效。

如果原生函数返回需要释放的指针，应将返回类型声明为 `ptr`，再调用该动态库提供的释放函数。不要把需要释放的地址声明为 `cstr` 或 `wstr`。

## ffi.buffer

创建固定长度、零填充、地址稳定且至少按 16 字节对齐的可写内存，并返回 `FfiBuffer`。它适合传给需要由原生函数写入数据的 `ptr` 参数。

### 参数

| 参数 | 类型 | 必填 | 默认值 | 有效范围或可选值 | 说明 |
| ------ | ------ | ------ | ------ | ------ | ------ |
| size | Int | 是 | 无 | `1..=BT_BYTES_LIMIT` | Buffer 的可见字节长度；创建后不能扩容。 |

### 返回值

| 类型 | 说明 |
| ------ | ------ |
| FfiBuffer | BT 托管的可写原生内存，可直接传给声明为 `ptr` 的参数。 |

### 方法

| 方法 | 参数 | 必填 | 默认值 | 有效范围 | 返回值 | 说明 |
| ------ | ------ | ------ | ------ | ------ | ------ | ------ |
| len() | 无 | 无 | 无 | 无 | Int | 返回 Buffer 的可见字节长度。 |
| ptr(offset) | offset: Int | 否 | `0` | `0..=len()` | FfiPointer | 返回指定偏移的地址；`len()` 位置只能作为末尾指针传递。 |
| write(data, offset) | data: Bytes；offset: Int | data 是；offset 否 | offset 为 `0` | 写入范围不能越界 | Int | 把 Bytes 写入 Buffer，返回写入字节数。 |
| to_bytes(offset, length) | offset: Int；length: Int | 否 | offset 为 `0`；length 为剩余长度 | 读取范围不能越界 | Bytes | 把指定范围复制为普通 Bytes。 |
| to_string(offset) | offset: Int | 否 | `0` | `0..len()` | String、`null` | 读取 NUL 结尾 UTF-8；非法 UTF-8 返回 `null`，找不到 NUL 时报错。 |
| to_wstring(offset) | offset: Int | 否 | `0` | 仅 Windows；offset 必须按 2 字节对齐 | String、`null` | 读取 NUL 结尾 UTF-16；非法 UTF-16 返回 `null`。 |

下面的示例让原生函数写入 UTF-8 文本：

```bt
sdk = ffi.load('./sdk.dll', {
    sdk_write_text: 'i32(ptr, usize)'
})
buffer = ffi.buffer(256)

written = sdk.sdk_write_text(buffer, buffer.len())
text = buffer.to_string()
bytes = buffer.to_bytes(0, written)

// 输出：原生函数写入的 UTF-8 文本
print text

ffi.close(buffer)
ffi.close(sdk)
```

## ffi.close

关闭 `FfiLibrary` 或 `FfiBuffer`。不再使用资源时应主动关闭，常驻进程尤其需要及时释放。

### 参数

| 参数 | 类型 | 必填 | 默认值 | 有效范围或可选值 | 说明 |
| ------ | ------ | ------ | ------ | ------ | ------ |
| value | FfiLibrary、FfiBuffer | 是 | 无 | `ffi.load()` 或 `ffi.buffer()` 返回的资源 | FfiPointer 不能单独关闭。 |

### 返回值

| 类型 | 说明 |
| ------ | ------ |
| Bool | 第一次成功关闭返回 `true`；资源已经关闭时返回 `false`。 |

关闭动态库后，不能再调用其中的函数；关闭 Buffer 后，不能再访问其方法。由这些资源产生的 FfiPointer 也会同时失效。

## 能力和资源状态

```bt
enabled = BT.has('ffi')
stats = BT.stats().ffi

// 启用 FFI 的解释器输出：true；Linux 官方包中的静态 musl bt 输出：false
print enabled

// 输出：当前打开的动态库数量
print stats.open_libraries
```

| 字段 | 类型 | 说明 |
| ------ | ------ | ------ |
| enabled | Bool | 当前解释器是否包含 FFI。 |
| open_libraries | Int | 当前打开的动态库数量。 |
| buffers | Int | 当前存活的 FfiBuffer 数量。 |
| buffer_bytes | Int | 当前 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` 和根据 `A`、`W` 后缀推断字符串的能力仅适用于 Windows。

## 注意事项

- 优先使用完整签名，并逐项对照动态库官方文档或 C 头文件。错误的参数类型、返回类型或调用约定可能直接导致进程崩溃。
- FFI 不是沙箱。动态库中的崩溃、非法内存访问和其他原生错误无法由 BT 安全捕获；不可信或需要超时隔离的动态库应放到独立子进程中调用。
- 原生调用是同步阻塞操作。不要在要求异步非阻塞的执行路径中调用耗时函数。
- 不支持结构体、联合体、可变参数、callback 和指针算术，也不适合要求 UI 主线程、COM apartment、原生线程回调或异步保存指针的 API。
- String 传给 `cstr` 或 `wstr` 时只在本次同步调用期间有效；原生函数不得保存该地址。FfiBuffer 也不能被原生函数保存到调用结束后异步使用。
- `cstr` 和 `wstr` 返回只适合原生文档保证可读且以 NUL 结尾的字符串。BT 会立即复制文本，但不会替原生库释放地址。
- Windows x64 的 `BOOL` 通常声明为 `i32`，句柄声明为 `ptr`，`W` API 的字符串声明为 `wstr`。
- `ffi` 受 `BT_PERMISSION_ALLOW` 和 `BT_PERMISSION_DENY` 中的 `ffi` 权限控制；`ffi.close()` 始终允许执行，确保权限收紧后仍能释放已有资源。
- 自行分发包含 FFI 的 BT 可执行文件时，必须同时分发项目根目录中的 `THIRD_PARTY_LICENSES_FFI.txt`，保留 libffi、libffi-sys、C libffi 和 libloading 的版权与许可文本。
- Windows 完整示例位于 `examples/ffi-user32/`。
