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:
| Kind | What it is | Written with |
|---|---|---|
service | Runs until it is stopped; the supervisor keeps it running and restarts it if it dies | gs::ipc, gs::call, gs::fs, gs::net, ... |
utility | One 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's | gs::program |
app | A utility that holds the screen and the keyboard while it runs, and gives them back when it ends | gs::app |
driver | Drives one device, named in its contract by PCI class code and register BAR, or a USB device by vendor and product | gs::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: draintake_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_noticepolls for an operator pressingq, 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.
| Module | What it is for |
|---|---|
registers | Your device's register window, every access bounds-checked: gs::driver::registers(&ctx) |
arena, Layout | Your DMA arena, laid out in named parts, each aligned on its PHYSICAL address and refused by name if it does not fit |
wait | Waiting for the device, bounded by TIME (Budget), not by a count of tries: wait::until, Deadline, Since |
delay | Holding still for a minimum the device needs and nothing can report the end of: hold, hold_parked |
irq | Waiting 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 |
input | A keyboard driver's keystrokes, pushed to the console: push, and ready when setup is done |
power | Cutting and restoring your device's power where the machine can: power::set |
device | What 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 |
bus | A 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::newcarries nothing of its own: every call rides the capability your contract was already granted, and a service that never asked for the filesystem getsError::Unreachablefrom 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.