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 {
printf(c"hello, %d\n", 42 as i32); // c"..." is a *u8
let banner: *u8 = c"=== ready ===\n";
printf(banner);
return 0;
}
A c"..." is a *u8 (not the fat-pointer str); forming it is unremarkable, since it is just a pointer to static data. The risky step is dereferencing a raw pointer, and that step is self-flagging: a *p deref is visible in the source on its own. The NUL is appended for you. For an owned, length-carrying string use Text 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 is self-flagging — there is no wrapper block, because each risky form is already visible in the source:
let p: *u8 = malloc(64 as usize);
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 *p deref you write — visible on its own line, with no wrapper hiding it — 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() { p.write_zeroed(); }
is_null() / is_not_null() are plain bit-pattern checks. write_zeroed() carries the validity obligation because it writes through the pointer.
No wrapper block — every UB-capable op is self-flagging
There is no unsafe block and no unsafe fn. Every operation that can cause undefined behaviour is already visible in the syntax, so it needs no enclosing marker: a pointer dereference or index is *p / p[i] (the only meaning of *), making a pointer is x as *T, a pointer-to-integer cast is the loud #addr(p) intrinsic, and a foreign call cannot appear without a preceding extern fn declaration. The act of writing the operation is the marker.
The word null never appears. At an FFI boundary, a null pointer is written explicitly:
let p: *u8 = 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.
#[repr(C)] union: one storage, several typed views
A union is the shape real C headers contain. Same declaration form as a struct (it rides the same item with is_union), C's layout rule (size = largest member, alignment = strictest, size rounded up so an array of unions strides correctly), and field access at offset 0 with the load's type picking the member.
#[repr(C)]
union FloatBits { f: f32, bits: u32 }
var u: FloatBits = FloatBits { f: 1.0f32 }; // a literal names exactly one member
let raw: u32 = u.bits; // reading another reinterprets the bytes
Members must be Copy, the union non-generic and non-empty, and a literal names exactly one member: all E0925. Those are consequences of a union having no tag: nothing can know which member is live, so no destructor could be run correctly. For an either/or value in ordinary code, use a tagged enum.
Layout is verified field-for-field against clang.
Packed structs and bitfields
#[repr(C, packed)] removes the padding between fields (and the struct's own alignment with it). #[repr(C, packed = N)] caps each field's alignment at N (C's #pragma pack(N)).
#[repr(C, packed)]
struct Wire { kind: u8, len: u32 } // size 5, align 1
#[repr(C, packed = 2)]
struct Legacy { a: u8, b: u32 } // b at offset 2
#[bits(N)] gives an integer field a width and packs it beside its neighbours. A field that would cross its declared type's storage unit starts a new one; packing lets it straddle instead. A signed field sign-extends on read. A write is a read-modify-write that preserves the neighbours sharing the unit.
#[repr(C)]
struct Flags {
#[bits(3)] kind: u32,
#[bits(5)] level: u32,
#[bits(4)] delta: i32,
}
var f: Flags = Flags { kind: 5 as u32, level: 21 as u32, delta: -3 as i32 };
f.kind = 2 as u32;
Neither a bitfield nor an under-aligned packed field has an address, so a ref parameter, a ref this receiver, and #addr_of are refused (E0927 / E0926). Packed fields must be Copy. A bitfield needs #[repr(C)], an integer type, a width of 1..=its type's bits, and a non-generic, non-union struct.
cpc-bindgen emits both forms from a C header. A record under #pragma pack(N) is refused with a comment: clang's JSON AST records that a maximum field alignment applies without saying what it is.
Payload-free enums take explicit discriminants and #[repr(u8)] … #[repr(i64)] so they cross the C ABI as the matching integer.
For a concrete cross-language proof, see C ABI consumer. It builds a C+ library, generates a C header, links it from a C program, and exercises scalar, aggregate, enum, raw pointer, and function-pointer ABI classes.
#[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 foreign calls. The direct compiler intrinsics are:
let sel: *u8 = #selector("setTitle:");
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 raw message-send 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. Turning a pointer into an integer is the loud #addr(p) intrinsic, which yields a usize; you narrow from there, never straight from the pointer:
let n: usize = #addr(p);
let i: i32 = n as i32;
let bad: i32 = p as i32; // ❌ E0315 — cannot cast a pointer to i32