Skip to content

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:

wyn
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:

WynC
intlong long
floatdouble
boolbool
stringconst char*
void / omittedvoid
ptrvoid*

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.

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 ...:

wyn
extern fn my_log_init(level: int);              // void return
extern fn my_logf(fmt: string, ...) -> int;     // variadic

Pointer 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.

wyn
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 cell
wyn
extern 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:

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:

c
// mylib.c  →  compiled to libmylib.a
int triple(int x) { return x * 3; }
toml
# wyn.toml
[ffi]
libs = "mylib"
lib_dirs = "."
wyn
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 any libs/lib_dirs/ include_dirs value containing shell metacharacters, so a wyn.toml from 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:

bash
wyn bind sqlite3.h > sqlite3.wyn

wyn 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.

bash
wyn add sqlite3

That 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:

wyn
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.

See also

MIT License - v1.21.0