Writing a program for GodspeedOS

This is the page for someone who wants to use GodspeedOS rather than understand it. You should not need MISCIS, the capability table, or the IPC wire format to read a file.

The API reference

The standard library API reference - godspeed, imported as gs. Every module carries its own documentation, including what each call blocks on, what authority it needs, and which failures are safe to retry.

The SDK API reference - godspeed_sdk, the layer underneath: the syscalls themselves, and the audited unsafe (CLAUDE.md 18.1). You should not need it. Services and drivers alike are written on gs, drivers through gs::driver (below), and scripts/one_way_check.py holds every program in this repository to zero raw SDK calls that gs covers. A driver package from outside this repository may call it too, but outside the compatibility promise: the SDK can change between releases where gs does not (docs/driver-library.md).

Four kinds of program

The developer CLI makes each with one command, godspeed new <kind> <name>, written on gs from its first line, with a contract and a README that says how to build and run it:

KindWhat it isWritten with
serviceRuns until it is stopped; the supervisor keeps it running and restarts it if it diesgs::ipc, gs::call, gs::fs, gs::net, ...
utilityOne job for one command line: handed its arguments, writes its output, ends with a status the shell reports; its output pipes like any built-in'sgs::program
appA utility that holds the screen and the keyboard while it runs, and gives them back when it endsgs::app
driverDrives one device, named in its contract by PCI class code and register BAR, or a USB device by vendor and productgs::driver

A fifth, library, is reusable code for the others and runs nowhere on its own. godspeed package turns a project into a signed package and godspeed install puts it on a machine; osdev test kinds installs one of each of the four, made from the templates untouched, and sees each do its job.

Start here

A whole program - every line of it, because the parts around your code are not guessable:

#![allow(unused)]
#![no_std]                  // no operating system underneath this one
#![no_main]                 // the entry point is `service_main`, not `main`
#![deny(unsafe_code)]       // required of every program; see the note below

fn main() {
use godspeed::{self as gs, Error, ServiceContext};

// `#[no_mangle]` is itself covered by the `unsafe_code` lint (an exported symbol can collide), and
// the entry symbol must be exported because the linker looks for `service_main` by name. So this
// ONE `#[allow]` is expected on this ONE item. Anywhere else it is rejected.
#[allow(unsafe_code)]
#[no_mangle]
pub extern "C" fn service_main(ctx: ServiceContext) -> ! {
    let mut fs = gs::fs::Fs::new(&ctx);

    // There is no `read_to_string`. GodspeedOS has no heap (CLAUDE.md 26.6.1), so the CALLER owns
    // the buffer and the bound is visible in the source.
    let mut buf = [0u8; 4096];
    match fs.read_into("/data/message.txt", &mut buf) {
        Ok(n) => gs::io::println(&ctx, core::str::from_utf8(&buf[..n]).unwrap_or("<not utf-8>")),
        Err(e) => gs::io::report(&ctx, "read", e),
    }

    // A program does not return. This one is a SERVICE, which waits to be stopped once its work is
    // done; a utility ends instead, with `gs::program::exit` or `Output::finish` (CLAUDE.md 14.5).
    loop { gs::task::yield_now(&ctx); }
}
}

And a contract, or it reaches nothing

Authority is granted, never assumed. A program granted no fs peer gets Error::Unreachable from its first call - not a crash, and not a silent nothing. Put this beside your Cargo.toml, in contracts/<name>.toml:

name    = "hello"
version = "0.1.0"

[resources.memory]
request = "8MiB"
limit   = "16MiB"

[capabilities]
ipc_send     = ["fs"]     # talk to the filesystem; the spawn row must grant it too (below)
ipc_receive  = ["hello"]  # your own endpoint, named after you
log_write    = true       # the log (`ctx.log`) AND the screen (`io::println`, `io::report`)

Printing is log_write. Everything in gs::io, io::report included, makes the kernel's ConsoleWrite call, and LOG_WRITE is the capability the kernel checks before it. Every task holds it, so output appears regardless; declaring it is the contract saying truthfully what the program does, and scripts/contract_check.py requires it of every program that prints.

Never ask for console_push to print. It is the authority to push bytes into the keyboard ring, which the shell reads as typed commands (SEC-2). A keyboard driver needs it; your program does not, and the same checker refuses a console_push declaration that nothing in the code uses.

This page said the opposite until 2026-10-09 - that printing needs console_push and that a program without it is silent. That was a conclusion drawn from documentation and never run; the kernel's code and the project's own test log both refute it (docs/stranger-test.md, "Run 4 was wrong about the screen").

Ask for what you use and nothing more: the contract is the reviewable statement of what your program may do (CLAUDE.md 26.9).

The contract declares; it does not grant. The kernel cannot read TOML. What a running program actually holds comes from the SPAWN REQUEST the supervisor sends, built from the program's row in the supervisor's spawn table (IMAGES in services/supervisor/src/main.rs) - for this program, ("stdlib-hello", .., &["fs"], ..). A peer in the contract with no matching row is a program that looks authorised on paper and is not (CLAUDE.md 13.6). For services, scripts/contract_check.py reconciles the two.

examples/stdlib-hello is the complete, buildable version of this, a step further: it writes a file larger than one IPC message and reads it back, and osdev test examples checks every byte.

The one thing to get right

Every call returns Result<_, gs::Error>, and the error that matters most is the one describing an operation whose outcome nobody knows:

#![allow(unused)]
fn main() {
match fs.write("/data/log.txt", b"hello") {
    Ok(()) => {}
    Err(e) if e.retry_is_safe() => { /* nothing happened: try again */ }
    Err(e) => { /* it MAY have happened: do NOT blindly retry */ }
}
}

Error::OutcomeUnknown means the request left and no answer came back before the deadline. Error::PeerDied means the request arrived and the service died before answering. Either way the write may have committed. Retrying it is not a retry, it is a second write, and for anything that is not idempotent that is a different bug from the one you were recovering from.

retry_is_safe() returns false for both, so that nobody has to derive the rule themselves. This is the failure the Stranger Test watches hardest, because it is invisible in a passing build.

If your program SERVES other tasks

Most programs do not, and can skip this. A service does: it answers requests on its own endpoint, and that is the same endpoint replies from fs and net-stack arrive on. So there is a question with a wrong answer that looks like the right one - what happens to a client request that lands while you are waiting for a reply?

The library's answer is that it does not touch it. A call waits on the reply capability it sent, so the kernel hands back that reply and leaves everything else queued for your own loop. You do not have to drain anything, and nothing you did not ask for is consumed.

#![allow(unused)]
fn main() {
// A service loop. `fs.read_into` may block for seconds; a client that speaks during it is still
// waiting on your endpoint afterwards, not lost.
loop {
    let _req = gs::ipc::recv(&ctx);
    let client = gs::ipc::take_sent_cap(&ctx);   // the reply capability the client sent with it
    let mut buf = [0u8; gs::ipc::MAX_BYTES];
    let n = fs.read_into("/data/answer.txt", &mut buf).unwrap_or(0);
    if let Some(c) = client {
        // Answers without blocking, AND gives the one-shot capability's slot back. A plain send
        // would keep it, and leak one slot per request until the table is full.
        let _ = gs::ipc::reply(&ctx, c, &gs::ipc::Message::from_bytes(&buf[..n]));
    }
}
}

Two exceptions, both of which say so where you reach for them:

  • A capability you hold - an open file (gs::file::File), a socket or a connection (gs::net) - is invoked rather than sent by name, and the kernel routes its reply the same way. Anything else that arrives during one of those is held for you: drain take_held() in a loop after each operation and feed what comes back into your own loop. They are real client requests, and only you can answer them.
  • A handle built with Fs::with_notice polls for an operator pressing q, which means it takes whatever arrives. That is right for a shell, which serves nobody, and wrong for a service.

If your program drives a DEVICE

A driver holds authority nothing else does - a register window, perhaps a DMA arena and an interrupt - and gs::driver is how it uses each. It holds device-NEUTRAL mechanisms only: what a register means stays in your driver, and there is no gs::driver::usb or ::wifi, ever.

ModuleWhat it is for
registersYour device's register window, every access bounds-checked: gs::driver::registers(&ctx)
arena, LayoutYour DMA arena, laid out in named parts, each aligned on its PHYSICAL address and refused by name if it does not fit
waitWaiting for the device, bounded by TIME (Budget), not by a count of tries: wait::until, Deadline, Since
delayHolding still for a minimum the device needs and nothing can report the end of: hold, hold_parked
irqWaiting for your interrupt on the endpoint your clients also send to, without losing a request: Irq::wait answers Interrupt, Request or Timeout, and Irq::take the same without blocking
inputA keyboard driver's keystrokes, pushed to the console: push, and ready when setup is done
powerCutting and restoring your device's power where the machine can: power::set
deviceWhat the kernel knows about your device that you cannot read yourself: the board's MAC, the NIC's PCI identity, a controller's base clock, PCI configuration reads
busA device with no registers of its own, reached through its bus's host: a USB device's identity, control transfers and bulk transfers (UsbDevice). A bus, not a class
#![allow(unused)]
fn main() {
let Some(regs) = gs::driver::registers(&ctx) else {
    ctx.log("mydev: no device was granted - nothing to drive");
    gs::ipc::park(&ctx)
};
regs.write32(CMD, START);
if gs::driver::wait::until(&ctx, gs::driver::wait::Budget::ms(10), || regs.read32(STATUS) & BUSY == 0).is_err() {
    ctx.log("mydev: the device did not finish within 10 ms");   // the CALLER says which wait
}
}

A driver installed as a package is given what its contract names and the operator approves: its register window, its interrupt, and a DMA arena (asked about by a question of its own, because where no IOMMU confines the device a driver with an arena can reach all of memory) - or, for a USB device, its host through bus. A device a GodspeedOS driver drives is taken over while the package is installed. All but the registers arrived after v0.30.0 (docs/developer-services-design.md 13.1). The drivers in this repository use all of the above. docs/driver-library.md is the design and the record of each module's arrival.

gs::program::mode is the mode a program was started in, for one that does more than one thing.

What the library will not do

  • Allocate on your behalf. No heap, by design. You pass the buffer.
  • Hide a failure. There is no silent retry and no fallback path. If something did not happen, the call says so, and if it might have happened, it says that instead.
  • Grant authority. A handle is not a permission. gs::fs::Fs::new carries nothing of its own: every call rides the capability your contract was already granted, and a service that never asked for the filesystem gets Error::Unreachable from the first call. The library makes granted authority convenient to use; it never makes ungranted authority available.

The design report

How the library was designed, and what building it changed - including the mistakes found by migrating real services onto it, the friction reported rather than worked around, and what is deliberately not built yet.

The brief it was built from - the original requirement, annotated with what was met and what was not.