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), 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 polymorphic abs/min/max/sqrt/hypot), <random> for seedable pseudo-randomness, and <io/files> for file I/O via FileResult/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.