Skip to content

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.is_ok()), 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

<io/fs> opens files with open(path, mode), which answers a File on an IoError channel. A File OWNS its operating-system descriptor: it moves to exactly one owner, and when that owner leaves scope the descriptor closes. So there is no close() to remember, and no way to leak one -- and the same IoError carries every read, write and seek, so one ?? chain covers a whole function.

Buffering is a separate type you opt into. <io/buf> gives BufReader@(R) over anything that can read and BufWriter@(W) over anything that can write, each paying one system call per WINDOW rather than one per line. The constructor TAKES the handle, so the unbuffered File cannot be used behind the reader's back.

Here we write a file under /tmp and read the first line back through a buffer.

use <io/fs>
use <io/buf>

# A handle owns its descriptor: it closes itself when its owner leaves scope,
# and every read and write answers the same IoError channel.
fn file_entry(string path, string entry) ~ | IoError:
    let File f = open(path, FileMode.Write())??
    f.writeln(entry)??
    return Result.Ok(~)
    # f drops here, and the descriptor closes.

# Buffering is a type you opt into. BufReader.new TAKES the handle, so the
# unbuffered File cannot be used behind the reader's back.
fn read_entry(string path) string | IoError:
    let File f = open(path, FileMode.Read())??
    let BufReader@(File) r = BufReader.new(nom f, 4096)??
    # The scrutinee is a temporary the match OWNS, so `nom` may take the line
    # out of it instead of borrowing it.
    match r.read_line()??:
        Maybe.Some(nom line) -> return Result.Ok(nom line)
        Maybe.None -> return Result.Ok("")

fn main() i32:
    match file_entry("/tmp/guide_entry.txt", "Earth: Mostly Harmless"):
        Result.Ok(_) ->
            println("Entry filed.")
        Result.Err(IoError.PermissionDenied()) ->
            println("Permission denied")
            return Result.Ok(1)
        Result.Err(_) ->
            println("Failed to write entry")
            return Result.Ok(1)

    match read_entry("/tmp/guide_entry.txt"):
        Result.Ok(content) ->
            println("Entry reads: {content}")
        Result.Err(IoError.NotFound()) ->
            println("Entry not found")
            return Result.Ok(1)
        Result.Err(_) ->
            println("Failed to read entry")
            return Result.Ok(1)

    return Result.Ok(0)

Output:

Entry filed.
Entry reads: Earth: Mostly Harmless

<io/files> sits UNDER all of that: the path utilities (exists, remove, read_dir) and the fd_* descriptor primitives that File is written on top of. Other modules you will reach for include <sys/env> for environment variables, <io/contracts> when a function should name the capability it needs rather than the type, 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. Everything a unit exports carries the marker -- a const, a struct, an enum and a perk as much as a fn -- and everything unmarked stays that unit's own:

# 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 the library's source text plus the index 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 --lib-version 1.0.0 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 library by name rather than compiling its source alongside:

# 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 polymorphic abs/min/max/sqrt/hypot), <random> for seedable pseudo-randomness, and <io/files> for file I/O via Result@(File, IoError)/FileError.
  • A native variadic parameter ...T name collects trailing arguments into an owned T[]; 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 implicit Result), so you wrap them in a safe Sushi function — and string arguments 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 ...T variadics.
  • Reuse your own code with source use "path" imports, or compile a reusable .slib with --lib and link it via SUSHI_LIB_PATH and use <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:

The compiler is still on your side. Go build something improbable.