Ffi
FFI and Unsafe Boundary
Status: stable reference
This document describes the current foreign-function interface boundary and the role of Unsafe in Concrete.
For the full safety model (capabilities, trusted, Unsafe, proof boundary), see SAFETY. For layout and representation rules, see ABI. For active priorities, see ../ROADMAP.
Core Principles
Concrete keeps the foreign boundary explicit:
- FFI functions are declared with
extern fn - raw pointers are explicit types (
*const T,*mut T) - unsafe operations are gated by
with(Unsafe) - safe code does not silently cross into foreign or pointer-sensitive behavior
Extern Functions
Extern declarations are the language-level entry point to foreign code:
extern fn puts(ptr: *const u8) -> i32;
Current rules:
extern fnhas no Concrete body- extern calls require
Unsafeby default - extern parameter and return types must be FFI-safe
Trusted Extern Functions
A trusted extern fn is an audited foreign binding that callers can use without with(Unsafe):
trusted extern fn sqrt(x: Float64) -> Float64;
This is the right tool for well-known, pure libc functions (math, abs, etc.) where requiring Unsafe at every call site adds noise without safety value.
Rules:
trusted extern fnuses the existingtrustedkeyword — no new syntax- callers do not need
with(Unsafe) - parameter and return types must still be FFI-safe
- the declaration itself is the audit boundary — it asserts the foreign function is safe to call with any valid arguments of the declared types
--report unsafeshows trusted extern declarations in a separate “Trusted extern functions” section
Keep the category narrow. trusted extern fn is for pure, well-understood foreign functions — not a general “safe FFI” escape hatch. If a foreign function has side effects, mutates global state, or can crash on valid inputs, it should remain a regular extern fn under with(Unsafe).
FFI-Safe Types
FFI-safe types currently include:
- integer types
- float types
BoolChar()- raw pointers (
*const T,*mut T) #[repr(C)]structs
The implementation authority for this check is Layout.isFFISafe.
Unsafe Boundary
Concrete currently requires Unsafe for:
- calling
extern fn(but nottrusted extern fn) - dereferencing raw pointers
- assigning through raw pointers
- pointer-involving casts, except reference-to-pointer casts
Safe exception:
&x as *const T&mut x as *mut T
These preserve compiler-known provenance and do not invent an address.
Intentional Design Rule
The goal is not to hide low-level operations behind “safe-feeling” library APIs.
The audit story should stay simple:
grep with(Unsafe)finds the boundary- extern declarations are visible in signatures
- raw pointers stay explicit in types
Concrete is aiming for an unsafe boundary that is:
- operationally obvious
- explicitly gated
- easier to audit than a broad ambient low-level model
Trusted Split
Concrete now has the three-way split described in the research notes:
- capabilities (
with(Alloc),with(File), etc.) = semantic effects visible to callers trusted= containment of internal pointer-level implementation techniques behind a safe APIwith(Unsafe)= authority to cross foreign boundaries (FFI, transmute) — always explicit, even inside trusted code
That means:
trusteddoes not suppress ordinary capabilitiestrusted fn/trusted impldoes not permitextern fncalls withoutwith(Unsafe)trusted extern fnis a separate, narrower mechanism: it marks a specific foreign binding as safe to call, rather than granting blanket trust to a block of code- builtin and stdlib internals are aligned to this same model instead of relying on silent exemptions
Authority Wrapper Patterns
The recommended pattern for containing unsafe or capability-requiring code is: write a small trusted wrapper, expose a safe narrow interface above it, keep raw pointers and FFI details inside the wrapper.
// Bad: callers need with(Unsafe) to use raw FFI
pub fn read_header(fd: i32) with(Unsafe) -> Int {
// raw extern call ...
}
// Good: trusted wrapper contains the unsafety
pub trusted fn read_header(fd: i32) with(File, Unsafe) -> Int {
// raw extern call inside trusted boundary
}
// Callers still need with(File, Unsafe); trusted contains the pointer work, not the capability
The stdlib demonstrates this pattern throughout:
trusted impl Vec<T>wraps pointer arithmetic behind safe push/pop/gettrusted impl HashMap<K, V>wraps raw allocation behind safe insert/get/removetrusted impl TextFilewraps POSIX file I/O behind open/read/write/closetrusted fn print/printlnwraps extern console I/O behindwith(Console)trusted extern fn sqrtmarks a pure foreign function as safe to call withoutwith(Unsafe)
The --report unsafe and --report authority modes make these boundaries visible for audit.
Capability Aliases
Capability aliases reduce signature repetition without hiding authority:
cap IO = File + Console;
cap Host = File + Network + Env + Process;
fn serve() with(Host) -> Int { ... }
fn log(msg: &String) with(IO) { ... }
An alias expands to its constituent capabilities at parse time — the rest of the compiler sees only concrete capability names. Aliases can use the Std macro (cap All = Std; expands to all standard capabilities except Unsafe).
Aliases are validated at definition time: all constituent names must be known capabilities. Unknown names produce a parse error.
Future Refinement
This doc should expand if the FFI surface grows substantially, for example:
- more explicit calling-convention rules
- ABI notes for additional targets
- low-level FFI helper patterns in the stdlib
- continued hardening of builtin and stdlib internals around the implemented
trustedboundary
For the exploratory direction behind those ideas, see ../research/language/unsafe-structure and ../research/language/trusted-boundary.