C FFI - Calling C from Wyn
Wyn compiles to C, which means the entire C ecosystem is within reach. Declare a C function with extern fn, and Wyn will call it directly - no wrappers, no glue code.
Declaring an external function
An extern fn is a declaration only - no body, ended with a semicolon. It names a C function and its signature:
extern fn sqrt(x: float) -> float;
extern fn pow(base: float, exp: float) -> float;
fn main() {
print("${sqrt(16.0)}") // 4.0
print("${pow(2.0, 10.0)}") // 1024.0
}The C math library is linked by default, so these run without any extra setup.
Type mapping
Wyn types map to C types as follows:
| Wyn | C |
|---|---|
int | long long |
float | double |
bool | bool |
string | const char* |
void / omitted | void |
ptr | void* |
Use ptr for opaque C pointers and handles - malloc's result, a FILE*, a sqlite3*, or any void*. A ptr is an opaque machine word: you can hold it in a variable, pass it to other extern fns, and null-check it against 0 (the C NULL idiom), but you can't dereference it from Wyn.
extern fn malloc(size: int) -> ptr;
extern fn free(p: ptr);
fn main() {
buf = malloc(1024)
if buf == 0 {
print("out of memory")
} else {
// … pass buf to other extern fns …
free(buf)
}
}A function with no -> T returns void. Variadic C functions are supported with ...:
extern fn my_log_init(level: int); // void return
extern fn my_logf(fmt: string, ...) -> int; // variadicPointer cells (Ptr) - C out-parameters
Many C APIs return a handle through an out-parameter of type T** - for example sqlite3_open(const char* path, sqlite3** out). Wyn has no address-of operator, so the Ptr helpers give you a heap cell that holds one pointer: pass the cell where the C function expects a T**, then read back the pointer it stored.
Ptr.cell() // -> ptr : a fresh zeroed pointer-sized cell (a T**)
Ptr.read(cell) // -> ptr : the pointer the callee stored into the cell
Ptr.write(cell, p) // store a pointer into the cell
Ptr.free(cell) // release the cellextern fn sqlite3_open(path: string, out: ptr) -> int;
fn main() -> int {
cell = Ptr.cell()
if sqlite3_open(":memory:", cell) != 0 { return 1 }
db = Ptr.read(cell) // the sqlite3* handle
// … use db …
Ptr.free(cell)
return 0
}TIP
You can declare and call standard-library functions directly - printf, puts, malloc, free, memset, sqrt, and friends all work. If a function is already declared by a standard header, Wyn skips emitting a duplicate prototype and calls the existing one, so there's no conflict. extern fn is equally for your own and third-party libraries linked via [ffi].
Linking a C library
To call into a library other than libc, tell the compiler what to link in your project's wyn.toml:
[ffi]
libs = "curl, z"
lib_dirs = "/usr/local/lib"
include_dirs = "/usr/local/include"libs- comma- or space-separated library names; each becomes-l<name>.lib_dirs- extra library search paths (-L). Optional.include_dirs- extra header search paths (-I). Optional.
The compiler passes these flags to the C compiler and links your program against the named libraries automatically. wyn.toml is only consulted when your program actually declares an extern fn.
Example: a custom C library
Given a compiled libmylib.a in your project directory:
// mylib.c → compiled to libmylib.a
int triple(int x) { return x * 3; }# wyn.toml
[ffi]
libs = "mylib"
lib_dirs = "."extern fn triple(x: int) -> int;
fn main() {
print("${triple(7)}") // 21
}Safety notes
- The FFI boundary is unchecked. A foreign call escapes Wyn's type system and memory guarantees - Wyn trusts the signature you declare. A wrong signature is undefined behavior, exactly as in C. Wrap foreign calls in a thin, well-tested Wyn layer rather than scattering them through your code.
[ffi]values are validated. The compiler rejects anylibs/lib_dirs/include_dirsvalue containing shell metacharacters, so awyn.tomlfrom an untrusted source cannot inject a shell command into the build.
Generating bindings from a header
You don't have to hand-write extern fn declarations. wyn bind reads a C header and emits the Wyn bindings for every function it can represent:
wyn bind sqlite3.h > sqlite3.wynwyn add - curated C packages
wyn add <name> pulls a curated C library end to end: it resolves the library, generates its bindings, and records the link flags in your wyn.toml - so you can import it and start calling. Nine libraries ship as curated recipes - m (libm), z (zlib), curl, sqlite3, crypto and ssl (OpenSSL), curses (ncurses), readline, and xml2 (libxml2). Run wyn add --list for the current set.
wyn add sqlite3That writes packages/sqlite3/sqlite3.wyn (the generated bindings) and adds [ffi] libs = "sqlite3" to wyn.toml. Then a real program - open an in-memory database, insert rows, query them back:
import sqlite3
fn run(db: ptr, sql: string) {
var cell = Ptr.cell()
sqlite3_prepare_v2(db, sql, -1, cell, 0)
var stmt = Ptr.read(cell)
sqlite3_step(stmt)
sqlite3_finalize(stmt)
Ptr.free(cell)
}
fn main() -> int {
var dbcell = Ptr.cell()
if sqlite3_open(":memory:", dbcell) != 0 { return 1 }
var db = Ptr.read(dbcell)
run(db, "CREATE TABLE nums (v INTEGER, label TEXT)")
run(db, "INSERT INTO nums VALUES (10, 'ten')")
run(db, "INSERT INTO nums VALUES (20, 'twenty')")
var cell = Ptr.cell()
sqlite3_prepare_v2(db, "SELECT v, label FROM nums ORDER BY v", -1, cell, 0)
var stmt = Ptr.read(cell)
while sqlite3_step(stmt) == 100 {
print("${sqlite3_column_int(stmt, 0)} ${sqlite3_column_text(stmt, 1)}")
}
sqlite3_finalize(stmt)
Ptr.free(cell)
sqlite3_close(db)
Ptr.free(dbcell)
return 0
}C-package bindings are called flat (sqlite3_open(...), not sqlite3.sqlite3_open(...)) - the extern fn names are already fully qualified by the library's own C naming convention.