C+
Systems · v0.0.13

FFI: calling C

C+ emits standard object files. The system linker stitches them with anything clang would. The language-level interop primitive is extern fn.

Declaring symbols

extern fn malloc(n: usize) -> *u8;
extern fn free(p: *u8);
extern fn printf(fmt: *u8, ...) -> i32;   // varargs OK on extern only

C-string literals: c"..."

C wants a NUL-terminated char*. A c"..." literal is exactly that: a bare *u8 pointing at a NUL-terminated .rodata blob.

extern fn printf(fmt: *u8, ...) -> i32;

fn main() -> i32 {
    unsafe { printf(c"hello, %d\n", 42 as i32); }   // c"..." is a *u8
    let banner: *u8 = c"=== ready ===\n";
    unsafe { printf(banner); }
    return 0;
}

A c"..." is a *u8 (not the fat-pointer str) and is safe to form, since it is just a pointer to static data; only dereferencing a raw pointer needs unsafe. The NUL is appended for you. For an owned, length-carrying string use string or str; c"..." is specifically the C-interop shape.

Raw pointers

*T is an 8-byte opaque address. It is Copy. Every operation on it requires unsafe:

let p: *u8 = unsafe { malloc(64 as usize) };
unsafe {
    p[0] = 65 as u8;                  // store
    let b: u8 = p[1];                 // load
    let q: *u8 = p + 1;               // pointer arithmetic (strides by sizeof(T))
    free(p);
}

Raw pointers are outside the borrow checker, by design. The compiler tracks nothing about a *T's lifetime: you can return one, store it in a global, or alias it freely. That is the escape hatch that makes FFI possible, and the flip side is that the validity obligation is entirely yours. A pointer into a value that has since dropped is a use-after-free the language will not catch. The unsafe you write at each dereference is exactly where you acknowledge taking on that obligation (see Ownership, "what the compiler checks, and what it trusts").

Raw pointers also have a few blessed helper methods:

if p.is_null() { return 1; }
if p.is_not_null() { unsafe { p.write_zeroed(); } }

is_null() / is_not_null() are safe bit-pattern checks. write_zeroed() is unsafe because it writes through the pointer.

unsafe { ... }

Required for: pointer dereference, pointer indexing, extern fn calls, str_from_raw_parts, and integer-to-pointer casts.

The word null never appears. At an FFI boundary, a null pointer is written explicitly:

let p: *u8 = unsafe { 0 as *u8 };

#[repr(C)]: stable C layout

#[repr(C)]
struct NSRect {
    origin: NSPoint,
    size: NSSize,
}

Promises field order is preserved and that padding and alignment match the platform C ABI. Always use it on structs that cross an extern fn boundary by value.

#[link_name = "..."]: multiple signatures, one symbol

When one C symbol has several typed shapes (the Objective-C objc_msgSend pattern):

#[link_name = "objc_msgSend"] extern fn msg_void(recv: *u8, sel: *u8);
#[link_name = "objc_msgSend"] extern fn msg_get_str(recv: *u8, sel: *u8) -> *u8;

Both resolve to _objc_msgSend at link time.

Objective-C interop

Objective-C is the one non-C-shaped ABI that C+ treats as a first-class systems target, because AppKit, Foundation, Metal, and MPS all sit behind it on macOS. Object handles are opaque *u8, selectors are data, and message sends are unsafe calls. The direct compiler intrinsics are:

let sel: *u8 = #selector("setTitle:");
unsafe {
    let title: *u8 = #msg_send(button, "title") -> *u8;
    #msg_send(button, "setEnabled:", true);
}

#selector("name") registers and caches the SEL. #msg_send(recv, "sel", ...) -> T emits a typed objc_msgSend call with the return type you spell at the call site. Most application code should import the typed packages instead: vendor/appkit wraps Cocoa and vendor/metal wraps Metal/MPS, keeping the unsafe details at the edge.

Two ABI gotchas worth memorising

Variadic functions must be declared variadic. If the C header says int fcntl(int fd, int cmd, ...); the C+ extern must be variadic too. On AArch64-darwin, named args go in registers but varargs go on the stack, so a fixed-arity declaration silently passes garbage:

extern fn fcntl(fd: i32, cmd: i32, ...) -> i32;       // ✅
extern fn fcntl(fd: i32, cmd: i32, arg: i32) -> i32;  // ❌ no-ops, returns 0

Pointer/integer casts go through usize:

let n: usize = unsafe { p as usize };
let i: i32   = n as i32;
let bad: i32 = unsafe { p as i32 };   // ❌ E0315 — cannot cast pointer to i32