rokdocs
v0.1

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 → i32 or i64, 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 _bitfieldN covering 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 becomes struct Name; (opaque).
  • enums become an alias of their integer type and one const per enumerator (pub type color = u32; pub const RED: color = 0;); an unnamed enum is its constants.
  • object pointers (T*, const T*) become CPtr<T>; void* becomes CPtr<u8>.
  • functions become extern "C" fn (a C variadic: ...); stdcall functions on 32-bit x86 Windows go into extern "system". Globals become static (static mut unless 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 const items.
  • 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 double wider 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:

ItemResponsible party
Pointer null checkrok code, for C → rok
Buffer length validationrok code (must know the length)
String null-terminationAutomatic on CString creation
GC pinrok code (for long-running FFI)
Freeing memory (free)rok code or safe wrapper
Matching calling conventionThe extern declarer
Thread safetyPer the C library's documentation
Errno / GetLastErrorRead immediately after the call

1.7.8. Comparison with other languages

Widening the comparison makes rok's position clearer.

AspectCRustZigGorok
External functions(direct)extern "C" { ... }extern "..."import "C" (cgo)extern "C" { ... }
Calling convention(one default)extern "system" etc.extern "C" etc."C" onlymultiple ("C"/"system"/etc.)
C stringchar*CStr / CString[*:0]u8C.CString / C.GoStringCStr / CString
nullable ptr(default)Option<*const T>?*T*C.struct_T (always nullable)CPtr<T>
binding toolnonebindgen / cbindgen@cImportcgorokc bindgen
GC pinN/AN/AN/Aruntime.Pinner (1.21+)rok::bare::gc::pin
RAII / using(manual)Dropdeferdeferusing block (Drop)

rok's biggest differences come down to three points.

  1. Integration of the GC and FFI. Using the Pinned<T> guard together with using blocks prevents both resource leaks and missed pins.
  2. A strict pointer model. Ptr<T> / ConstPtr<T> / ManyPtr<T> / CPtr<T> are separate (1.1).
  3. Enforced safe wrappers. as casts are rejected and only named casts are allowed (1.2).

Edit this page on GitHub