# FFI
FFI (Foreign Function Interface) lets you load shared libraries,
look up symbols, and call C functions with automatic type
marshaling. You can also allocate native memory, define C structs,
and pin GC-managed values for safe passage across the boundary.
FFI is **off by default** — build with `-Dffi=true`:
```
$ zig build deft -Dffi=true -Doptimize=ReleaseFast
```
Verify it's enabled with `[capabilities]`:
if [get [capabilities] :ffi] { echo "FFI available" }
## Type Tags
FFI uses **type keywords** prefixed with `::` (double colon) to
distinguish them from regular keywords:
| Type | Description |
|---|---|
| `::void` | No return value |
| `::i8`, `::u8`, `::i16`, `::u16`, `::i32`, `::u32`, `::i64`, `::u64` | Integers |
| `::f64` | Double-precision float |
| `::ptr` | Raw pointer |
| `::cstring` | Null-terminated C string |
| `::bool` | Boolean |
## `ffi/*` Stdlib functions
| Function | Returns | Purpose |
|---|---|---|
| `[ffi/load "lib.so"]` | DynLib resource | dlopen |
| `[ffi/sym $lib "fn"]` | fn-ptr resource | dlsym |
| `[ffi/call $fn ::ret-type @{ ::arg-types... } args...]` | marshalled return | Call a C function |
| `[ffi/close $resource]` | nil | Release any native resource |
| `[ffi/alloc $n]` | buffer resource | Allocate N bytes |
| `[ffi/cstring $s]` | cstring resource | Deft string → C string |
| `[ffi/string $cs]` | string | C string → Deft string |
| `[ffi/read $buf $offset ::type]` | value | Typed read at offset |
| `[ffi/read-bytes $buf $offset $len]` | string | Binary-safe bulk read (null bytes preserved) |
| `[ffi/write $buf $offset ::type $value]` | nil | Typed write at offset |
| `[ffi/addr $ptr]` | number | Raw address (0 for nullptr) |
| `[ffi/ptr $n]` | pointer resource | Wrap a raw address |
| `[ffi/nullptr]` | nullptr resource | Construct a null pointer |
| `[ffi/pin $value]` | value | Pin (GC safety during FFI) |
| `[ffi/unpin $value]` | nil | Release pin |
| `[ffi/cdef "struct {...};"]` | name | Register a struct |
| `[ffi/cdef "ret fn(args);"]` | name | Register a function signature |
| `[ffi/sizeof ::type \| "Struct"]` | number | Byte size |
| `[ffi/new "Struct"]` | instance resource | Allocate zero-initialised struct |
| `[ffi/get $instance "field"]` | value | Read a struct field |
| `[ffi/set $instance "field" $value]` | nil | Write a struct field |
| `[ffi/callback $fn ::ret-type @{ ::arg-types... }]` | fn-ptr resource | C-callable from Deft |
| `[ffi/gc $resource $destructor]` | gc-wrapped resource | Attach a C destructor |
## Loading and Calling
# 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
### Short library names
`ffi/load` resolves these short names automatically:
| Short | Resolves to |
|---|---|
| `"c"` | `libc.so.6` |
| `"m"` | `libm.so.6` |
| `"pthread"` | `libpthread.so.0` |
| `"dl"` | `libdl.so.2` |
| `"rt"` | `librt.so.1` |
### No-argument calls
For functions taking no arguments, pass an empty list:
def getpid-fn [ffi/sym $C "getpid"] set pid [ffi/call $getpid-fn :i32 @{}] # getpid takes no args echo $pid
### Errors
`ffi/load`, `ffi/sym`, and `ffi/call` return `%Err` on failure:
def bad [ffi/load "nonexistent.so"] if [err? $bad] { echo "missing: $bad~message" }
A wrong-argument-count call returns `%Err`. Type mismatches raise
at the C call boundary — wrap risky calls in `try` if you don't
trust the input.
## Function Declaration Parsing
For libraries with rich C headers, `ffi/cdef` parses a C function
signature string and registers the function for tilde-dispatch on
the library:
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
Supported declarations:
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();"
Once registered, `$lib~name args...` invokes the function with the
declared argument and return types. No need to thread types through
every call.
## C Strings
def cs [ffi/cstring "hello world"] set s [ffi/string $cs] # back to Deft string echo $s # => hello world ffi/close $cs
Empty strings round-trip correctly. The cstring resource holds a
null-terminated buffer the GC can manage.
## Native Memory
`ffi/alloc N` allocates an N-byte buffer. `ffi/read` and `ffi/write`
move typed values in and out:
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
Use `ffi/addr` to get the raw integer address of a buffer or
pointer; `ffi/ptr` wraps a raw address back into a pointer
resource. `ffi/nullptr` constructs a null pointer (whose `ffi/addr`
is 0).
## Struct Definitions
`ffi/cdef "struct Name { ... };"` parses a C struct definition
and registers it under the name (returned as a string):
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)
### Primitive type sizes
| Type | Bytes |
|---|---|
| `::i8`, `::u8` | 1 |
| `::i16`, `::u16` | 2 |
| `::i32`, `::u32` | 4 |
| `::i64`, `::u64` | 8 |
| `::f64` | 8 |
| `::ptr` | 8 (on 64-bit) |
C struct alignment and padding follow the platform ABI: e.g.
`struct Mixed { char c; int i; double d; }` is 16 bytes (1 char +
3 bytes padding + 4 int + 8 double).
### Allocating and using structs
`ffi/new` allocates a zero-initialised instance:
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
### Tilde field access
Once a struct is registered, `$instance~field` reads the field:
ffi/cdef "struct TildePoint { int x; int y; };" def p [ffi/new "TildePoint"] ffi/set $p "x" 42 echo $p~x # => 42
This works for any field name in any registered struct.
## Pointers and GC Safety
Deft's GC may move heap-allocated values. If you're holding a
pointer into GC-managed memory across an FFI call, pin it first:
def s "important" ffi/pin $s # ... do FFI work referencing $s ... ffi/unpin $s
`ffi/pin` returns the value unchanged; it just marks it as
non-movable. Unpin when done to let the GC reclaim it.
For buffer resources from `ffi/alloc`, no pinning is needed —
those are C-heap allocations.
## Callbacks (`ffi/callback`)
Wrap a Deft closure as a C function pointer:
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]
The closure must remain live (or be pinned) for as long as C might
call it.
## Destructors (`ffi/gc`)
Attach a C destructor to a native resource. When the GC reclaims
the wrapper (or you `ffi/close` it explicitly), the destructor
runs:
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
Use this when you're given a pointer by a C library and need to
ensure its destructor runs even if your script exits early.
## A Real Example: Video via deft/ffmpeg
The `deft/ffmpeg` package (in `stdext/src/ffmpeg/`) is a full FFI video
player: it dlopens the system `libav*` libraries, probes the FFmpeg
major version, reads libav structs through per-version offset tables,
and decodes on a defworker. A simplified sketch of the pattern:
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
See `stdext/src/ffmpeg/` for the complete binding (offset tables,
decoder workers, streaming playback).
## Limits and Caveats
- **Max 10 arguments** per `ffi/call` when all arguments are
integer/pointer; max 6 when any argument is a float (mixed
int+float patterns max 3). Longer signatures (e.g. `printf`
with many args, `sws_getCachedContext`) aren't supported.
- **x86_64 only.** The C-call trampolines target the System V ABI.
AArch64 support is on the roadmap but not yet shipped.
- **No variadic C functions yet.** `printf`-style APIs aren't
supported — wrap them in a fixed-arity helper.
- **No bitfields or unions** in `ffi/cdef`.
- **Callbacks are synchronous.** The Deft closure runs on the
thread that called it; if C calls it from a non-Deft thread,
behaviour is undefined.
- **Resource lifetime is GC-managed.** `ffi/close` is optional;
the GC will release resources eventually. Use it for prompt
cleanup of large buffers or limited system resources.
## Quick Reference
# 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