14. The Standard Library, FFI & Libraries¶
You have a language. Now you need batteries. This chapter is a guided tour of the parts of Sushi that connect your programs to the wider universe: the standard library of ready-made modules, variadic functions for flexible argument lists, the foreign function interface for calling C, and the library system for sharing your own code across projects.
If you come from Python, think of this as import time, import math, ctypes, and
pip-installable packages — except everything here is compiled, statically typed, and
checked ahead of time. Java programmers will recognise the standard packages, JNI, and
JARs. We will meet a small piece of each, and every example below is a complete program
that really compiles and runs.
A tour of the standard library¶
Standard-library modules are pulled in with use <name> (angle brackets), distinct from
importing your own source files (which uses quotes — more on that later). Each module is a
precompiled unit that the compiler links into your binary.
Time¶
The <time> module gives you POSIX-precision sleep functions. They all return
Result<i32> (0 on success, or the remaining microseconds if a signal interrupts the
sleep), so you unwrap them like any other Result. We keep the duration tiny here so the
program returns almost instantly.
use <time>
fn main() i32:
println("Improbability drive warming up...")
# Sleep functions return Result<i32>; .realise(default) unwraps safely.
let i32 r = msleep(5 as i64).realise(-1)
if (r == 0):
println("Drive online. Anything is now infinitely probable.")
else:
println("Interrupted by a passing whale.")
return Result.Ok(0)
Output:
Improbability drive warming up...
Drive online. Anything is now infinitely probable.
No ?? in main
We unwrap with .realise(default) rather than ??. The ?? operator is wonderful
inside ordinary functions, but using it in main triggers a CW2511 warning — and a
warning means a non-zero compile exit, which we treat as failure. In main, prefer
match, if (result), or .realise(default).
Math¶
The <math> module wraps LLVM's numeric intrinsics. Alongside the type-suffixed forms
(abs_i32, min_f64, …) there are polymorphic helpers — abs, min, max, sqrt,
hypot — that pick the right instruction for whatever numeric type you hand them.
use <math>
fn main() i32:
# Polymorphic helpers work across numeric types.
let i32 biggest = max(7, 42)
let i32 magnitude = abs(-42)
# Floating-point intrinsics.
let f64 root = sqrt(1764.0)
let f64 hyp = hypot(3.0, 4.0)
println("max(7, 42) = {biggest}")
println("abs(-42) = {magnitude}")
println("sqrt(1764) = {root}")
println("hypot(3, 4) = {hyp}")
return Result.Ok(0)
Output:
max(7, 42) = 42
abs(-42) = 42
sqrt(1764) = 42
hypot(3, 4) = 5
Random¶
The <random> module offers a non-cryptographic pseudo-random generator. Seeding it with
srand makes a run reproducible, which is exactly what you want in a tutorial whose output
must match every time.
use <random>
fn main() i32:
# Seed the generator so the sequence is reproducible across runs.
srand(42 as u64)
println("Rolling three dice for the Total Perspective Vortex...")
foreach(n in 1..=3):
let i32 roll = rand_range(1, 7)
println("roll {n}: d6 -> {roll}")
return Result.Ok(0)
Output:
Rolling three dice for the Total Perspective Vortex...
roll 1: d6 -> 1
roll 2: d6 -> 2
roll 3: d6 -> 5
Reproducible, not secret
The same seed produces the same sequence on the same platform — great for tests and
procedural generation, but <random> is explicitly not cryptographically secure.
Do not use it for anything a Vogon might try to break.
Files¶
The <io/files> module opens files with open(path, mode), which returns a
FileResult<file>. Instead of the usual Result, file operations use a dedicated
FileResult/FileError pair so you can match on specific failures like
FileError.NotFound() or FileError.PermissionDenied(). Here we write a file under /tmp
and read it straight back.
use <io/files>
fn main() i32:
# Write a line to a file in /tmp, then read it back.
match open("/tmp/guide_entry.txt", FileMode.Write()):
FileResult.Ok(f) ->
f.write("Earth: Mostly Harmless")
f.close()
println("Entry filed.")
FileResult.Err(FileError.PermissionDenied()) ->
println("Permission denied")
return Result.Err(StdError.Error)
FileResult.Err(_) ->
println("Failed to write entry")
return Result.Err(StdError.Error)
match open("/tmp/guide_entry.txt", FileMode.Read()):
FileResult.Ok(f) ->
let string content = f.read()
f.close()
println("Entry reads: {content}")
FileResult.Err(FileError.NotFound()) ->
println("Entry not found")
return Result.Err(StdError.Error)
FileResult.Err(_) ->
println("Failed to read entry")
return Result.Err(StdError.Error)
return Result.Ok(0)
Output:
Entry filed.
Entry reads: Earth: Mostly Harmless
Other modules you will reach for include <env> for environment variables, <io/stdio>
for stream access, and <collections/strings> for UTF-8-aware string utilities. The
Standard Library reference
lists them all.
Variadic functions¶
Sometimes you don't know in advance how many arguments a function will get. Sushi has a
native, safe variadic mechanism: a trailing parameter written ...T name collects all
the trailing call arguments into an owned dynamic array T[], which the callee iterates
with .iter(), foreach, and .len() — and which is RAII-destroyed at scope exit. The
marker ... is a prefix on the element type, and the variadic parameter must be last.
# A trailing ...i32 collects the trailing call arguments into an owned i32[].
fn sum(...i32 nums) i32:
let i32 total = 0
foreach(n in nums.iter()):
total := total + n
return Result.Ok(total)
fn main() i32:
let i32 a = sum(1, 2, 3, 4).realise(0)
let i32 b = sum(40, 2).realise(0)
# Zero trailing arguments is valid: the callee receives an empty array.
let i32 c = sum().realise(0)
println("sum(1, 2, 3, 4) = {a}")
println("sum(40, 2) = {b}")
println("sum() = {c}")
return Result.Ok(0)
Output:
sum(1, 2, 3, 4) = 10
sum(40, 2) = 42
sum() = 0
Notice the last call, sum(): zero trailing arguments is valid, and the callee simply
receives an empty array. A variadic parameter can also follow fixed parameters:
# A fixed prefix parameter followed by a trailing variadic.
fn log_all(string prefix, ...i32 values) ~:
println("{prefix} ({values.len()} values):")
foreach(v in values.iter()):
println(" {v}")
return Result.Ok(~)
fn main() i32:
log_all("readings", 10, 20, 30)
log_all("empty")
return Result.Ok(0)
Output:
readings (3 values):
10
20
30
empty (0 values):
Two kinds of variadic, kept apart
The ...T form above is the safe, native one: homogeneous element type, owned array,
full RAII. There is a second, unsafe form — a bare ... — that exists only inside an
unsafe external "C" block for binding C varargs like printf. We meet it next. The
two are deliberately separated so C's untyped varargs never leak into safe Sushi code.
Calling C (FFI)¶
When the standard library doesn't have what you need, you can reach down into C. The foreign function interface lets you declare an external C function and call it. This is the escape hatch toward self-hosting, and Sushi makes you acknowledge that you are stepping outside its guarantees.
You declare externals inside an unsafe external "C" as <namespace> block. The
because "<reason>" clause documents why the unsafety is acceptable and silences the
CW5001 four-guarantees warning so the build stays clean. Each declaration is bodyless, and
= "symbol" names the actual C link symbol.
# The danger zone. `because "..."` acknowledges the four suspended guarantees
# and keeps the build clean. The Sushi name and the C symbol are joined by `= "..."`.
unsafe external "C" as libc because "string length via libc strlen":
fn strlen(string s) i64 = "strlen"
# Safe wrapper - ordinary Sushi. The raw i64 from the boundary is folded back
# into a Result. The `string` argument is marshalled to a C char* and freed at
# scope exit, so there is no leak.
fn length(string s) i64:
return Result.Ok(libc.strlen(s))
fn main() i32:
let i64 n = length("Mostly Harmless").realise(0 as i64)
println("len = {n}")
return Result.Ok(0)
Output:
len = 15
Two things are doing quiet work here. First, the string argument is automatically
marshalled to a C char* for the call and the copy is freed at scope exit — no leak.
Second, and crucially: externals return raw C values, not Result. That is the single
exception to Sushi's implicit-Result rule. So libc.strlen(s) yields a bare i64, and
we wrap it ourselves in the length safe wrapper. Trying to use ?? directly on a raw
external would be a CE2507 error.
Wall off the foreign world
The guiding rule is "FFI is not Sushi." Keep the unsafe external block thin, and
immediately wrap each foreign call in an ordinary Sushi function that folds the raw
value back into a Result. After that wrapper, all four guarantees — borrow checking,
RAII, Result/Maybe, and bounds/null safety — are back in force for callers.
The unsafe block is also the only place a bare ... variadic is allowed, which is how
you bind C's variadic functions like printf:
# A variadic libc function. The bare trailing `...` is allowed ONLY inside an
# unsafe external "C" block; it lowers to a real C-ABI variadic call.
unsafe external "C" as libc because "formatted output via libc printf":
fn printf(string fmt, ...) i32 = "printf"
# Safe wrappers fold the raw i32 return back into Sushi's Result world.
fn say(string label, i32 value) i32:
return Result.Ok(libc.printf("%s = %d\n", label, value))
fn main() i32:
let i32 r = say("answer", 42).realise(0)
if (r > 0):
println("printf reported {r} bytes written")
return Result.Ok(0)
Output:
answer = 42
printf reported 12 bytes written
The story continues in Chapter 16, which covers the ptr type —
the opaque handle C functions like malloc return — and the fences that keep it inside the
unsafe realm. The full FFI guide, including all diagnostic codes and the C
argument-promotion rules, lives in the FFI documentation.
Building a library¶
Finally, your own code. There are two ways to reuse Sushi across files.
The simplest is source import: use "path" (quotes, no extension) pulls another .sushi
file in directly and compiles it together with yours. The path is relative to the importing
file. Here is a small guidelib.sushi whose API functions are marked public so other files
can see them:
# guidelib.sushi - a tiny reusable library.
# Compile as a .slib with: ./sushic --lib guidelib.sushi -o /tmp/guidelib.slib
# Public functions form the library's API.
public fn add(i32 a, i32 b) i32:
return Result.Ok(a + b)
public fn answer() i32:
return Result.Ok(42)
A program imports it by path and calls those functions as if they were local:
# Import another source file directly. The path is relative to this file.
use "guidelib"
fn main() i32:
let i32 sum = add(40, 2).realise(0)
let i32 ans = answer().realise(0)
println("add(40, 2) = {sum}")
println("answer() = {ans}")
return Result.Ok(0)
Compile and run it with the usual one-liner — the compiler finds guidelib.sushi next to
it automatically:
./sushic use-library.sushi -o use-library
./use-library
Output:
add(40, 2) = 42
answer() = 42
For genuine reuse you compile the library once into a .slib — a single file holding
bitcode plus the metadata the compiler needs for type information — and link it by name.
Build the library with --lib, then point SUSHI_LIB_PATH at it and import it with
use <lib/...>:
./sushic --lib guidelib.sushi -o /tmp/guidelib.slib
export SUSHI_LIB_PATH=/tmp
./sushic use-slib.sushi -o use-slib
./use-slib
where the program imports the precompiled library rather than the source:
# Link a precompiled .slib found on SUSHI_LIB_PATH.
use <lib/guidelib>
fn main() i32:
let i32 sum = add(40, 2).realise(0)
let i32 ans = answer().realise(0)
println("add(40, 2) = {sum}")
println("answer() = {ans}")
return Result.Ok(0)
Output:
add(40, 2) = 42
answer() = 42
Sharing further afield
For distributing libraries beyond your own machine there is the nori packager and
the central Omakase repository at omakase.lubica.net. Be aware of the current
limits: libraries have no transitive dependencies, are not portable across platforms,
and do not share generic instantiations across the boundary. See the
libraries guide
for the details.
What you learned¶
- Standard-library modules are imported with
use <name>:<time>for sleeping,<math>for numeric intrinsics (with polymorphicabs/min/max/sqrt/hypot),<random>for seedable pseudo-randomness, and<io/files>for file I/O viaFileResult/FileError. - A native variadic parameter
...T namecollects trailing arguments into an ownedT[]; zero arguments is valid, and it must be the last parameter. - FFI lets you call C from inside an
unsafe external "C" as <ns> because "<reason>"block; externals return raw C values (the one exception to implicitResult), so you wrap them in a safe Sushi function — andstringarguments are marshalled and freed for you. - C varargs (
printf-style bare...) are bound only inside the unsafe external block, kept strictly apart from safe native...Tvariadics. - Reuse your own code with source
use "path"imports, or compile a reusable.slibwith--liband link it viaSUSHI_LIB_PATHanduse <lib/...>.
Where to go next¶
You have travelled from "Mostly Harmless" all the way to linking C and shipping libraries — and two more chapters await: variadic functions and foreign pointers. From here, the reference documentation goes deeper than any tutorial can:
- Language Reference — the complete grammar, types, and operators.
- Standard Library Reference — every module and function.
- FFI Guide and the Variadics Design Note — the full story behind this chapter.
- Libraries Guide
— building, distributing, and the
noripackager.
The compiler is still on your side. Go build something improbable.