Calling C from Raven¶
Raven talks to C through an extern "C" block: you declare the C function's
signature, then call it like an ordinary Raven function. The compiler emits a
direct C-ABI call and the linker resolves the symbol (the C runtime is always
on the link line; other libraries arrive with project link flags). This guide
walks through the FFI from the simplest call to structs by value, callbacks,
and variadics.
The runnable examples live under examples/v2/ (ffi_*.rv); each is checked by
the golden suite, so the output shown here is exact.
A first call¶
Declare the C function, then call it:
extern "C" {
fun strlen(s: CStr) -> CSize
fun abs(x: CInt) -> CInt
}
fun main() {
print(strlen(c"hello")) // 5
print(abs(-7)) // 7
}
c"..." is a C string literal: a static, null-terminated const char * with
no allocation. A native Int is accepted where an integer C type (CInt,
CLong, CSize) is expected, so abs(-7) checks without a cast.
The C type set¶
| Raven | C | Notes |
|---|---|---|
CInt |
int |
32-bit |
CLong, CSize |
long, size_t |
64-bit |
CFloat, CDouble |
float, double |
a native Float is accepted; CFloat narrows f64 to f32 |
CStr |
const char * |
from a c"..." literal or to_cstr |
CPtr<T> |
T * |
an opaque, typed pointer |
CFnPtr |
a function pointer | for callbacks |
Passing a runtime String¶
A c"..." literal is fixed at compile time. To pass a String value, convert
it with std/ffi's to_cstr, which copies into a fresh null-terminated
buffer. The buffer lives outside the garbage collector; release it with
free_cstr (or leave it for short-lived programs):
import std/ffi { to_cstr, free_cstr }
extern "C" {
fun puts(s: CStr) -> CInt
}
fun main() {
let msg = "hello, " .concat("world")
let c = to_cstr(msg)
let _ = puts(c)
free_cstr(c)
}
Structs by value¶
A struct marked @repr(C) crosses the C ABI by value, both as an argument and
as a return value. Its fields must be C scalars (or nested @repr(C) structs):
@repr(C)
struct Point {
x: CDouble,
y: CDouble,
}
extern "C" {
fun hypot(x: CDouble, y: CDouble) -> CDouble
fun length(p: Point) -> CDouble // a C function taking Point by value
}
The back end follows each platform's ABI: small structs travel in registers
(integer or SSE), and larger ones in memory or by reference, with a hidden
return pointer where the ABI calls for one. There is no size limit, and a field
may itself be a nested @repr(C) struct, whose bytes are inlined. A struct
literal accepts native Int/Float for its C-scalar fields:
Point { x: 1.0, y: 2.0 }. See examples/v2/ffi_struct_*.rv.
Callbacks¶
A Raven function or closure can be passed where a C CFnPtr is expected, so a
C API can call back into Raven. A top-level function passes directly:
extern "C" {
fun qsort(base: CPtr<CInt>, n: CSize, size: CSize, cmp: CFnPtr)
}
fun compare(a: CPtr<CInt>, b: CPtr<CInt>) -> CInt { ... }
// qsort(buf, 5, 4, compare)
A capturing closure also works, through a generated trampoline. Because the
closure carries an environment, you pass it to the C function's callback slot
and to its userdata slot (a CPtr<Unit>); C threads the closure back to the
trampoline, which invokes it. This is the userdata-last convention (for example
glibc qsort_r):
extern "C" {
fun apply_cb(cb: CFnPtr, data: CPtr<Unit>, x: CLong) -> CLong
}
fun run(base: CLong) {
let add = fun(x: CLong) -> CLong = x + base // captures `base`
print(apply_cb(add, add, 5)) // add -> trampoline; add -> userdata
}
The callback's parameters and return must be C types. A callback that allocates
is safe: the collector traces the suspended Raven stack across the C call. A C
API whose userdata is not the last callback argument (or that has none) needs a
small C shim. See examples/v2/ffi_callback_closure.rv.
Variadic functions¶
An extern signature ending in ... is variadic; a call may pass extra
arguments after the fixed ones:
extern "C" {
fun printf(fmt: CStr, ...) -> CInt
}
fun main() {
let _ = printf(c"%d items, %s\n", 3, c"ok")
}
Each variadic argument must be a C integer or pointer type (or a native Int).
Float variadic arguments are rejected at compile time, the backend cannot
honor the platform's variadic float rules, so a %f format needs a fixed-arity
C shim.
Raw pointers¶
std/ffi wraps manual allocation and unchecked pointer access for buffers a C
API reads or writes. The memory is outside the GC and is yours to free:
import std/ffi { alloc, store, load, free }
fun main() {
let buf: CPtr<CInt> = alloc<CInt>(3)
store(buf, 10)
store(offset(buf, 1), 20)
print(load(buf)) // 10
free(buf)
}
This is unchecked, exactly like C: no bounds checks, no use-after-free
protection. Guard a possibly-null pointer with is_null.
Platform support and linking¶
Raven ships for Linux and Windows x86_64. The C runtime (the CRT, including the
printf family) is always on the link line; symbols from other libraries
arrive through project link flags. See docs/v2/specs/ffi.md and
docs/v2/specs/std-ffi.md for the full ABI rules and out-of-scope notes.