1.7. FFI - extern "C" / CStr / CString / CPtr
The rok::bare::ffi module provides the mechanism for defining external functions and data compatible with the C ABI. It works together with the extern "C" { ... } block defined by refs-lang 5.9 extern block.
This chapter covers only traditional C FFI; FFI based on the WASM Component Model is described separately in 1.8 wit.
1.7.1. extern "C" block
The most basic form is:
extern "C" {
// function declarations
fn malloc(size: usize) -> CPtr<u8>;
fn free(ptr: CPtr<u8>);
fn strlen(s: ConstPtr<c_char>) -> usize;
// global (read-only)
static errno: i32;
// global (mutable) - the `mut` keyword is allowed _only here_
static mut atexit_count: u64;
}
The mut keyword appearing here is defined in refs-lang 1.5 keywords and has meaning only in this position. Note that it cannot be used on ordinary variables.
#[link("name")] - specifying a library
To specify a library explicitly, use the #[link] attribute.
#[link("c")]
extern "C" {
fn malloc(size: usize) -> CPtr<u8>;
fn free(ptr: CPtr<u8>);
}
#[link("m")]
extern "C" {
fn cos(x: f64) -> f64;
fn sin(x: f64) -> f64;
}
The linker attaches flags such as -lc or -lm automatically.
Calling convention
Various calling conventions can be chosen depending on the platform and interface.
extern "C" { ... } // System V (Unix) / Microsoft x64 (Windows)
extern "system" { ... } // Windows: stdcall on x86, C on x64 / aarch64
extern "fastcall" { ... } // x86 fastcall
extern "stdcall" { ... } // x86 stdcall
extern "aapcs" { ... } // ARM AAPCS
extern "thiscall" { ... } // C++ this pointer (x86 only)
The default "C" is enough for most users. The other conventions appear mainly when dealing with the Windows API or C++ interfaces.
str parameters and returns
A str parameter of an extern "C" function is passed as a NUL-terminated char*, and a returned char* becomes a str whose length is measured with strlen (refs-lang 5.9.4). (ptr, len) passing belongs to the runtime ABI extern "rok" (5.9.2).
extern "C" {
fn puts(s: str) -> i32; // "hi" → NUL-terminated char*
fn getenv(name: str) -> CPtr<c_char>; // may be null - received as CPtr
}
1.7.2. CStr / CString - null-terminated string
CStr is the borrowed type for handling C strings.
struct CStr {
// internal: null-terminated UTF-8 or raw bytes (bytes on Unix, UTF-16 separately on Windows)
}
impl CStr {
fn from_ptr(p: ConstPtr<c_char>) -> CStr | none;
// null pointer → none
// no null terminator - undefined (caller's responsibility)
fn from_bytes_with_nul(bytes: Slice<u8>) throws (FromBytesWithNulError) -> CStr;
// checked - last byte is 0, no interior 0
fn from_bytes_with_nul_unchecked(bytes: Slice<u8>) -> CStr;
fn as_ptr(self) -> ConstPtr<c_char>;
fn as_bytes(self) -> Slice<u8>; // excluding the null terminator
fn as_bytes_with_nul(self) -> Slice<u8>; // including the null terminator
fn to_str(self) throws (Utf8Error) -> str; // UTF-8 conversion
fn len(self) -> usize; // excluding the null terminator
}
from_ptr copies the C string in. CStr / CString keep bytes and NUL in one GC object; as_ptr is valid as str::as_ptr is (2.15.2): while a variable holding the CStr / CString is in scope in a function where a pointer value can appear.
CString - owned, null-terminated
When an owned form is needed, use CString.
struct CString { /* GC heap, null-terminated */ }
impl CString {
fn new(bytes: Slice<u8>) throws (NulError) -> CString;
// rejects interior null bytes
fn from_str(s: str) throws (NulError) -> CString;
fn as_c_str(self) -> CStr;
fn as_ptr(self) -> ConstPtr<c_char>;
fn into_bytes(self) -> List<u8>; // owned bytes (excluding null)
fn into_bytes_with_nul(self) -> List<u8>;
}
Example - strlen
extern "C" {
fn strlen(s: ConstPtr<c_char>) -> usize;
}
let s: str = "hello";
let cs = try CString::from_str(s);
let n = unsafe { strlen(cs.as_ptr()) }; //→ 5
Note
Conversion between str and CStr requires an allocation (a null terminator must be added). str is generally not null-terminated, so converting explicitly only at FFI sites is recommended.
1.7.3. CPtr<T> and nullability
The authoritative definition is in 1.1.5 CPtr. When a C function returns T*, the possibility of null must be made explicit, and this is the type used there.
extern "C" {
fn fopen(path: ConstPtr<c_char>, mode: ConstPtr<c_char>) -> CPtr<FILE>;
fn fclose(file: CPtr<FILE>) -> i32;
}
struct FILE; // opaque
let path = try CString::from_str("data.txt");
let mode = try CString::from_str("r");
let f: CPtr<FILE> = unsafe { fopen(path.as_ptr(), mode.as_ptr()) };
match f.as_ptr() {
file: Ptr<FILE> => {
// use
unsafe { fclose(f) };
}
none => panic("fopen failed"),
}
1.7.4. using block - RAII
The GC does not guarantee finalizers. This is a principle every GC language learns: RAII for external resources must be handled with explicit using.
using f = try rok::bare::ffi::fopen(path, "r") {
let buf = try f.read_to_end();
process(buf);
} // ← automatic fclose at end of scope (Drop)
// without using - resources may leak
let f = try rok::bare::ffi::fopen(path, "r");
try process(f); // fclose skipped when an error propagates
rok::bare::ffi::fclose(f); // guaranteed only if _explicit_
The using syntax itself is governed by refs-lang 3.17 using expression and works integrated with the Drop trait of 2.13 marker.
1.7.5. Safe wrapper pattern - separating sys and safe
The standard idiom in the Rust ecosystem is to separate a *-sys crate (raw FFI) from a same-named crate (safe wrapper). rok recommends the same pattern.
// libc-sys module - raw FFI
mod c {
#[link("c")]
extern "C" {
pub fn malloc(size: usize) -> CPtr<u8>;
pub fn free(ptr: CPtr<u8>);
pub fn strdup(s: ConstPtr<c_char>) -> CPtr<c_char>;
}
}
// libc - safe wrapper
pub fn alloc_buffer(size: usize) throws (AllocError) -> List<u8> {
let p = unsafe { c::malloc(size) };
match p.as_ptr() {
ptr: Ptr<u8> => {
// unsafe → safe boundary
let bytes = unsafe { Slice::from_raw_parts(ptr, size).to_list() };
unsafe { c::free(p) };
bytes
}
none => { throw AllocError::OutOfMemory; }
}
}
This separation confines unsafe code to a narrow area. User code then calls only the safe wrapper, which secures safety.
1.7.6. Automatic binding generation
rokc bindgen (refs-lang 9.1) generates rok declarations from a C header through libclang, which the toolchain ships in its lib/ and which only this command loads.
rokc bindgen libfoo.h > libfoo_sys.rok
rokc bindgen libfoo.h --target x86_64-pc-windows-msvc -o libfoo_sys.rok -- -I include -DFOO=1
The output is for one target (--target, default the host): sizes, layouts and integer widths are clang's for it. Arguments after -- are passed to clang. The machine's system headers are used for its own platform, for Apple targets (the SDK) and for MSVC targets on an MSVC host; for any other target clang gets only its built-in headers unless --sysroot is given, and the target's headers are passed after -- (-isystem <dir>). The declarations of the header and of the non-system headers it includes are written out (for a header in a system directory, also those of the headers it includes from its own directory tree), together with the declarations of other headers that they use. A header clang rejects is an error (exit 1) with clang's diagnostics.
The mapping conventions are:
- C integer and floating-point types become the rok primitive of their size on the target (
int→i32,long→i32ori64,char→c_char,_Bool→bool);size_t/uintptr_t→usize,ssize_t/intptr_t/ptrdiff_t→isize. A system header's typedef of such a type is that type. - typedefs become rok type aliases.
- structs and unions map to
#[repr(C)] struct/union(#[repr(C, align(N))]or#[repr(packed)]where clang's layout needs it). A run of bitfields becomes one byte-array field_bitfieldNcovering its storage. A layout rok cannot express (#pragma pack(N)with N > 1, a bitfield in a union) becomes a struct of the same size and alignment holding_opaque: [u8; N]. A struct declared but not defined becomesstruct Name;(opaque). - enums become an alias of their integer type and one
constper enumerator (pub type color = u32; pub const RED: color = 0;); an unnamed enum is its constants. - object pointers (
T*,const T*) becomeCPtr<T>;void*becomesCPtr<u8>. - functions become
extern "C" fn(a C variadic:...); stdcall functions on 32-bit x86 Windows go intoextern "system". Globals becomestatic(static mutunless const). Static and inline functions are skipped (no symbol to link). - function pointers become
unsafe extern "C" fn(...) -> R | none(refs-lang 5.9.4; NULL =none). - object-like macros that evaluate to an integer, floating-point or string constant become
constitems. - a C name that is a rok keyword or primitive type name gets a trailing
_; functions and statics keep their C symbol with#[link_name]. - anything else (128-bit integers,
_Float16,long doublewider than 8 bytes, vector and block types, variadic function pointers, thread-local variables, other calling conventions) is skipped with a// skipped ...comment. A struct written as bytes (_opaque) is never passed by value: a function or function pointer that takes or returns one, directly or inside another struct, is skipped, since C classifies it by its fields.
1.7.7. Division of call-safety responsibility
Who is responsible for what at the call site:
| Item | Responsible party |
|---|---|
| Pointer null check | rok code, for C → rok |
| Buffer length validation | rok code (must know the length) |
| String null-termination | Automatic on CString creation |
| GC pin | rok code (for long-running FFI) |
Freeing memory (free) | rok code or safe wrapper |
| Matching calling convention | The extern declarer |
| Thread safety | Per the C library's documentation |
| Errno / GetLastError | Read immediately after the call |
1.7.8. Comparison with other languages
Widening the comparison makes rok's position clearer.
| Aspect | C | Rust | Zig | Go | rok |
|---|---|---|---|---|---|
| External functions | (direct) | extern "C" { ... } | extern "..." | import "C" (cgo) | extern "C" { ... } |
| Calling convention | (one default) | extern "system" etc. | extern "C" etc. | "C" only | multiple ("C"/"system"/etc.) |
| C string | char* | CStr / CString | [*:0]u8 | C.CString / C.GoString | CStr / CString |
| nullable ptr | (default) | Option<*const T> | ?*T | *C.struct_T (always nullable) | CPtr<T> |
| binding tool | none | bindgen / cbindgen | @cImport | cgo | rokc bindgen |
| GC pin | N/A | N/A | N/A | runtime.Pinner (1.21+) | rok::bare::gc::pin |
| RAII / using | (manual) | Drop | defer | defer | using block (Drop) |
rok's biggest differences come down to three points.
- Integration of the GC and FFI. Using the
Pinned<T>guard together withusingblocks prevents both resource leaks and missed pins. - A strict pointer model.
Ptr<T>/ConstPtr<T>/ManyPtr<T>/CPtr<T>are separate (1.1). - Enforced safe wrappers.
ascasts are rejected and only named casts are allowed (1.2).