Foreign functions
A foreign "C" block declares functions that live outside Dusk, so dusk code can call into C. The functions have no body. Each binds at link time to a C symbol of the same name in anything the binary links, which is libc, libm, and the dusk runtime today. The standard library uses this to bind the runtime’s cool_* shims, and std.math binds libm’s scalar functions the same way.
Foreign functions arrived in 0.2.4 with the scalar and raw pointer boundary, and three releases since have widened it. 1.4.0 brought the variadic C function, the @link and @csource directives that reach the linker, and std.os’s view of the C library’s errno. 1.4.1 let a C plain struct cross by value, classified the way clang’s own ABI places it. 1.4.2 let a foreign parameter be a function type, so a dusk function crosses as a bare C function pointer a C API calls back into. This page is the normative reference for that boundary. For the pointer layers a call crosses on, see memory management and the type system.
The raw pointer boundary
Section titled “The raw pointer boundary”The boundary is the raw pointer layer only. A parameter or return type is a scalar, a *raw T, or a *void. A scalar is an integer of any width, float32, float64, bool, char, or rune. A buffer crosses as *raw T, and an opaque pointer as *void. Two narrower rules widen that set past the bare word: a named struct crosses by value when every one of its fields is C plain, covered in structs by value, and a function type crosses as a callback, covered in callbacks.
A managed *T is rejected at the boundary, since it is a fat value carrying a generation that C cannot read. A *raw T passes anywhere a *void is expected, because both are the same bare word, but the reverse binding is refused: a *void that could become a typed *raw T would let a managed pointer launder into a dereferenceable alias the generation check can no longer see. Keep managed pointers on the managed layer, and hand C only the raw one. The split between the two layers lives in memory management.
Once declared, a foreign function is called like any other function. This program copies a string into a raw buffer, hands it to C’s strlen, and frees the buffer it owns:
@import std.string
foreign "C" { func strlen(s: *raw char) -> int64}
func main() -> int32 { buf: *raw char = cbuf("hello") n: int64 = strlen(buf) free(buf) println(n) // 5 return 0}cbuf, covered below, is the bridge from a string view to the NUL terminated *raw char a C function reads. Only the "C" calling convention is supported.
Variadic foreign functions
Section titled “Variadic foreign functions”Added in 1.4.0. A foreign block’s parameter list may end in a bare ..., marking the function variadic in the C sense. This is how you reach printf, snprintf, and the rest of C’s variadic family.
@import std.string
foreign "C" { func printf(fmt: *raw char, ...) -> int32}
func main() -> int32 { fmt: *raw char = cbuf("answer = %d\n") printf(fmt, 42) free(fmt) return 0}The ... is legal only as the last parameter. Writing it anywhere else is rejected:
'...' must be the last parameter of a variadic foreign functionOnly a foreign declaration can take ... at all. An ordinary dusk function still cannot be variadic.
Arity and the admitted tail
Section titled “Arity and the admitted tail”A call checks its fixed parameters exactly as an ordinary call does, and must supply at least that many. Falling short is rejected by name before a single type is checked:
expected at least N argument(s), found MEvery argument beyond the fixed count is the vararg tail, and each one must be in the admitted crossing set: a scalar (an integer of any width, float32, float64, bool, char, rune), a *raw T, or a *void. C reads a vararg positionally with no type of its own to check against, so a fat or generation carrying value handed across would read as garbage. Anything outside the set is rejected by kind at the argument that carries it, each with a fix:
argument N to a variadic foreign function is a managed pointer; pass a *raw T or *void, since a managed *T carries a generation C cannot readargument N to a variadic foreign function is a string; pass a *raw char, a string view cannot cross a C vararg directlyargument N to a variadic foreign function is a struct value; pass a pointer to itA slice or array asks for its backing as a *raw T, a tuple for its elements passed individually, and a closure, a collected value, a future, an interface value, or an error is named outright. The flat form of the rule reads a variadic foreign function takes only scalars and raw pointers.
To pass a string, copy it to a *raw char with cbuf first, exactly as the printf sample above does with its format string.
Default argument promotions
Section titled “Default argument promotions”A value riding the ... tail takes the C default argument promotions before it is passed, the same widening a C compiler applies at its own variadic call sites:
| Argument type | Crosses as |
|---|---|
int8, int16 | int32, sign extended |
char, bool | int32, zero extended |
float32 | float64 |
int32, int64, rune, a raw pointer, *void | unchanged |
A fixed parameter never promotes. Its own declared type already tells C what to expect at that position, so only a value in the tail widens. This matters when you write the format string: a char or a bool you pass to printf arrives as an int32, and a float32 arrives as a float64, so the conversion in the format holes must match the promoted width, not the source one.
Structs by value
Section titled “Structs by value”Added in 1.4.1. A named struct may be a foreign function’s fixed parameter type or its return type, crossing by value, when every one of its fields is C plain: an integer of any width, char, rune, float64, a *raw T, a *void, a nested struct that is itself C plain, or a fixed array of any of those.
libc’s ldiv returns one such struct, a quotient and a remainder side by side, so you can declare the shape yourself and take the return straight across with no C of your own in between:
struct LDiv { quot: int64, rem: int64 }
foreign "C" { func ldiv(numer: int64, denom: int64) -> LDiv}
func main() -> int32 { r: LDiv = ldiv(17, 5) println(r.quot) // 3 println(r.rem) // 2 return 0}The struct you declare has to match the C layout field for field, and nothing checks your declaration against the C header. LDiv above is correct because ldiv_t really is two longs in that order; get the order or a width wrong and the call miscompiles quietly.
What is not C plain
Section titled “What is not C plain”A field of any other type is rejected by name at the struct, and every message names the field and the fix:
field '<name>' is bool; use char or int8 at the boundaryfield '<name>' is float32; use float64 or pass by pointerfield '<name>' is a managed pointer; use *raw T or *void, since a managed *T carries a generation C cannot readfield '<name>' is a string; the C boundary takes scalars, raw pointers, and nested plain structsfield '<name>' is a generic struct; instantiate it or pass a pointer to itbool is out because an i1 field has no settled one byte storage rule at the boundary, so reach for char or int8 there instead. float32 is out because coercing an eightbyte that packs two float32 fields is not classified, so use float64 or pass the struct by pointer. Past those two, a string, a slice, a tuple, a closure, a collected value, a future, an error, a thread handle, an interface, and an enum are each named outright, and a generic struct asks to be instantiated or passed by pointer.
A variadic foreign function never takes a struct by value at all, at either position, fixed parameter or vararg tail:
foreign function '<name>' is variadic and cannot take a struct by value; a variadic foreign function takes only scalars and raw pointersC reads a vararg positionally with nothing to classify a struct against, which is the same reason the tail admits only scalars and raw pointers.
How a struct is placed
Section titled “How a struct is placed”A C plain struct crosses the way clang’s own classification places it, not as a flat memory copy. The struct is split into eightbyte pieces. A piece holding only floating point data is an SSE register and anything else is an INTEGER register, and each register is coerced to the narrowest type that covers the data inside it, the same coercion clang emits at -O0. A struct of two eightbytes or fewer rides in registers, up to a pair of them. A struct larger than sixteen bytes falls back to memory entirely, passed by a hidden pointer, byval going in and sret coming back.
That placement is byte exact against clang, so the .ll dusk emits for a call or a declare links cleanly against an object a C compiler produced, from either side of the boundary. LDiv above is two INTEGER eightbytes and comes back as a register pair. A struct of three int32 fields packs into an i64 and an i32. A struct of three float64 fields is over the sixteen byte line and comes back through an sret slot instead.
The classification is the System V x86_64 ABI, and x86_64-pc-linux-gnu is the only target triple dusk emits today. A struct by value across another target’s own calling convention is future work, standing on the same single target footing every other codegen decision already stands on.
Passing a struct in reads the same way. This one needs a companion shapes.c beside it defining dist2, compiled in with @csource below, so it is shown here rather than compiled in place:
@csource "shapes.c"
struct Point { x: float64, y: float64 }
foreign "C" { func dist2(p: Point, q: Point) -> float64}
func main() -> int32 { d: float64 = dist2(Point { x: 0.0, y: 0.0 }, Point { x: 3.0, y: 4.0 }) println(d == 25.0) // 1 return 0}Point is two float64 fields, so it classifies to two SSE eightbytes and each argument rides in a pair of SSE registers, exactly where the C definition reads them from.
Callbacks
Section titled “Callbacks”Added in 1.4.2. A foreign function’s parameter may be declared with a function type, which means a bare C function pointer: one LLVM word carrying no environment, the shape a C API’s own callback parameter already expects. This is narrower than a dusk closure, which carries a code pointer and an environment pointer together as a two word { ptr, ptr } value. A callback drops the second word entirely, since C has nowhere to put it.
qsort is the natural one to reach for. Its comparator is int (*)(const void *, const void *), and a capture free lambda literal lowers to exactly that:
@paradigm procedural
foreign "C" { func qsort(base: *raw int64, n: int64, w: int64, cmp: (*raw int64, *raw int64) -> int32) -> void}
func main() -> int32 { buf: *raw int64 = alloc_bytes(40) buf[0] = 5 buf[1] = 2 buf[2] = 8 buf[3] = 1 buf[4] = 9 qsort(buf, 5, 8, lambda (a: *raw int64, b: *raw int64) -> int32 { if a[0] < b[0] { return int32(-1) } if a[0] > b[0] { return int32(1) } return int32(0) }) mut i: int64 = 0 while i < 5 { println(buf[i]) // 1 2 5 8 9 i = i + 1 } free(buf) return 0}That comparator captures nothing, so it is lifted to a fresh top level function with no environment parameter at all, its LLVM signature exactly the callback’s own, and the argument becomes that function’s bare address. No trampoline stands in between.
The bare name of a top level function stands in the same position, and since it already has that signature it crosses as its own address with no forwarding thunk either:
@paradigm procedural
foreign "C" { func qsort(base: *raw int64, n: int64, w: int64, cmp: (*raw int64, *raw int64) -> int32) -> void}
func descending(a: *raw int64, b: *raw int64) -> int32 { if a[0] < b[0] { return int32(1) } if a[0] > b[0] { return int32(-1) } return int32(0)}
func main() -> int32 { buf: *raw int64 = alloc_bytes(32) buf[0] = 5 buf[1] = 2 buf[2] = 8 buf[3] = 1 qsort(buf, 4, 8, descending) mut i: int64 = 0 while i < 4 { println(buf[i]) // 8 5 2 1 i = i + 1 } free(buf) return 0}What may be declared a callback
Section titled “What may be declared a callback”A callback’s own parameters and return must themselves be C legal, the boundary’s ordinary scalar, *raw T, or *void set. A function type nested inside a callback’s own signature is refused outright, since C carries a callback as one function pointer word with no room underneath it for a second:
foreign function '<name>': a C callback's own parameters and return must be scalars or raw pointers; a nested function type cannot crossA foreign function cannot return a function pointer at all. A callback only ever crosses as an argument, the one direction dusk can actually supply a code pointer for:
foreign function '<name>' cannot return a function pointer; a returned C function pointer has no dusk value to call, only an argument callback is supportedA callback parameter cannot be combined with a variadic tail either, since a callback rides the direct call’s exact signature check, a path the variadic call never runs:
foreign function '<name>' is variadic and cannot take a function pointer parameter; a C callback and a varargs tail cannot be combinedWhat may stand in the argument
Section titled “What may stand in the argument”At the call site a callback argument takes exactly one of two forms: a capture free lambda literal, or the bare name of a top level, non generic, non foreign dusk function. Each is checked against the callback’s declared parameter type by strict equality, with no integer width widening and no wildcarding, since C reads two mismatched widths as different registers.
A lambda that captures a local is refused, naming the capture:
this callback captures '<var>'; C has no environment for it, pass state through the *void user data argument the API carriesThat message names the way through. C has no closure environment, so the sanctioned way to thread state is the *void user data argument a C API’s own registration call typically carries alongside its callback, passed through untouched from the call site to every invocation:
@csource "callback_fixture.c"
foreign "C" { func call_n(n: int64, user: *void, fn: (int64, *raw int64) -> int64) -> int64}
func scale(i: int64, user: *raw int64) -> int64 { return i * user[0]}The remaining rejects each name their own cause. A signature mismatch prints both sides. A generic function name is refused before it has one concrete symbol to point a bare code pointer at. Anything else in that position, a closure value, a local, an arbitrary expression, is the wrong shape entirely:
callback argument N: expected <sig>, found <sig>callback argument N cannot be the generic function '<name>'; a C callback needs one concrete function, and a generic function has none until it is instantiatedcallback argument N must be a capture-free lambda literal or the name of a top-level function; a closure value has no bare C function pointerA callback cannot be taken as a value
Section titled “A callback cannot be taken as a value”A foreign function whose signature carries a callback cannot itself be taken as a value. Call it directly:
'<name>' is a foreign function that takes a C callback; call it directly, it cannot be taken as a valueThat is the same restriction a variadic foreign function and a struct passing foreign function already carry, and it holds for the same reason. Taken as a value the call would route through a funcval thunk shaped for an ordinary closure, passing a two word closure where C reads one bare pointer word, dropping the ABI a direct call preserves and silently miscompiling the call.
A callback body is ordinary dusk, checked and compiled like any other function, and every runtime guarantee inside it holds as it does anywhere else: the generational dereference check, bounds checking, a named fault on abort. What changes is the thread. A callback runs on whichever thread the C code holding its pointer calls it on, and a library keeping its own workers may not call it on the thread that made the foreign call at all. Off the anchor thread the same rules govern it as a spawned thread’s body: the generational heap stays thread safe, but the collector does not, so a collector<T> mint or a forced collection reached from a callback running off the anchor thread aborts by name with fatal: the collector runs on the main thread only.
Linking C in
Section titled “Linking C in”Two top of file directives, collected by the prescan alongside @paradigm and @import, reach the clang line a build runs. See source files and modules for where directives sit in a file and how the prescan collects them.
@link <value> names a library or object for the linker. A value with a path separator, or one ending in .a, .o, or .so, is passed to clang verbatim as a file argument. Every other value is a bare library name, turned into a -l flag. So @link "m" becomes -lm and @link "curl" becomes -lcurl, while @link "vendor/libfoo.a" is handed to clang as that path. There is no third form: a value is always read as a library name or a path, never spliced in as an arbitrary flag.
Every dusk binary already links libm, so std.math reaches sin and sqrt with no directive at all. The @link "m" line below is therefore redundant for libm specifically, shown to demonstrate the directive. For a library the runtime does not already pull in, that one line is what resolves the symbol at link time.
@link "m"
foreign "C" { func sqrt(x: float64) -> float64 func pow(base: float64, exp: float64) -> float64}
func main() -> int32 { r: float64 = sqrt(2.0) // a raw float64's text format is not pinned across platforms, so check // against a known value with a bool rather than printing the float itself println(r * r > 1.999 && r * r < 2.001) // 1, the comparison holds println(pow(2.0, 10.0) == 1024.0) // 1 return 0}@csource
Section titled “@csource”@csource "<path>" compiles a C file in beside the runtime. The path resolves against the directory of the file that declares it, and a foreign block in that file, or any file, then binds against whatever the source defines. The example below needs a companion adder.c beside it that defines add, so it is shown here rather than compiled in place:
@csource "adder.c"
foreign "C" { func add(a: int64, b: int64) -> int64}
func main() -> int32 { println(add(2, 3)) // 5 return 0}The link line is a pure function of the collected @csource and @link lists, plus the fixed -pthread -lm and the runtime’s own sources. Both directives fold into one module wide, deduplicated, order preserving list, and first appearance in the loader’s file walk order wins. Nothing a directive names becomes an arbitrary flag: a value is always read as a bare library name or a bare path.
Helpers across the boundary
Section titled “Helpers across the boundary”A few standard library helpers exist for exactly this boundary.
std.string’scbuf(s: string) -> *raw charcopies a string into a fresh NUL terminated raw buffer a foreign call can read. The caller owns the buffer and frees it once the call that reads it has returned. It replaces the old privateto_cbufhelper. To read a NUL terminated buffer C hands back, thecstrbuiltin reinterprets it as astringat no cost; see builtins.std.os’sos_errno() -> int64, namederrnobefore 1.6.0, reads the C library’serrno. Dusk never sets it, so a read reports whatever the most recent foreign call left behind. Read it immediately after the call whose failure it reports, before anything else crosses the boundary and overwrites it. Under the pthreads runtimeerrnois thread local, so a read never races another thread’s foreign call.std.os’serrstr(code: int64) -> stringreturnsstrerror’s message for a code, copied into a fresh heap string the caller owns. Do not pin the exact wording in a test: glibc’s text is not fixed across platforms.
For the scalar float64 functions of libm bound straight across this boundary, see std.math, which every dusk binary can reach since it already links -lm.
The other direction
Section titled “The other direction”Everything on this page carries C into Dusk. A foreign block names a symbol someone else’s object already defines, and dusk calls it; even a callback, which sends a dusk function across, only goes out as an argument to a call dusk itself makes. Carrying Dusk out is the reverse: export "C" marks a dusk function a C caller reaches directly by its own bare symbol, and dusk build --lib compiles a module into a static archive plus a generated C header a host links against. See C libraries for that side of the boundary.