13. FFI / External Functions
A program calls a C function by declaring it with @extern: a signature without a body, whose types say what crosses to C. Decorators on the declaration and its parameters say how a value crosses when its type does not. A native build calls the function as C does; vader run looks it up when the call runs (VM behaviour).
@extern: declaring imports#
import "std/ffi"
LIBM: CLibrary: CLibrary { .name = "m" }
@extern add_i32 :: fn(a: i32, b: i32) -> i32 // C symbol: add_i32
@extern("puts") c_puts :: fn(s: string) -> i32 // C symbol: puts
@extern(LIBM, "cbrt") cube_root :: fn(x: f64) -> f64 // C symbol: cbrt, from LIBM
@extern takes 0 to 2 arguments:
- without arguments, the symbol is the function's own name;
- the last argument, a string, is the symbol: the C linker symbol, or the
fieldof a WASM import on the planned WASM target (Future WASM target); - the first of two is the library, a
CLibraryconstant (below). Without one, the symbol is one the program links anyway, such as a C library function.
The following are errors:
- a body;
- more than two arguments;
- a symbol that is not a plain string literal, an interpolated string included;
- a library that is not a constant of type
CLibrary; - two
@externdeclarations of one C symbol, even in two modules, unless both take their prototype from a header (@c_header).
CLibrary: the library a binding calls#
import "std/ffi"
import "std/target"
export RAYLIB: CLibrary: CLibrary {
.name = "raylib",
.header = "raylib.h",
.include = "include",
.archives = ["lib/${VADER_OS}-${VADER_ARCH}/libraylib.a"],
.system = match VADER_OS {
.Darwin -> ["Cocoa.framework", "IOKit.framework", "OpenGL.framework"]
.Linux -> ["GL", "m", "pthread", "dl", "rt", "X11"]
.Windows -> ["opengl32", "gdi32", "winmm"]
.Wasi | .Browser -> []
},
}
@extern(RAYLIB, "InitWindow")
export init_window :: fn(width: i32, height: i32, title: string) -> void
A CLibrary (std/ffi) names a C library once; each @extern from it names the constant. The compiler evaluates the constant at compile time for the build target, without @comptime, so its fields may depend on VADER_OS and VADER_ARCH. It may use:
- literals, struct literals and enum variants;
- a
matchwith variant and_arms; - constants and single-expression functions, from any module;
- the interpolation of an enum whose
Displayimpl is one expression overself.
Anything else is an error, reported where the evaluation stopped. The fields:
name: the libraryvader runopens (lib<name>.dylibon macOS,lib<name>.soon Linux,<name>.dllon Windows), unlessarchivesis set, whose functions go through adapters (VM behaviour). A native build links onlyarchivesandsystem.header: the header declaring the library's functions, written as the argument of@c_header. It serves each@externof the library without a@c_headerof its own.include: a header folder searched by every compilation, relative to the declaring module. A quoted header ("raylib.h") is found there.archives: static archives shipped with the binding, relative to the declaring module. A missing one stops the build before the C compiler runs, naming the path.system: platform libraries by name ("GL"links-lGL) and Apple frameworks by bundle name ("Cocoa.framework"links-framework Cocoa).
A native build adds a library's flags only when the program reaches one of its functions, and compiles each library's calls with its own header alone, so headers declaring the same names (Rectangle in raylib.h and <windows.h>) bind in one program.
@c_pointer: a parameter crossing as an address#
A C function often takes a pointer where the value would do (localtime(const time_t *)). @c_pointer on the parameter passes C the address of a copy of the argument, valid for the call; no pointer type appears in Vader.
TimeT :: i64
@extern("localtime")
localtime :: fn(@c_pointer t: TimeT) -> CPointer
@c_pointer applies to a scalar (a number, bool, char, CPointer, or a distinct type over one), which crosses read-only as a const T* (@c_out), or to a @c_struct mirror (below).
The following are errors:
- any other decorator on a parameter;
@c_pointeror@c_outon a parameter of a function that is not@extern;- an argument to
@c_pointeror@c_out; @c_pointeron any other type, astringor an array included, which already cross as pointers.
@c_out: C writes through the parameter#
@extern("read")
sys_read :: fn(fd: i32, @c_out buf: u8[], count: i64) -> i64
Without @c_out, C only reads what it is lent: an array crosses as a const void*, and a mirror lent with @c_pointer is not read back. With it, an array crosses as a void* whose elements C writes in place, and a mirror is copied back after the call.
@c_out needs storage that outlives the call, an array or a mirror lent with @c_pointer: on a scalar, a string or a mirror passed by value, it is an error. To read a scalar back on both backends, lend a one-element array (@c_out written: u32[]), the shape of every T* out-parameter (WriteFile's lpNumberOfBytesWritten).
Passing a frozen value (§5 Frozen values) to a @c_out parameter is an error, as to any parameter its callee modifies.
@c_header: letting the header own the prototype#
Without a header, the compiler declares the C function from its Vader signature, which fails the build when the C compiler already sees a prototype (clock_gettime in <time.h>). @c_header includes the header instead, and the compiler declares nothing:
@c_header("<time.h>")
@extern("clock_gettime")
sys_clock_gettime :: fn(clock_id: i32, @c_out ts: i64[]) -> i32
The C compiler then checks every argument against the real declaration. Without a header nothing does: isatty declared as fn(fd: i64) -> i64 compiles, links, and passes its argument wrongly.
The argument is written as in a C #include:
"<time.h>"is a system header;"\"my_header.h\"", or a name without delimiters ("raylib.h"), is a local header, searched in the directory of the entry file wherever--outwrites the C:"\"include/my.h\""is one folder below the entry.
@c_variadic: a variadic C function#
A variadic C function (fcntl, ioctl) is declared once per argument list the program calls it with, each declaration taking its prototype from a header (@c_header or its library's header) to share one symbol (@extern). @c_variadic(N) gives the number of fixed parameters, those before the ... of the C prototype:
@c_header("<fcntl.h>")
@c_variadic(2)
@extern("fcntl")
set_flags :: fn(fd: i32, command: i32, flags: i32) -> i32
A native build reads the prototype from the header, vader run reads N: without it, vader run calls the function as a fixed one, and on some systems (darwin-arm64) the function then reads its variadic arguments from the wrong place.
@c_type: the C spelling of an integer#
Without a header, the compiler spells a number by its Vader type (a u64 is a uint64_t), which conflicts with a header that uses another name for the same width (size_t, off_t, DWORD). @c_type names the C type:
@c_type("size_t")
@extern("strnlen")
c_strnlen :: fn(s: string, @c_type("size_t") max: u64) -> u64
On a parameter, @c_type spells that parameter; on the declaration, the result. The value is converted to the C type and back, and the C type must be an integer exactly as wide as the Vader one, or a native build fails. With @c_header, the header gives the prototype and @c_type only adds the conversions.
The following are errors:
@c_typewithout exactly one argument, a plain string;@c_typeon a parameter or a result that is not an integer, or a distinct type over one;@c_typeoutside an@externdeclaration.
@c_struct: mirroring a C struct#
A struct marked @c_struct mirrors a C struct, and can cross an @extern call as a parameter or as the result.
@c_struct("div_t")
Div :: struct { quot: i32, rem: i32 }
@extern fill_div :: fn(@c_pointer @c_out d: Div, quot: i32, rem: i32) -> void
@c_header("<stdlib.h>")
@extern("div") c_div :: fn(numerator: i32, denominator: i32) -> Div
@c_struct takes at most one argument, a plain string naming the C type ("struct pollfd", "div_t"); without it, the C type is the struct's own name, read as a typedef name.
Each field is copied to and from the C member of the same name, which the C compiler places from the real header:
- by address, for a parameter with
@c_pointer, C receives a C struct filled from the mirror, copied back after the call with@c_out; - by value otherwise, C receives a copy passed as the platform's ABI wants, and a result is copied into a new Vader struct, nested mirrors included.
A mirrored field is a number, bool, char, CPointer, an enum with a written width and no variant data, or another @c_struct. Any other field type, a plain struct included, is an error when the mirror crosses an @extern.
A mirror is a contiguous leading prefix of the C struct: its members in order, from the first, none skipped. Lent by address, it may stop early, and the rest is ignored. Crossing by value, it names every member; the check misses only a member small enough to hide in the struct's tail padding.
A native build checks each mirror against the header, and fails when:
- a field names no member of the C struct;
- a field's width or signedness differs from its member's (A mirrored field's width);
- a field is not at its member's offset, as when a member is skipped;
- a mirror lent by address is larger than the C struct, or one crossing by value is not exactly its size.
Under vader run alone, nothing reports a mirror breaking these rules, which can hand C its fields at the wrong offsets.
A member that is itself a struct takes a nested mirror, a field whose type is another @c_struct: C holds it inline, Vader as a struct of its own, and each leaf is copied by its path (.srWindow.Right). A mirror nests at most 16 levels deep.
@c_struct("SMALL_RECT")
SmallRect :: struct { Left: i16, Top: i16, Right: i16, Bottom: i16 }
// Whole, not `{ srWindow: SmallRect }`: `srWindow` is the fourth member, and a
// mirror that skipped the three before it would fail the offset check.
@c_struct("CONSOLE_SCREEN_BUFFER_INFO")
ScreenBufferInfo :: struct {
dwSize: Coord
dwCursorPosition: Coord
wAttributes: u16
srWindow: SmallRect
dwMaximumWindowSize: Coord
}
A C struct whose member order or offsets differ between systems (struct stat) cannot be mirrored; docs/adr/0013-what-the-ffi-boundary-cannot-express.md gives what to use instead.
A mirrored field's width#
A field's width is that of its Vader type, and must match its C member's exactly, signedness included, whichever way the copy runs. A type whose width differs between systems (suseconds_t: 4 bytes on Darwin, 8 on Linux) takes one mirror and one @extern per system, chosen with @target at the call (§17 Target selection), not the wider of the two.
The opaque buffer, when there is no mirror to write#
A C struct whose members Vader never reads can cross as a lent u8[] that nothing interprets, the same array handed back each time: termios in lib/system/posix (tcgetattr fills it, tcsetattr applies it).
Nothing checks the buffer: the callee writes sizeof(struct ...) bytes whatever the reserved size, so that size must cover the largest system in scope, or the write goes out of bounds unreported. Use it only when no mirror can exist: no member Vader needs to name, or no stable public layout.
Allowed signature types#
These scalars cross as their C counterparts (void as a result only, null never):
i8 i16 i32 i64 u8 u16 u32 u64
f32 f64 bool char void string
The shapes below cross too, only in the direction given. Any other type in an @extern signature (a plain struct, a trait, a type parameter) is an error.
| Shape | Crosses as | Direction |
|---|---|---|
CPointer |
An address C hands out | Both |
| A distinct type | Its backing type | Both |
CPointer | null |
An address, with null as NULL |
Both |
string | null |
A C string, with null as NULL |
Result only |
An array of scalars other than string |
A pointer to its first element | Argument only |
A @c_struct mirror |
A C struct, by address or by value | Both |
| An enum with a written width | The integer value of its variant | Argument only |
| A function type | The address of a named function | Argument only |
A string argument reaches C as a NUL-terminated UTF-8 const char*, valid for the call only: nothing reports a callee that keeps it. A string result is copied before the arguments are released, so one pointing into an argument (strstr) reads correctly. Vader never frees the pointer C returns, which is right for one the library owns (getenv) and a leak for one the caller should free. A NULL string result is "", or null for a string | null result, telling "absent" from "empty" (getenv).
CPointer (§4 Primitive types) is an address C hands out: a program receives it, passes it back and compares it, but cannot read through it or build one. Handle :: CPointer, a distinct type, names a kind of handle. CPointer | null narrows on NULL alone: a sentinel that is not NULL (INVALID_HANDLE_VALUE, MAP_FAILED, both -1) is tested through i64(p) or u64(p), which hold an address on every target. Converting a CPointer to a narrower integer or a float, or an integer to a CPointer, is an error.
An array crosses as a pointer to its first element; a length the callee wants is a parameter of its own.
An enum with a written width (§4 Representation) crosses as its variant's integer at that width; an enum with variant data crosses as the value written after its = (§4 Variant data). The following are errors:
- an enum whose width is inferred;
- an
@externthat takes both an enum with variant data and a callback.
Further pointer types, with an unsafe block, are deferred (Appendix A).
Callbacks#
A parameter of function type is a C callback: C receives the function's address and calls it with its signature's types.
double_it :: fn(x: i32) -> i32 = x * 2
@extern call_twice :: fn(f: fn(i32) -> i32) -> i32
call_twice(double_it) // ok
call_twice((x: i32) -> x * 2) // error: a lambda is not a named function
- The argument names a function of a module, directly or through a namespace alias (
M.double_it). A lambda, a closure, or a local or parameter holding a function is an error, whatever its type. - A callback runs only in a native build: under
vader run, a call that passes one stops the program. - A callback's signature holds only numbers,
bool,char,CPointer, distinct types over those, and avoidresult; any other type,stringand arrays included, is an error.qsort's comparatorint (*)(const void *, const void *)isfn(CPointer, CPointer) -> i32.
Native target: code generation#
A native build calls each @extern as a C function linked into the binary (Linking). The compiler writes every conversion the call needs, and the prototype when no header gives it (@c_header), so a program never writes C glue.
Linking#
A native build links what the program's CLibrary constants declare. Anything else, an object file or a library no CLibrary declares, goes to the linker on the command line (docs/TOOLING.md vader build).
VM behaviour#
// with c_puts and cube_root of *`@extern`*
NOPE: CLibrary: CLibrary { .name = "nosuchlib" }
@extern(NOPE, "f") f :: fn() -> i32
@extern("no_such_fn") missing :: fn() -> i32
c_puts("hi") // prints hi: puts, from the running process
cube_root(27.0) // 3.0: cbrt, from libm, opened at the call
f() // stops the program, naming `f` and the library
missing() // stops the program, naming the symbol
vader run looks an @extern up when the call runs, in the library its CLibrary's name gives, or without one in the running process (on Windows, the C runtime); nothing has to be registered. The program stops, naming the @extern and the reason, when the library does not open, the symbol is missing, or an array argument's elements are not integers.
At compile time (§14), a call that needs an adapter (docs/TOOLING.md vader run) fails, naming the reason.
Future WASM target#
A WASM target is planned (§17 Compilation Targets). There, @extern will also give a WASM import, a (module, field) pair whose module is "env" without a library: @extern("console_log") log becomes (import "env" "console_log" (func ...)). How to name another module is not designed yet.
@export: exposing a function#
@export is planned: it exposes a function to the host under an unmangled name, its own or the argument. Accepted today, it keeps the function in the program but does not change its name yet. On the WASM target, the host is JavaScript:
@export("addNumbers")
add_numbers :: fn(a: i32, b: i32) -> i32 {
return a + b
}
JS side:
const { instance } = await WebAssembly.instantiateStreaming(fetch("app.wasm"));
console.log(instance.exports.addNumbers(2, 3));