# FFI
FFI(外部函数接口)允许你加载共享库、查找符号,并以自动类型
封送调用 C 函数。你还可以分配原生内存、定义 C 结构体,
并固定 GC 管理的值,以便安全地跨越边界传递。
FFI 默认关闭——使用 `-Dffi=true` 构建:
```
$ zig build deft -Dffi=true -Doptimize=ReleaseFast
````
使用 `[capabilities]` 验证它是否已启用:
if [get [capabilities] :ffi] { echo "FFI available" }
## 类型标签
FFI 使用以 `::`(双冒号)为前缀的类型关键字,以区别于普通关键字:
| 类型 | 描述 |
|---|---|
| `::void` | 无返回值 |
| `::i8`, `::u8`, `::i16`, `::u16`, `::i32`, `::u32`, `::i64`, `::u64` | 整数 |
| `::f64` | 双精度浮点数 |
| `::ptr` | 原始指针 |
| `::cstring` | 以 null 结尾的 C 字符串 |
| `::bool` | 布尔值 |
## `ffi/*` 标准库函数
| 函数 | 返回值 | 用途 |
|---|---|---|
| `[ffi/load "lib.so"]` | DynLib 资源 | dlopen |
| `[ffi/sym $lib "fn"]` | fn-ptr 资源 | dlsym |
| `[ffi/call $fn ::ret-type @{ ::arg-types... } args...]` | 封送后的返回值 | 调用 C 函数 |
| `[ffi/close $resource]` | nil | 释放任意原生资源 |
| `[ffi/alloc $n]` | 缓冲区资源 | 分配 N 字节 |
| `[ffi/cstring $s]` | cstring 资源 | Deft 字符串 → C 字符串 |
| `[ffi/string $cs]` | 字符串 | C 字符串 → Deft 字符串 |
| `[ffi/read $buf $offset ::type]` | 值 | 在偏移处进行类型化读取 |
| `[ffi/read-bytes $buf $offset $len]` | 字符串 | 二进制安全的批量读取(保留 null 字节) |
| `[ffi/write $buf $offset ::type $value]` | nil | 在偏移处进行类型化写入 |
| `[ffi/addr $ptr]` | 数字 | 原始地址(nullptr 时为 0) |
| `[ffi/ptr $n]` | 指针资源 | 包装原始地址 |
| `[ffi/nullptr]` | nullptr 资源 | 构造空指针 |
| `[ffi/pin $value]` | 值 | 固定(FFI 期间保证 GC 安全) |
| `[ffi/unpin $value]` | nil | 解除固定 |
| `[ffi/cdef "struct {...};"]` | 名称 | 注册结构体 |
| `[ffi/cdef "ret fn(args);"]` | 名称 | 注册函数签名 |
| `[ffi/sizeof ::type \| "Struct"]` | 数字 | 字节大小 |
| `[ffi/new "Struct"]` | 实例资源 | 分配零初始化的结构体 |
| `[ffi/get $instance "field"]` | 值 | 读取结构体字段 |
| `[ffi/set $instance "field" $value]` | nil | 写入结构体字段 |
| `[ffi/callback $fn ::ret-type @{ ::arg-types... }]` | fn-ptr 资源 | 可从 Deft 调用的 C 函数指针 |
| `[ffi/gc $resource $destructor]` | gc 包装的资源 | 附加 C 析构器 |
## 加载与调用
# Load libc (short names work: "c", "m", "pthread", "dl", "rt") def C [ffi/load "libc.so.6"] # Or equivalently: def C [ffi/load "c"] # Look up a symbol def strlen-fn [ffi/sym $C "strlen"] # Call: ffi/call $fn ::ret-type @{ ::arg-types } args... set result [ffi/call $strlen-fn :u64 @{ :cstring } "hello"] echo $result # => 5 ffi/close $C
### 短库名
`ffi/load` 会自动解析这些短名称:
| 短名称 | 解析为 |
|---|---|
| `"c"` | `libc.so.6` |
| `"m"` | `libm.so.6` |
| `"pthread"` | `libpthread.so.0` |
| `"dl"` | `libdl.so.2` |
| `"rt"` | `librt.so.1` |
### 无参数调用
对于不接受参数的函数,请传入空列表:
def getpid-fn [ffi/sym $C "getpid"] set pid [ffi/call $getpid-fn :i32 @{}] # getpid takes no args echo $pid
### 错误
失败时,`ffi/load`、`ffi/sym` 和 `ffi/call` 会返回 `%Err`:
def bad [ffi/load "nonexistent.so"] if [err? $bad] { echo "missing: $bad~message" }
参数数量错误的调用会返回 `%Err`。类型不匹配会在 C 调用边界引发错误——如果你不信任输入,请将风险调用包裹在 `try` 中。
## 函数声明解析
对于拥有丰富 C 头文件的库,`ffi/cdef` 会解析 C 函数签名字符串,并为该库注册函数以支持波浪号分派:
def C [ffi/load "c"] ffi/cdef "size_t strlen(const char* s);" # Now you can call via tilde on the library — types are inferred: set n [$C~strlen "hello"] echo $n # => 5
支持的声明:
ffi/cdef "int puts(const char* s);" ffi/cdef "void abort(void);" ffi/cdef "int getpid();" ffi/cdef "double pow(double x, double y);" # Multiple declarations in one call (semicolon-separated): ffi/cdef "int abs(int n);\nsize_t strlen(const char* s);" # Mixed struct and function: ffi/cdef "struct Pt { int x; int y; };\nint getpid();"
注册后,`$lib~name args...` 会使用声明的参数类型和返回类型调用该函数。无需在每次调用时都传递类型。
## C 字符串
def cs [ffi/cstring "hello world"] set s [ffi/string $cs] # back to Deft string echo $s # => hello world ffi/close $cs
空字符串可以正确往返。cstring 资源持有一个以 null 结尾的缓冲区,由 GC 管理。
## 原生内存
`ffi/alloc N` 分配一个 N 字节的缓冲区。`ffi/read` 和 `ffi/write`
负责类型化值的读写:
def buf [ffi/alloc 64] ffi/write $buf 0 :i32 42 ffi/write $buf 8 :f64 3.14 ffi/write $buf 16 :u8 255 echo [ffi/read $buf 0 :i32] # => 42 echo [ffi/read $buf 8 :f64] # => 3.14 echo [ffi/read $buf 16 :u8] # => 255 ffi/close $buf
使用 `ffi/addr` 获取缓冲区或指针的原始整数地址;`ffi/ptr`
将原始地址重新包装为指针资源。`ffi/nullptr` 构造一个空指针
(其 `ffi/addr` 为 0)。
## 结构体定义
`ffi/cdef "struct Name { ... };"` 解析 C 结构体定义,
并以该名称注册它(以字符串形式返回):
ffi/cdef "struct Point { int x; int y; };" echo [ffi/sizeof "Point"] # => 8 ffi/cdef "struct Vec2 { double x; double y; };" echo [ffi/sizeof "Vec2"] # => 16 ffi/cdef "struct Mixed { char c; int i; double d; };" echo [ffi/sizeof "Mixed"] # => 16 (with padding)
### 基本类型大小
| 类型 | 字节数 |
|---|---|
| `::i8`, `::u8` | 1 |
| `::i16`, `::u16` | 2 |
| `::i32`, `::u32` | 4 |
| `::i64`, `::u64` | 8 |
| `::f64` | 8 |
| `::ptr` | 8(64 位平台上) |
C 结构体的对齐和填充遵循平台 ABI:例如
`struct Mixed { char c; int i; double d; }` 为 16 字节(1 char +
3 字节填充 + 4 int + 8 double)。
### 分配与使用结构体
`ffi/new` 分配一个零初始化的实例:
ffi/cdef "struct Coord { int x; int y; };" def p [ffi/new "Coord"] ffi/set $p "x" 10 ffi/set $p "y" 20 echo [ffi/get $p "x"] # => 10 echo [ffi/get $p "y"] # => 20 ffi/close $p
### 波浪号字段访问
结构体注册后,`$instance~field` 会读取该字段:
ffi/cdef "struct TildePoint { int x; int y; };" def p [ffi/new "TildePoint"] ffi/set $p "x" 42 echo $p~x # => 42
这适用于任何已注册结构体中的任何字段名。
## 指针与 GC 安全
Deft 的 GC 可能会移动堆分配的值。如果你在 FFI 调用期间
持有指向 GC 管理内存的指针,请先固定它:
def s "important" ffi/pin $s # ... do FFI work referencing $s ... ffi/unpin $s
`ffi/pin` 原样返回该值;它只是将其标记为不可移动。
完成后解除固定,让 GC 回收它。
对于来自 `ffi/alloc` 的缓冲区资源,无需固定——
它们是 C 堆分配。
## 回调(`ffi/callback`)
将 Deft 闭包包装为 C 函数指针:
def on-event {|code^number| echo "C called us with: $code" } ffi/cdef "void register_callback(void (*cb)(int));" def cb [ffi/callback :void @{ :i32 } $on-event] [$C~register-callback $cb]
只要 C 可能调用它,闭包就必须保持存活(或被固定)。
## 析构器(`ffi/gc`)
将 C 析构器附加到原生资源。当 GC 回收包装器
(或你显式 `ffi/close` 它)时,析构器会运行:
def C [ffi/load "c"] ffi/cdef "void* malloc(size_t n); void free(void* p);" def buf [$C~malloc 64] def free-fn [ffi/sym $C "free"] def gc-ref [ffi/gc $buf $free-fn] # ... use $buf ... ffi/close $gc-ref # calls free($buf) ffi/close $C
当你从 C 库获得指针,并且需要确保即使脚本提前退出
其析构器也会运行时,请使用此方法。
## 真实示例:通过 deft/ffmpeg 播放视频
`deft/ffmpeg` 包(位于 `stdext/src/ffmpeg/`)是一个完整的 FFI
视频播放器:它 dlopen 系统 `libav*` 库,探测 FFmpeg 主版本号,
通过按版本区分的偏移表读取 libav 结构体,并在 defworker 上解码。
该模式的简化示例如下:
def lib [ffi/load "libavutil.so.60"] # Look up the functions we need set fns %{ :version [ffi/sym $lib "avutil_version"] :alloc [ffi/sym $lib "av_frame_alloc"] } # Call: version int is (major<<16 | minor<<8 | micro) set v [ffi/call ($fns~version) :u32 @{}] echo [int ($v / 65536)] # => 60 (FFmpeg 8) # ::ptr returns arrive as pointer resources; read fields at offsets set frame [ffi/call ($fns~alloc) :ptr @{}] echo [ffi/read $frame 104 :i32] # AVFrame.width ffi/close $lib
完整的绑定(偏移表、解码工作进程、流式播放)请参见
`stdext/src/ffmpeg/`。
## 限制与注意事项
- 当所有参数都是整数/指针时,每个 `ffi/call` 最多 10 个参数;
只要有任何参数是浮点数则最多 6 个(int+float 混合模式最多 3 个)。
更长的签名(例如带很多参数的 `printf`、`sws_getCachedContext`)
不受支持。
- 仅限 x86_64。 C 调用跳板以 System V ABI 为目标。
AArch64 支持已在路线图中,但尚未发布。
- 尚不支持可变参数 C 函数。 不支持 `printf` 风格的 API——
请将它们包装为固定参数数量的辅助函数。
- `ffi/cdef` 中不支持位域或联合体。
- 回调是同步的。 Deft 闭包在调用它的线程上运行;
如果 C 从非 Deft 线程调用它,行为未定义。
- 资源生命周期由 GC 管理。 `ffi/close` 是可选的;
GC 最终会释放资源。对于大型缓冲区或有限的系统资源,
请使用它以尽早清理。
## 快速参考
# Load + call def C [ffi/load "c"] def fn [ffi/sym $C "strlen"] echo [ffi/call $fn :u64 @{ :cstring } "hello"] # => 5 # Or via cdef + tilde ffi/cdef "size_t strlen(const char* s);" echo [$C~strlen "hello"] # => 5 # Struct ffi/cdef "struct Pt { int x; int y; };" def p [ffi/new "Pt"] ffi/set $p "x" 10 echo $p~x # => 10 # Buffer def buf [ffi/alloc 64] ffi/write $buf 0 :i32 99 echo [ffi/read $buf 0 :i32] # => 99 # Cleanup ffi/close $C ffi/close $p ffi/close $buf