ffi
Call C libraries. Declare the functions you need with their C signatures, copied from the header, and call them like Oxigen functions. For how this works inside the VM, see FFI architecture.
introduce ffi
libm := ffi.open("m", {
"cos": "double(double)",
"pow": "double(double x, double y)",
})
println(libm.cos(0.0)) // 1
println(libm.pow(2.0, 10.0)) // 1024| Function | Signature | Returns |
|---|---|---|
open | open(lib, decls?) | a module of the declared functions |
fn | fn(lib, signature) | one function |
cstruct | cstruct(name, fields) | the struct's name |
callback | callback(signature, func) | a C function pointer |
alloc | alloc(size) | pointer to zeroed memory |
new | new(type, value?) | pointer to one zeroed type |
free | free(ptr) | None |
sizeof | sizeof(type) | int |
read | read(ptr, type, index?) | the value |
write | write(ptr, type, value, index?) | None |
string | string(ptr, len?) | str |
cstring | cstring(s) | pointer to a C copy of s |
sym | sym(lib, name) | address of a symbol |
#Libraries
A library is one of:
- a bare name:
"m","sqlite3","z". Oxigen tries the platform's file names (libm.dylib;libm.so,libm.so.6, ...;m.dll) and the usual install directories (on macOS, Homebrew's/opt/homebrew/lib). - a file name or path:
"libfoo.so.2","./build/libgame.dylib". None: the C runtime (malloc,strlen,qsort,snprintf, ...).
A library is loaded once and stays loaded.
#open(lib, decls)
Binds every function in decls, a map of name to C signature, and returns
them as a module:
libc := ffi.open(None, {
"strlen": "size_t(const char *s)",
"getenv": "char *(const char *name)",
})
println(libc.strlen("hello")) // 5
println(libc.getenv("HOME")) // /Users/youThe key is the C function's name. To call it something else, put the C name
in the signature: {"len": "size_t strlen(const char *)"}. A missing symbol or
a bad signature fails here, not on the first call.
#fn(lib, signature)
One function, named in the signature:
abs := ffi.fn(None, "int abs(int)")
println(abs(-3)) // 3Given a function pointer instead of a library, fn makes it callable; the
name can then be left out: ffi.fn(ptr, "int(int)").
#Signatures and types
Signatures are C: ret name(params), with the name optional and parameter
names allowed and ignored. const, struct and volatile are ignored too.
| C type | Oxigen value |
|---|---|
char, short, int, long, long long, their unsigned forms, int8_t ... uint32_t, ssize_t, intptr_t, enum X | int |
unsigned long long, uint64_t, size_t, uintptr_t | uint |
bool | bool |
float, double | float |
char * | str in and out (None for NULL) |
void *, function pointers | int address, None for NULL |
T * | an address, or an array or map to pass in and out (see below) |
a struct from cstruct, by value | map of field name to value |
void | return type only: None |
The short names i8 ... i64, u8 ... u64, f32, f64, str and ptr
work too.
A function returning char * gives a copy of the string. If you must free
the pointer afterwards, declare the return as void *, read it with
ffi.string, then free it.
#Pointer parameters
For a T * parameter, pass an Oxigen array (or a map, for a struct). It is
copied into C memory for the call and copied back afterwards, which is how out
parameters work:
libm := ffi.open("m", {"frexp": "double(double x, int *exp)"})
exp := [0]
println(libm.frexp(8.0, exp)) // 0.5
println(exp[0]) // 4An address from ffi.alloc or another C call works too, and so does None
for NULL. A void * parameter takes a string or an address, never an array:
declare the element type (int *) to pass one.
#Variadic functions
End the parameters with .... Extra arguments go as C's default promotions:
int as long long, float as double, str as char *.
libc := ffi.open(None, {"snprintf": "int(char *buf, size_t n, const char *fmt, ...)"})
buf := ffi.alloc(64)
libc.snprintf(buf, 64, "%s=%d (%.1f)", "x", 42, 2.5)
println(ffi.string(buf)) // x=42 (2.5)
ffi.free(buf)#cstruct(name, fields)
Declares a C struct so its name works as a type in signatures, by value or as
Name *. List the fields as in C:
ffi.cstruct("div_t", "int quot; int rem")
ffi.cstruct("Vec2", ["double x", "double y"]) // an array works too
libc := ffi.open(None, {"div": "div_t(int, int)"})
println(libc.div(7, 2)) // {quot: 3, rem: 1}Struct values are maps. Missing fields are zero, and an array or tuple sets the fields in order. Layout and padding follow the platform's C ABI. A struct can contain another struct declared before it.
To paste a declaration straight from a header, give it as one string, with
its braces escaped (\{ \}), since a bare { in an Oxigen string starts an
interpolation:
ffi.cstruct("typedef struct \{ int id; char *name; \} Item;")#callback(signature, func)
A C function pointer that calls func, for APIs that take callbacks:
libc := ffi.open(None, {"qsort": "void(int *base, size_t n, size_t size, void *cmp)"})
cmp := ffi.callback("int(const void *a, const void *b)", fun(a, b) {
ffi.read(a, "int") - ffi.read(b, "int")
})
xs := [5, 1, 4, 2, 3]
libc.qsort(xs, len(xs), ffi.sizeof("int"), cmp)
println(xs) // [1, 2, 3, 4, 5]The callback gets its arguments converted the same way as return values: a
char * arrives as a str, a struct as a map. If it fails, the C call that
ran it fails with the same error once C returns. A callback stays valid until
the program exits. C must call it on the script's thread while a C function
the script called is still running, which is how sorting, iteration and
event-polling APIs work. A callback that a C thread calls later, on its own,
fails.
#Memory
alloc, new and cstring return C memory that you free with free.
read and write take a C type and an optional index into an array of them:
p := ffi.new("Vec2", {"x": 1.5, "y": -2.0})
println(ffi.read(p, "Vec2")) // {x: 1.5, y: -2}
println(ffi.read(p, "double", 1)) // -2 (the second double)
ffi.write(p, "double", 9.0)
ffi.free(p)ffi.string(ptr) reads a NUL-terminated string, and ffi.string(ptr, len)
reads len bytes. ffi.cstring(s) makes a C copy of s that outlives the
call, for a char * struct field or a global.
#sym(lib, name)
The address of a symbol, usually a global variable a library exports:
ffi.read(ffi.sym("mylib", "mylib_version"), "int").
#A complete example
SQLite, with an out parameter and a callback per row:
introduce ffi
sq := ffi.open("sqlite3", {
"open": "int sqlite3_open(const char *path, void **db)",
"exec": "int sqlite3_exec(void *db, const char *sql, void *cb, void *arg, char **err)",
"close": "int sqlite3_close(void *db)",
})
db := [None]
sq.open(":memory:", db)
sq.exec(db[0], "create table t(name text); insert into t values ('a'), ('b');", None, None, None)
row := ffi.callback("int(void *arg, int n, char **vals, char **cols)", fun(_, n, vals, cols) {
println(ffi.read(vals, "char *", 0))
0
})
sq.exec(db[0], "select name from t", row, None, None)
sq.close(db[0])#Threads
A library opened at the top level of a script cannot be used inside a
diverge task: the task runs on its own VM, and C functions do not cross into
it. Put the declarations in a function and call it inside the task. Each
thread loads the library once and reuses it after that:
fun libc() { ffi.open(None, {"labs": "long(long)"}) }
println(diverge each n in [-1, -2, -3] { libc().labs(n) }) // [1, 2, 3]Structs are declared per thread too: call ffi.cstruct inside the task before
using one there.
#Safety
The signature is a promise the program cannot check. A wrong one, a bad pointer or a freed buffer is undefined behaviour, as in C, and can crash the process.
Every ffi function needs the ffi capability. A host that calls
vm.restrict must list Capability::Ffi to allow it, and should grant it only
to a script it would trust as native code.
#Limits
- Struct fields cannot be inline arrays (
char name[16]). Declare the bytes as separate fields, or use a pointer andffi.readwith an index. - No unions, bit-fields or
long double. - Not available in the browser playground (WebAssembly).
- The prebuilt Linux release binaries are statically linked against musl,
which cannot load shared libraries: every
ffi.openfails there. Build from source (./scripts/install.sh) to useffion Linux. The macOS releases are not affected. - Windows support (
LoadLibrary, the universal CRT forNone) is untested.