16. Foreign Pointers¶
Chapter 14 showed you how to call C: declare a function in an
unsafe external block, call it through its namespace, wrap the raw result. But it politely
stepped around a question every real C API forces on you: what happens when C hands you a
pointer? malloc returns one. fopen returns one. Practically every interesting C
library communicates through opaque handles. This chapter is about ptr — Sushi's type for
exactly that — and about the fence the compiler builds around it.
If you come from Python, ptr is roughly what ctypes.c_void_p is: a foreign address you
can carry around but not look inside. Java programmers: this is the MemorySegment of
Java's modern FFI (or the long-typed JNI handle of the old days, done honestly). The
difference is how hard Sushi works to keep it from ever pretending to be a normal value.
A token, not a value¶
A ptr is an opaque token. It is the one type in Sushi that deliberately lives outside
the four guarantees: the borrow checker ignores it, RAII never frees it, it carries no
bounds or null promises, and there is nothing you can do with one in Sushi — no
dereference, no arithmetic, nothing. The only meaningful move is to hand it back to another
external function.
Where does one come from? Only from an external call. There is no null literal, no cast
produces a ptr (0 as ptr is a compile error), and Sushi has no uninitialized variables —
so every ptr in a running program traces back to a C function that returned it. That
single fact is the wall everything else leans on.
Here is the full life of a handle — born in malloc, carried through a safe wrapper's
Result<ptr>, and handed back to free:
# A foreign handle's whole life: born in an external call, carried through
# Result<ptr>, handed back to C. Holding it is safe; only C can act on it.
unsafe external "C" as libc because "borrowing memory from the universe":
fn malloc(i64 n) ptr = "malloc"
fn free(ptr p) ~ = "free"
# Safe wrapper: acquires the handle and folds it into Sushi's Result world.
fn borrow_bytes(i64 n) ptr:
let ptr p = libc.malloc(n)
return Result.Ok(p)
fn give_back(ptr p) ~:
libc.free(p)
println("returned to the universe")
return Result.Ok(~)
fn main() i32:
match borrow_bytes(42 as i64):
Result.Ok(p) -> give_back(p)
Result.Err(_) -> println("the universe declined")
return Result.Ok(0)
Output:
returned to the universe
Note what the wrapper buys you: borrow_bytes is ordinary Sushi, so its callers get the
error channel back (Result.Ok/Result.Err, match, all of it). Maybe<ptr> works the
same way. But be precise about what it does not buy: wrapping a handle in Result adds
error handling, not RAII or null-checking. The free is still your job — guarantee 2
is restored by hand, in give_back, or not at all.
Holding is the safe half
This is the same insight Rust's FFI is built on: holding a raw pointer is harmless —
only creating and using it are dangerous. Sushi gates creation behind unsafe external
and doesn't offer dereferencing at all, so a ptr sitting in a variable, a struct
field, a Result, or a plain ptr[] array threatens nobody.
What a ptr refuses to do¶
Because a handle is a token with no inspectable inside, the compiler rejects every operation that would treat it as a value with behavior:
| You write | The compiler says |
|---|---|
a == b (or <, arithmetic, not, ~) |
CE5010 — no comparable identity, no arithmetic |
p.hash() (or any method) |
CE5011 — an opaque handle has no methods |
HashMap<i32, ptr>, List<ptr>, MyBox<ptr> |
CE5012 — only Result<ptr, E> and Maybe<ptr> carry a ptr |
println("{p}") |
CE2035 — no string form |
0 as ptr, p as i64 |
CE2014 — no forging, no laundering into an integer |
That can feel strict until you ask what the alternative would mean. Two handles comparing "equal" tells you nothing C didn't already know; a hash of an address is garbage the moment C reallocates; and a collection of raw handles is a collection of lies about ownership. If you find yourself wanting any of these, you actually want the next section.
The wrapper struct: giving a handle a personality¶
The idiomatic home for a foreign handle is a struct. The raw ptr rides inside as a
field, and the struct — which is real Sushi and plays by all the rules — is what gets
methods, crosses unit boundaries, and appears in your APIs:
# The wrapper-struct pattern: the raw handle rides inside a named struct,
# and the struct - real Sushi - is what gets methods and crosses units.
unsafe external "C" as libc because "a raw buffer behind a tidy struct":
fn malloc(i64 n) ptr = "malloc"
fn free(ptr p) ~ = "free"
struct Towel:
ptr raw
i64 size
fn issue_towel(i64 n) Towel:
return Result.Ok(Towel(libc.malloc(n), n))
# Extension methods attach to the STRUCT, never to the ptr itself.
extend Towel surrender() i64:
libc.free(self.raw)
return self.size
fn use_towel(Towel t) ~:
let i64 freed = t.surrender()
println("towel surrendered, {freed} bytes returned")
return Result.Ok(~)
fn main() i32:
match issue_towel(42 as i64):
Result.Ok(t) -> use_towel(t)
Result.Err(_) -> println("no towel today")
return Result.Ok(0)
Output:
towel surrendered, 42 bytes returned
A Towel knows things its raw pointer never could — its size, here — and the
surrender() extension method gives the handle's cleanup a name and a place. This is the
Rust newtype idiom, compiler-encouraged: wrap the foreign thing once, then program
against the wrapper forever.
Two fences: public and the unit gate¶
Two compile-time rules keep ptr boxed into the unsafe realm:
A public fn may not expose ptr — not as a parameter, not as a return type, not
tucked inside Result<ptr, E> (CE5008). What a unit exports must be Sushi-shaped:
digested values, or wrapper structs like Towel. Struct fields may carry a ptr across
units — that is the deliberate escape hatch, and it is safe because a ptr is inert
outside its home unit (the libc namespace it came from isn't even visible there).
No danger zone, no ptr — the type name itself may only be spelled in a unit that
declares an unsafe external block (CE5009). A unit without externals could never
produce a handle anyway, so a ptr type written there is dead plumbing, and the compiler
says so. The pleasant side effect: grep your codebase for unsafe external and you have
found every file that can possibly touch a raw foreign pointer.
Why bother, if holding is safe?
The fences don't add memory safety — that's already covered by "can't forge, can't deref". They add legibility. FFI is supposed to be a thin, auditable layer ("FFI is not Sushi"), and these rules make the layer's edges visible in the source instead of in someone's memory of how the code is organized.
What you learned¶
ptris an opaque token for C handles: exempt from borrow checking and RAII, with no dereference, no arithmetic, and nonullliteral anywhere in the language.- A
ptrvalue can only be born from an external call — no cast or literal produces one — so a program withoutunsafe externalblocks cannot have one at runtime. - Holding is safe: variables, private params/returns,
Result<ptr, E>,Maybe<ptr>, struct fields, andptr[]arrays all work. Wrapping inResultrestores the error channel but not RAII — freeing is your job. - Doing is forbidden: no comparisons or arithmetic (
CE5010), no methods (CE5011), no generic containers beyondResult/Maybe(CE5012), no interpolation, no casts. - The wrapper struct is the idiom: put the handle in a field, attach extension methods to the struct, export the struct.
- Two fences keep FFI legible:
public fnsignatures may not exposeptr(CE5008), andptrmay only be named in a unit with anunsafe externalblock (CE5009).
The complete reference — marshalling rules, variadic externs, every diagnostic — is the
FFI guide. And if a future need for null-checking arises, it will arrive as an
is_null(ptr) intrinsic, never as == — the Guide is quite firm on this point.