# ffi 原生动态库标准库

## 功能

`ffi` 用于按本机系统 C ABI 加载 `.dll`、`.so` 或 `.dylib`，通过完整签名调用精确导出符号，并提供地址稳定的可写 FfiBuffer。使用前应查阅原生库的官方头文件或文档，确认函数的真实导出名称和完整原型。

BT 官方发布的 `bt.exe` 已包含 FFI 能力，无需安装额外版本或启用额外功能。

## 语法

```bt
library = ffi.load(path, schema)
result = library.ExactExportName(...args)
buffer = ffi.buffer(size)
closed = ffi.close(library)
```

调用过程为：

```text
加载动态库 -> 校验严格 schema -> 首次查找精确符号并创建调用描述 -> 同步调用 -> 转换返回值 -> 关闭
```

## ffi.load 参数

| 参数 | 类型 | 必填 | 默认值 | 有效范围或可选值 | 说明 |
| ------ | ------ | ------ | ------ | ------ | ------ |
| path | String | 是 | 无 | 裸库名或文件路径 | 裸库名交给系统加载器；带目录路径按当前源码目录、`@` 项目根或绝对路径解析。不能为空或包含 NUL。 |
| schema | Object | 是 | 无 | `0..=128` 项 | 必须传入严格完整签名表。显式空对象表示不允许调用任何符号。 |

`schema` 动态字段：

| 字段 | 类型 | 必填 | 默认值 | 有效范围或可选值 | 说明 |
| ------ | ------ | ------ | ------ | ------ | ------ |
| 精确导出符号名 | String | 每个要调用的函数必填 | 无 | 非空、无 NUL、最多 255 个 UTF-8 字节 | 字段名大小写精确匹配；不会自动添加、删除或尝试 `A` / `W` 后缀。 |
| 字段值 | String | 是 | 无 | 最多 512 个 UTF-8 字节 | 必须是 `返回类型(参数类型, ...)` 完整签名；最多 16 个参数。 |

支持的完整签名类型：

| 类型 | 可用位置 | 参数接受值 | 返回值 | 说明 |
| ------ | ------ | ------ | ------ | ------ |
| void | 仅返回类型 | 不允许 | Empty | 原生函数无返回值时，BT 返回 `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 位目标检查；usize 返回值超过 BT Int 上限时报错。 |
| f32、f64 | 参数、返回 | Int、Float | Float | 按声明宽度转换，不在 f32 与 f64 之间自动猜测。 |
| ptr | 参数、返回 | `null`、FfiPointer、FfiBuffer | FfiPointer、`null` | FfiBuffer 以稳定基地址零拷贝传入；零地址返回 `null`。 |
| cstr | 参数、返回 | String、`null` | String、`null` | 输入编码为 UTF-8 NUL 结尾临时缓冲；返回时立即有界复制，非法 UTF-8 返回 `null`。 |
| wstr | 仅 Windows 参数、返回 | String、`null` | String、`null` | 输入编码为 UTF-16 NUL 结尾临时缓冲；返回时立即有界复制，非法 UTF-16 返回 `null`。 |

## ffi.load 返回值

| 类型 | 说明 |
| ------ | ------ |
| FfiLibrary | 加载成功后的动态库对象。属性访问只允许 schema 已声明的精确导出符号。 |

schema 会在 `ffi.load()` 时完整解析，但系统符号查找和 libffi 调用描述会延迟到对应函数第一次调用。首次成功后按符号缓存；重复调用不会重复解析 schema、查找符号或创建调用描述。

## 动态库函数参数与返回值

动态库方法的参数数量和类型必须与 schema 完全一致。所有可检测的参数数量、BT 类型、整数范围、内部 NUL、关闭状态、权限和符号缺失错误都会在原生调用前转换为中文运行时错误。

`ptr` 返回值规则：

| 原生结果 | BT 返回值 | 说明 |
| ------ | ------ | ------ |
| 空地址 | null | 表示原生函数明确返回空指针。 |
| 非空地址 | FfiPointer | 只能判空、比较或传回 FFI；不能读取、写入、转整数或做指针算术。 |

FfiPointer 会继承已知 owner：返回地址落在传入 FfiBuffer 可见范围内时继承 Buffer；等于传入 FfiPointer 时继承原 Pointer；其他地址由当前动态库维持。对应 Library 或 Buffer 关闭后，Pointer 立即失效。

如果 `ptr` 返回值落在本次调用的临时 cstr/wstr 输入内，BT 会拒绝生成悬空 FfiPointer。cstr/wstr 返回会在临时输入释放前完成复制。

## ffi.buffer 参数

| 参数 | 类型 | 必填 | 默认值 | 有效范围或可选值 | 说明 |
| ------ | ------ | ------ | ------ | ------ | ------ |
| size | Int | 是 | 无 | `1..=BT_BYTES_LIMIT` | 创建固定长度、零填充、基地址至少 16 字节对齐的可写内存。不能扩容。 |

## ffi.buffer 返回值

| 类型 | 说明 |
| ------ | ------ |
| FfiBuffer | 地址稳定的 BT 托管可写 Buffer；可直接传给声明为 `ptr` 的参数。 |

FfiBuffer 方法：

| 方法 | 参数 | 返回值 | 说明 |
| ------ | ------ | ------ | ------ |
| len() | 无 | Int | 返回脚本请求的可见字节长度。 |
| ptr(offset = 0) | offset: Int | FfiPointer | offset 范围为 `0..=len()`；末尾地址仅可作为 one-past-end 指针传递。 |
| write(data, offset = 0) | data: Bytes、offset: Int | Int | 零拷贝读取 Bytes 并复制到 Buffer；越界报错。 |
| to_bytes(offset = 0, length = 剩余长度) | offset: Int、length: Int | Bytes | 把指定范围复制为普通共享只读 Bytes。 |
| to_string(offset = 0) | offset: Int | String、`null` | 在 Buffer 可见范围内读取 NUL 结尾 UTF-8；非法 UTF-8 返回 `null`，无 NUL 报错。 |
| to_wstring(offset = 0) | offset: Int | String、`null` | 仅 Windows；offset 必须按 2 字节对齐；非法 UTF-16 返回 `null`。 |

FfiBuffer 固定上限：同时存活 256 个，实际分配总量 64 MiB。实际分配按 16 字节向上取整，`BT.stats().ffi.buffer_bytes` 展示实际分配量。

## ffi.close 参数

| 参数 | 类型 | 必填 | 默认值 | 有效范围或可选值 | 说明 |
| ------ | ------ | ------ | ------ | ------ | ------ |
| value | FfiLibrary、FfiBuffer | 是 | 无 | BT 通过 `ffi.load()` 或 `ffi.buffer()` 创建的资源 | FfiPointer 不能单独关闭。 |

## ffi.close 返回值

| 类型 | 说明 |
| ------ | ------ |
| Bool | 第一次完成逻辑关闭并且系统卸载未报告错误时返回 `true`；已经关闭返回 `false`。 |

关闭 Library 时会先标记资源失效，再清空 schema 和函数缓存，最后请求系统卸载。关闭 Buffer 时立即释放内存额度并使派生 Pointer 失效。操作系统可能延迟物理卸载 Library；BT 保证脚本侧失效，不承诺映像立即从进程移除。

## 代码示例

Windows `GetSystemMetrics` 完整签名调用：

```bt
user32 = ffi.load('user32.dll', {
    GetSystemMetrics: 'i32(i32)'
})

screen_width = user32.GetSystemMetrics(0)

// 输出：当前屏幕宽度，数值大于 0
print screen_width

ffi.close(user32)
```

让原生函数写入 FfiBuffer：

```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()

// 输出：原生函数写入的 UTF-8 文本
print text

ffi.close(buffer)
ffi.close(sdk)
```

查看 FFI 资源统计：

```bt
stats = BT.stats().ffi

// 输出：true
print stats.enabled
```

| 字段 | 类型 | 说明 |
| ------ | ------ | ------ |
| enabled | Bool | 当前构建是否启用 FFI。 |
| open_libraries | Int | 当前逻辑打开的动态库数量。 |
| buffers | Int | 当前存活的 FfiBuffer 数量。 |
| buffer_bytes | Int | 当前 FfiBuffer 实际分配总字节数。 |

Windows `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)
```

检查当前解释器是否启用 FFI：

```bt
enabled = BT.has('ffi')

// BT 官方解释器输出：true
print enabled
```

## 注意事项

- 当前只支持 `x86_64-pc-windows-msvc`、`x86_64-unknown-linux-gnu`、`x86_64-apple-darwin` 和 `aarch64-apple-darwin`，统一使用目标平台默认 C ABI。
- 当前仍不支持 `ffi.load(path)` 免声明调用和 schema 返回类型提示；必须提供完整签名。结构体、联合体、可变参数、callback 和指针算术也不支持。
- `ffi` 受 `BT_PERMISSION_ALLOW` 和 `BT_PERMISSION_DENY` 中的 `ffi` 能力控制，并且完全禁止在 Web 请求 VM 中加载、访问动态库属性或执行原生函数。
- 原生调用是同步阻塞操作。CLI VM 和 App 长期 VM 可以调用，但要求 UI 主线程、COM apartment、callback、异步保存指针或原生线程回调的 API 不在支持范围内。
- cstr/wstr 临时缓冲和 FfiBuffer 只允许同步调用。原生函数不得保存这些地址供返回后或后台线程继续访问。
- 声明为 `ptr` 的返回值若落在本次调用的临时 cstr/wstr 缓冲内，调用会报错，避免生成已经失效的指针。
- 完整签名必须与真实 C 原型完全一致。错误签名、把数据导出当函数、非法指针解引用、动态库初始化/析构或第三方函数内部缺陷都可能直接终止进程；FFI 不是沙箱，也无法捕获原生崩溃。
- 需要崩溃隔离或超时取消的第三方库应放在独立子进程中调用。
- cstr/wstr 返回只适用于原生文档明确保证当前调用期间可读的 NUL 结尾字符串。BT 立即复制但不释放原生地址；需要第三方函数释放的字符串必须声明为 `ptr`，再显式调用库提供的释放函数。
- Windows x64 的 `BOOL` 应声明为 `i32`，句柄声明为 `ptr`，`W` API 的输入字符串声明为 `wstr`；不要把平台相关的 C `long` 宽度当作固定类型。
- 完整示例位于 `examples/ffi-user32/` 和 `examples/ffi-testlib/`。
