17Foreign Function Interface
Cryo follows the platform C ABI and uses an LLVM backend, so calling C from Cryo and Cryo from C is straightforward.
17.1 Extern Blocks
Declare C function signatures with Cryo syntax inside an extern "C" block. The compiler trusts the declarations and the linker resolves the symbols.
extern "C" {
function puts(s: string) -> int;
function atoi(s: string) -> int;
}
A standalone extern is also valid:
extern function exit(code: int) -> void;
It is the programmer's responsibility to ensure the Cryo signature matches the C signature; the compiler cannot verify this across the language boundary.
A function declared in an extern block is a top-level declaration of the
module that holds the block, with the top-level visibility default: it is
public - importable as import ffi::libc::{ puts } and nameable as
libc::puts - unless written private function, which keeps it to that
module.
An extern block also declares imported globals — variables whose storage
belongs to the library being linked:
extern "C" {
function gv_bump() -> void;
mut gv_counter: i32; // read and written through the C library's storage
const gv_name: u8*;
}
The type is required and an initializer is rejected. There is nothing to infer a type from, and an initializer would define the symbol here, colliding with the library that already defines it.
mut and const describe what Cryo may do with the binding, not what the
library may. A const imported global is not assumed to hold still: the owning
library may change the value at any time, so the compiler never treats a read of
one as foldable.
Use ![symbol("name")] when the C symbol is not a valid Cryo identifier —
for instance when it collides with a keyword:
extern "C" {
![symbol("type")]
mut type_: i32;
}
An imported global is reached like any other: bare inside its own module, and
qualified (Binding::gv_counter) from outside.
17.2 C Header Import
For larger C libraries, transcribing every signature by hand is error-prone. Cryo can import a C header directly. The compiler drives libclang (Clang's stable C API), which parses the header and generates Cryo declarations for the functions and the structs, unions, enums, and typedefs it finds.
extern module c := "C" {
#include <stdio.h> // angle: system include search path
#include <stdlib.h>
#include "./my_header.h" // quoted: relative to this source file
}
function main() -> int {
c::printf("Value: %d\n", 42);
const buf: void* = c::malloc(256);
c::free(buf);
// Imported C types are reached through the alias too:
const p: c::Point = c::Point { x: 1 as i32, y: 2 as i32 };
return 0;
}
Each #include takes either an angle-bracketed name (<stdio.h>, resolved on the C preprocessor's system include search path) or a quoted path ("./my_header.h", resolved relative to the importing file) - exactly as in C. The identifier after extern module (c here) introduces a namespace into which the imported declarations are placed; access both functions and types with :: (c::printf, c::Point) to prevent collisions between C and Cryo names. Only declarations from the named header(s) are imported - types pulled in transitively from system headers (size_t, int32_t, ...) are resolved to their Cryo primitive directly rather than re-emitted.
Type mapping: a C struct becomes a ![repr(c)] type struct and a named C union a native type union (one field per member, accessed directly); a tag that the header declares but never defines - the opaque-handle idiom, and any tag completed only on another platform - becomes a zero-field opaque record, usable through a pointer exactly as in C; a named enum becomes a type enum with its explicit discriminant values; an anonymous enum (which has no nameable type - its constants are plain integers in C) contributes one alias-namespaced const per constant (enum { LO = 1, HI = 2 } -> c::LO, c::HI); a typedef becomes a type alias; function-pointer parameters/fields map to Cryo (Args) -> Ret. Imported types and constants are namespaced under the alias only (c::Point, c::LO), never the global namespace.
Some C constructs have no first-class Cryo equivalent, so they are translated to layout-faithful opaque storage - a struct of that type round-trips by value over the FFI boundary (correct size and alignment, verifiable with static_assert; see section 17.5) but offers no member access to that part: a bitfield run (adjacent unsigned x : 3 fields share a storage unit) collapses to one blob field; a C11 anonymous struct/union member (union { ... }; with no field name) becomes an aligned blob field named _anon0, _anon1, ...; and a field whose type is an inline anonymous struct/union likewise maps to a blob rather than a pointer. Each such approximation is reported (see below).
Object-like #define constants are imported too, each as an alias-namespaced const with an inferred type: a numeric literal (#define MAX_LEN 256 -> c::MAX_LEN: i32; hex, octal, negative, and floating-point are supported, with integer width inferred from the value and u/l suffixes); a string literal (#define NAME "cryo" -> c::NAME: string, with C escape sequences decoded); and a character literal (#define TAB '\t' -> c::TAB: char carrying the code point).
A body that is not a bare literal is constant-folded by clang and bound to the value it computes. That covers the shape most current C APIs use for flags, where a macro's body is another macro's invocation pinning the constant's width (#define SDL_INIT_VIDEO SDL_UINT32_C(0x20)), as well as ordinary constant arithmetic (#define AREA (W * H)). The width the C expression has is the width bound: SDL_UINT64_C(0x10000000) binds as u64 even though its value would fit u32, because an API that ORs it into a 64-bit field needs it to.
Macros that still can't be bound - function-like macros (#define SQUARE(x) ...), valueless guards (#define HEADER_H), and bodies that are not constant expressions (#define NOW time(0)) - are reported rather than silently dropped.
Macros are taken from any header the import pulled in, not only the file named in the #include line: a library's entry header is typically an umbrella of #includes that defines nothing itself. The compiler's own predefined macros (__STDC__ and friends) live in no header and are excluded.
A header's extern globals bind as imported globals under the alias (extern int errno_value; -> c::errno_value), including the loader idiom of exposing each entry point as a global function pointer. A static global is skipped and reported: it has no symbol outside the translation unit that defines it.
Every construct that is skipped or approximated is recorded and printed as a per-header translation report under --debug ([skip] for an unbound construct, [approx] for a layout-only binding), so nothing is lost silently.
To import only a header's function prototypes - suppressing all struct/enum/typedef emission - apply the ![functions_only] directive to the extern-module block. It is valid only on a C-import extern module:
![functions_only]
extern module c := "C" {
#include "./api.h" // import c::do_thing(...) etc.; skip the header's types
}
An aliased import block holds only #include directives, never Cryo declarations; conversely, a plain extern "C" block (section 17.1) holds only hand-written Cryo signatures, never #include. Don't also hand-declare a symbol that an imported header already defines (e.g. puts from <stdio.h>) - reach it through the alias (c::puts) instead.
17.3 Calling Cryo from C
Cryo emits each declaration under its mangled symbol name. To make a function callable from C, apply ![no_mangle] to its definition: it then ships under its bare declared identifier, which your C header declares directly.
function cryo_add(a: i32, b: i32) -> i32 { return a + b; } ![no_mangle]
// emits `cryo_add` (not the mangled `C$…` symbol); C sees `int cryo_add(int, int);`
Use ![symbol("name")] instead when the exported name must differ from the Cryo identifier. The older approach - a thin wrapper inside an extern "C" block that forwards to the mangled function, declared in C under the wrapper's mangled symbol - still works and is the only option when you cannot annotate the definition itself (e.g. exporting a stdlib function you don't own).
17.4 Function-pointer callbacks
Many C APIs take a function pointer (qsort, event loops, AST visitors). Declare the parameter with Cryo's function-pointer type (Args) -> Ret and pass a named Cryo function by its bare name - taking the address is implicit, no &:
extern "C" {
function qsort(base: void*, nmemb: u64, size: u64,
compar: (const void*, const void*) -> i32) -> void;
}
function cmp_i32(a: const void*, b: const void*) -> i32 {
const pa: i32* = a as i32*;
const pb: i32* = b as i32*;
return *pa - *pb;
}
// qsort(&arr[0] as void*, n, 4, cmp_i32); // sorts in place via the callback
The callback must be a bare function pointer - a named function or a non-capturing lambda. A capturing closure is not C-compatible: a C function pointer has no environment slot. Thread per-call state through an explicit void* client-data parameter instead (the standard C idiom, e.g. libclang's clang_visitChildren(cursor, visitor, client_data)):
extern "C" {
function with_each(items: void*, n: u64,
visit: (void*, void*) -> i32, client: void*) -> void;
}
Where a C API documents an optional callback, a typed null is a valid value: pass null and guard the call site with f == null.
17.5 Compile-time layout assertions (static_assert)
When binding a C library, a Cryo struct must match the C struct's layout exactly. static_assert checks a constant condition at compile time and fails the build (E0237) if it is false - so a binding can verify its own layout against the numbers the C side guarantees, rather than miscompiling silently.
type struct Color { r: u8; g: u8; b: u8; a: u8; }
static_assert(sizeof(Color) == 4);
static_assert(alignof(Color) == 1);
static_assert(sizeof(Color) == 4, "Color must be 4 bytes to match the C ABI");
static_assert is a module-scope declaration: static_assert(cond) or static_assert(cond, "message"). The condition is folded after layouts are computed and may use anything that is a compile-time integer in an array size (section 2.4) - integer/boolean literals, named constants, enum variants, integer casts, and the arithmetic, comparison, logical, and bitwise operators - plus sizeof(T) and alignof(T), which only this position can see. A condition that is false - or that is not a compile-time constant - is a compile error. (It is a general feature, not FFI-only, but layout verification is its primary use.)
const CAPACITY: i64 = 512;
static_assert(CAPACITY == 512, "the wire format fixes the capacity");
static_assert(sizeof(Grid) == CAPACITY);
extern function exit(code: i32) -> void;
extern "C" {
function puts(s: string) -> i32;
}Declares a function whose body the linker supplies - typically a C library symbol. The compiler trusts the Cryo signature; keeping it in step with the real C signature is on you.
language referencefunction name<T>(param: Type) -> Ret { ... }Declares a function. Parameters are written name: Type and are never inferred; the return type follows -> and defaults to void. Declaration order does not matter: every signature is collected before any body is checked, so functions may call each other freely and recurse.
function puts(s: string) -> int(parameter) s: stringprimitive stringNUL-terminated raw string (u8*), matching the C ABI. Length-typed text is Str / String.
function atoi(s: string) -> intextern function exit(code: int) -> void(parameter) code: intfunction gv_bump() -> voidmut name: Type = value;A mutable binding, reassignable after declaration - mutability is opt-in, const is the default. At module level it declares mutable global state. Before & it marks an exclusive reference or receiver instead.
mut gv_counter: i32const NAME: Type = value;At module level, a true compile-time constant, conventionally SCREAMING_SNAKE_CASE. (mut at module level declares mutable global state instead.)
const gv_name: u8*mut type_: i32public module collections::array;In a directory's _module.cryo aggregator, lists a submodule and whether it is public. Importing the directory's namespace sees only the modules declared public here.
const buf: void*const buf: void*const p: c::Pointvalue as TypeExplicit conversion: between numeric types, or reinterpreting one pointer type as another. Cryo never converts implicitly, and as inserts no range checks - a narrowing cast is the programmer's responsibility.
return value;Leaves the current function with the given value. A function whose return type is not void must return one; a bare return; is for void. In an async function it completes the future with the value.
function cryo_add(a: i32, b: i32) -> i32(parameter) a: i32(parameter) b: i32(parameter) a: i32(parameter) b: i32function qsort(base: void*, nmemb: u64, size: u64, compar: (const void*, const void*) -> i32) -> void(parameter) base: void*(parameter) nmemb: u64(parameter) size: u64(parameter) compar: (const void*, const void*) -> i32const name: Type = value;An immutable binding: it cannot be reassigned after initialisation. The annotation may be omitted when an initialiser is present - the binding takes the initialiser's concrete type, inferred locally. Mutability is opt-in through mut.
function cmp_i32(a: const void*, b: const void*) -> i32(parameter) a: ?(parameter) void(parameter) b: ?const pa: i32*(parameter) a: ?const pb: i32*(parameter) b: ?const pa: i32*const pb: i32*function with_each(items: void*, n: u64, visit: (void*, void*) -> i32, client: void*) -> void(parameter) items: void*(parameter) n: u64(parameter) visit: (void*, void*) -> i32(parameter) client: void*type struct Name { ... }
type enum Name { ... }
type trait Name { ... }
type union Name { ... }Begins a type declaration; the keyword that follows says which kind. On its own, type Name = Existing; declares an alias.
type struct Name {
field: Type;
method(&this) -> Ret { ... }
}A value type with named fields and optional methods. Structs live on the stack and are passed by value. Fields are public by default and may carry = default values; methods take their receiver as &this, mut &this, or this.
type struct ColorColor.r: u8Color.g: u8Color.b: u8Color.a: u8static_assert(sizeof(T) == 4, "message");A module-scope, compile-time assertion. The condition is folded after layouts are computed - literals, sizeof, alignof, and the arithmetic, comparison, logical, and bitwise operators - and a false or non-constant condition fails the build (E0237). Its main use is checking that a struct matches a C layout.
sizeof(T)The size of a type in bytes, as a compile-time constant - usable in static_assert. alignof(T) gives the alignment.
type struct Coloralignof(T)The alignment of a type in bytes, as a compile-time constant - usable in static_assert. sizeof(T) gives the size.
const CAPACITY: i64const CAPACITY: i64import a::b;
import a::b::{ X, Y };
import a::b as c;
import a::b::*;Brings a module, or selected items from it, into scope. One path per declaration: use the brace form for several items, as to alias, and * for everything public (prefer the brace form; wildcards invite name collisions).
namespace ffi;namespace ffi::libc;extern "C" function puts(s: u8*) -> i32private type struct Name { ... }
private:On a field: only the declaring type's own methods may touch it (E0353). On a top-level item: confined to its own module, and naming a private type elsewhere is E0503. private: opens a block of such members.
type union Name {
i: i64;
f: f64;
}An untagged, C-style union: every field shares the same storage at offset 0, so the size is that of the largest member. Writing one field and reading another reinterprets the bytes; there is no runtime tag.
language referencetype enum Name {
Unit,
Payload(T),
Named { x: i32 },
}An algebraic data type. Variants may be bare or carry a tuple or struct payload, and a match on an enum must cover every variant. Name::Variant names a variant; methods are added with implement enum Name { ... }.
type Name = Existing;A type alias: a new, transparent name for an existing type - the two are interchangeable everywhere. Aliases may be generic: type StringResult<T> = Result<T, string>;.
type alias// reservedReserved for future use: the lexer recognises it, but no current syntax gives it meaning.
language referencefunction name(count: i32, args...)A variadic parameter: the trailing bucket receives any number of further arguments with C's calling convention, so the function stays ABI-compatible with the printf family. Wrap the bucket in VaArgs to read typed values from it.
extern "C" function time(t: i64*) -> i64static name(param: Type) -> Ret { ... }A method that belongs to the type rather than an instance, called as Type::name(..). For structs, static new(..) returning a struct literal is the idiomatic constructor.
function puts(s: string) -> intfunction qsort(base: void*, nmemb: u64, size: u64, compar: (const void*, const void*) -> i32) -> voidnullThe null pointer literal, valid in any pointer context - including the primitive string, which is a NUL-terminated u8*. Compare with == null; it is not an Option.