# 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