# 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