Examples
Real projects, each started with godspeed new and written only against the
public standard library, gs. CI builds every one for x86_64, aarch64, armv7 and riscv64
against GodspeedOS v0.30.0, so none of this is code that does not compile. For a utility, an app or a
driver, What you can make starts from each template.
hello
The template itself: the smallest valid service.
heartbeat
A loop that sleeps, and log lines with numbers in them.
shout
A request/reply server - the shape of every Godspeed server.
bootcount
Files, a library dependency, and an outcome nobody can know.
fixedbuf
A library: text without a heap, tested on your workstation.
They build, package, publish and install. Publish one, then at a Godspeed machine's
gsh> prompt, godspeed fetch <you>/heartbeat and then
godspeed install <you>/heartbeat. godspeed run, which
would boot a machine with it from your workstation, still waits on the OS.
The source of every example is in
examples/. To try
one:
$ git clone https://github.com/godspeedhq/godspeed-cli $ cd godspeed-cli/examples/heartbeat $ godspeed check service heartbeat is valid against GodspeedOS v0.30.0. $ godspeed build --all Built x86_64 target/x86_64-unknown-none/release/heartbeat Built aarch64 target/aarch64-unknown-none/release/heartbeat Built armv7 target/armv7a-none-eabi/release/heartbeat Built riscv64 target/riscv64imac-unknown-none-elf/release/heartbeat These are program binaries, not an OS image; nothing was installed or run.
Two things every service has
A contract - contracts/<name>.toml - states the authority the
service requests. It grants nothing: what a service is actually given is the operating system's
decision. godspeed check validates it against the OS's own schema. Read it first: it is the
whole answer to "what can this program do?"
An entry point - service_main - that is handed a
ServiceContext, the only authority the service holds, and never returns. There is no
fn main() yet, and no heap: no String, no Vec, no
format!. Buffers are arrays you can see, and format_args! formats without
allocating.
hello service
What godspeed new service hello creates: log one line, then park - hold still, using no
CPU, until stopped.
//! hello: a Godspeed service. //! //! A service starts at `service_main`, is handed its `ServiceContext` - the only authority it holds - //! and never returns. This one logs that it started, then parks: it holds still and uses no CPU until //! it is stopped. #![deny(unsafe_code)] #![no_std] #![no_main] use godspeed::{self as gs, ServiceContext}; // `#[no_mangle]` is what lets the loader find the entry point. Without it the service starts at // address 0 and faults before its first line. The lint allowance is needed because `#[no_mangle]` // itself counts as unsafe code; nothing else in this crate may be. #[allow(unsafe_code)] #[no_mangle] pub extern "C" fn service_main(ctx: ServiceContext) -> ! { ctx.log("hello: started"); gs::ipc::park(&ctx) }contracts/hello.toml
# The service contract: what this service REQUESTS. It grants nothing. What a service is actually # given is decided by the operating system, not by this file. # Format: contracts/schema/service.schema.json in GodspeedOS. `godspeed check` validates against it. name = "hello" version = "0.1.0" [resources.memory] request = "32MiB" limit = "32MiB" [capabilities] log_write = true # write lines to the kernel log - and nothing else
heartbeat service
Logs how long the machine has been up, every five seconds.
gs::task::sleep_msgives the CPU back for a while - a service that loops without sleeping or waiting burns a core for nothing.ctx.log_fmt(format_args!(...))writes a formatted line to the log with no heap.- Its contract asks for nothing but the log.
//! heartbeat: logs how long the machine has been up, every five seconds. //! //! The smallest service that does something over time: a loop that sleeps - giving the CPU back, not //! spinning - and a formatted log line built with `format_args!`, which needs no heap. #![deny(unsafe_code)] #![no_std] #![no_main] use godspeed::{self as gs, ServiceContext}; /// How often to log. const EVERY_MS: u64 = 5_000; // `#[no_mangle]` is what lets the loader find the entry point; it counts as unsafe code, so it alone is // allowed. #[allow(unsafe_code)] #[no_mangle] pub extern "C" fn service_main(ctx: ServiceContext) -> ! { ctx.log("heartbeat: started"); let mut beats: u64 = 0; loop { gs::task::sleep_ms(&ctx, EVERY_MS); beats = beats.wrapping_add(1); let up = gs::task::uptime_secs(&ctx); ctx.log_fmt(format_args!( "heartbeat: beat {}, up {}m {:02}s", beats, up / 60, up % 60 )); } }contracts/heartbeat.toml
# The service contract: what this service REQUESTS. It grants nothing. What a service is actually # given is decided by the operating system, not by this file. # Format: contracts/schema/service.schema.json in GodspeedOS. `godspeed check` validates against it. name = "heartbeat" version = "0.1.0" [resources.memory] request = "32MiB" limit = "32MiB" [capabilities] log_write = true # write lines to the kernel log - and nothing else
shout service
A request/reply server: send it text and it answers with the same text in capitals.
- It owns an endpoint (
ipc_receive = ["shout"]), and asks to send to no one. It can reach a client only through the reply capability that client puts in its request - no ambient channel, no reaching anyone by name. - It never blocks on a client.
try_send_toreturns at once, so a slow or vanished client cannot stall the server. A send that succeeds means the reply was queued, not that it was read. - It gives each reply capability back with
gs::cap::remove, so a server that runs for years stays bounded. - A client is a single call:
gs::call::request_within(&ctx, "shout", &request, 5), which returns the reply, or an error that says what happened - including that nobody can tell.
//! shout: a request/reply server. Send it text; it answers with the same text in capitals. //! //! The shape of every Godspeed server: wait for a request, answer over the reply capability the request //! carries, and give that capability back. It has no way to reach anyone else - not even the client, //! except through the capability the client chose to hand it. #![deny(unsafe_code)] #![no_std] #![no_main] use godspeed::{self as gs, ipc::Message, ServiceContext}; #[allow(unsafe_code)] #[no_mangle] pub extern "C" fn service_main(ctx: ServiceContext) -> ! { ctx.log("shout: ready"); loop { // Block until a request arrives. With no client, the service simply waits here, idle. let mut request = gs::ipc::recv(&ctx); // A request carries a capability to reply with. One without is malformed: say so and move on. let Some(reply_to) = gs::ipc::take_sent_cap(&ctx) else { ctx.log("shout: a request came with no way to reply; dropped"); continue; }; // Capitals, in place: a message is 4 KiB, and there is no heap to spend on a copy. let len = request.payload_bytes().len(); request.payload[..len].make_ascii_uppercase(); let reply = Message::from_bytes(&request.payload[..len]); // Never block on a client: a slow or vanished one must not stall the server. A send that // succeeds means the reply was queued, not that it was read. if let Err(e) = gs::ipc::try_send_to(&ctx, reply_to, &reply) { ctx.log_fmt(format_args!( "shout: could not reply ({}); the client must ask again", e.as_str() )); } // Done with the reply capability: give its slot back, so a long-running server stays bounded. gs::cap::remove(&ctx, reply_to); } }contracts/shout.toml
# The service contract: what this service REQUESTS. It grants nothing. What a service is actually # given is decided by the operating system, not by this file. # Format: contracts/schema/service.schema.json in GodspeedOS. `godspeed check` validates against it. name = "shout" version = "0.1.0" [resources.memory] request = "32MiB" limit = "32MiB" [capabilities] log_write = true # write lines to the kernel log ipc_receive = ["shout"] # own the endpoint clients send requests to # No ipc_send: a server answers over the reply capability each request carries, not a named peer.
bootcount service
Counts how many times the machine has started, in a file.
- Files through
gs::fs.Fs::newgrants nothing; the contract'sipc_send = ["fs"]is why the calls can work at all. Without it, the same code compiles and every call failsUnreachable. The library makes the filesystem convenient; only the OS can make it available. - A library dependency: fixedbuf, by path, turns the count into bytes without a heap and parses it back.
- Honest failure. A write that got no answer may or may not have happened. It says exactly that - and does not write again, which could count one boot twice. Something in the file it does not understand is left alone, not overwritten.
//! bootcount: counts how many times this machine has started, in a file. //! //! Reads the count, adds one, writes it back, and logs it - using the filesystem through the standard //! library, and the `fixedbuf` library from this repository to turn the number into bytes without a heap. //! The part worth reading is the error handling: a write that may or may not have happened is reported //! as exactly that, never retried blindly. #![deny(unsafe_code)] #![no_std] #![no_main] use core::fmt::Write; use fixedbuf::{parse_u64, FixedBuf}; use godspeed::{self as gs, Error, ServiceContext}; const PATH: &str = "/bootcount.txt"; #[allow(unsafe_code)] #[no_mangle] pub extern "C" fn service_main(ctx: ServiceContext) -> ! { // Grants nothing by itself: it reaches the filesystem only because the contract asks for // `ipc_send = ["fs"]` and the OS granted it. Without that, every call below fails `Unreachable`. let mut disk = gs::fs::Fs::new(&ctx); let mut stored = [0u8; 32]; let previous = match disk.read_into(PATH, &mut stored) { Ok(n) => match parse_u64(&stored[..n]) { Some(count) => count, None => { // Never overwrite something we do not understand. ctx.log_fmt(format_args!( "bootcount: {} does not hold a count; leaving it alone", PATH )); gs::ipc::park(&ctx); } }, Err(Error::NotFound) => 0, // the first boot Err(e) => { gs::io::report(&ctx, "bootcount: read", e); gs::ipc::park(&ctx); } }; let count = previous.saturating_add(1); let mut text = FixedBuf::<24>::new(); // A u64 and a newline always fit in 24 bytes, so this cannot fail. let _ = writeln!(text, "{}", count); match disk.write(PATH, text.as_bytes()) { Ok(()) => ctx.log_fmt(format_args!("bootcount: this is boot {}", count)), // No answer, or the filesystem died holding the request: the count may or may not be saved. // Saying so is the honest answer; writing again could count this boot twice. Err(e @ (Error::OutcomeUnknown | Error::PeerDied)) => ctx.log_fmt(format_args!( "bootcount: boot {}, but whether it was saved is unknown ({})", count, e.as_str() )), Err(e) => gs::io::report(&ctx, "bootcount: write", e), } gs::ipc::park(&ctx) }contracts/bootcount.toml
# The service contract: what this service REQUESTS. It grants nothing. What a service is actually # given is decided by the operating system, not by this file. # Format: contracts/schema/service.schema.json in GodspeedOS. `godspeed check` validates against it. name = "bootcount" version = "0.1.0" [resources.memory] request = "32MiB" limit = "32MiB" [capabilities] log_write = true # write lines to the kernel log ipc_send = ["fs"] # ask the filesystem service for filesCargo.toml (dependencies)
[dependencies] # The fixedbuf library from this repository, beside this project. fixedbuf = { path = "../fixedbuf" } # The Godspeed standard library, at the OS release this project was created against. godspeed = { git = "https://github.com/godspeedhq/godspeed-os", tag = "v0.30.0" }
fixedbuf library
Text built into a fixed-size buffer. write! into it, then hand
as_bytes() to whatever sends or stores them. Text that does not fit is refused - and the
buffer says so - never wrapped around or silently lost, and a character is never cut in half.
A library is plain no_std Rust, so it builds for every architecture, and
godspeed test runs its tests on your workstation:
$ godspeed test Running tests... test tests::builds_text_in_place ... ok test tests::never_splits_a_character ... ok test tests::parses_numbers_and_refuses_the_rest ... ok test tests::refuses_what_does_not_fit_and_says_so ... ok test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out All tests passed.src/lib.rs
//! fixedbuf: text built into a fixed-size buffer, for programs with no heap. //! //! A Godspeed service has no allocator, so there is no `String` and no `format!`. Logging needs neither //! (`ctx.log_fmt(format_args!(...))` formats straight to the log), but an IPC message or a file needs the //! bytes themselves. `FixedBuf` is a `core::fmt::Write` over an array: `write!` into it, then hand //! `as_bytes()` to whatever sends or stores them. Text that does not fit is refused - `write!` returns an //! error and `is_truncated()` says so - never wrapped around or silently lost. //! //! Plain `no_std` Rust with no dependencies, so it builds for every Godspeed architecture and its tests run //! on your workstation with `godspeed test`. #![cfg_attr(not(test), no_std)] #![deny(unsafe_code)] use core::fmt; /// Up to `N` bytes of UTF-8 text, built in place. pub struct FixedBuf<const N: usize> { bytes: [u8; N], len: usize, truncated: bool, } impl<const N: usize> FixedBuf<N> { /// An empty buffer. pub const fn new() -> Self { FixedBuf { bytes: [0; N], len: 0, truncated: false, } } /// What has been written so far. pub fn as_bytes(&self) -> &[u8] { &self.bytes[..self.len] } /// The same, as text. Only whole characters are ever kept, so this always succeeds. pub fn as_str(&self) -> &str { core::str::from_utf8(self.as_bytes()).unwrap_or("") } /// Whether some text was refused because it did not fit. pub fn is_truncated(&self) -> bool { self.truncated } /// Empty it, to build the next piece of text in the same space. pub fn clear(&mut self) { self.len = 0; self.truncated = false; } } impl<const N: usize> Default for FixedBuf<N> { fn default() -> Self { Self::new() } } impl<const N: usize> fmt::Write for FixedBuf<N> { fn write_str(&mut self, s: &str) -> fmt::Result { let room = N - self.len; if s.len() <= room { self.bytes[self.len..self.len + s.len()].copy_from_slice(s.as_bytes()); self.len += s.len(); return Ok(()); } // Keep what fits, cut at a character boundary so the text stays valid UTF-8, and say so. let mut cut = room; while !s.is_char_boundary(cut) { cut -= 1; } self.bytes[self.len..self.len + cut].copy_from_slice(&s.as_bytes()[..cut]); self.len += cut; self.truncated = true; Err(fmt::Error) } } /// Parse an unsigned decimal number, allowing surrounding ASCII whitespace (a trailing newline, say). /// /// Returns `None` for no digits, any other byte, or a value that does not fit in a `u64` - a refusal, /// never a wrapped or truncated number. pub fn parse_u64(text: &[u8]) -> Option<u64> { let text = text.trim_ascii(); if text.is_empty() { return None; } let mut value: u64 = 0; for &b in text { if !b.is_ascii_digit() { return None; } value = value.checked_mul(10)?.checked_add(u64::from(b - b'0'))?; } Some(value) } #[cfg(test)] mod tests { use super::{parse_u64, FixedBuf}; use core::fmt::Write; #[test] fn builds_text_in_place() { let mut buf = FixedBuf::<32>::new(); write!(buf, "boot {} of {}", 3, "this machine").unwrap(); assert_eq!(buf.as_str(), "boot 3 of this machine"); assert!(!buf.is_truncated()); buf.clear(); write!(buf, "{}", 42).unwrap(); assert_eq!(buf.as_bytes(), b"42"); } #[test] fn refuses_what_does_not_fit_and_says_so() { let mut buf = FixedBuf::<8>::new(); assert!(write!(buf, "{}", "123456789").is_err()); assert_eq!(buf.as_str(), "12345678"); assert!(buf.is_truncated()); } #[test] fn never_splits_a_character() { let mut buf = FixedBuf::<5>::new(); assert!(write!(buf, "abcé!").is_err()); // 'é' is two bytes: only "abc" + 'é' would be 5 assert_eq!(buf.as_str(), "abcé"); let mut buf = FixedBuf::<4>::new(); assert!(write!(buf, "abcé").is_err()); assert_eq!(buf.as_str(), "abc"); } #[test] fn parses_numbers_and_refuses_the_rest() { assert_eq!(parse_u64(b"7\n"), Some(7)); assert_eq!(parse_u64(b" 18446744073709551615 "), Some(u64::MAX)); assert_eq!(parse_u64(b""), None); assert_eq!(parse_u64(b"\n"), None); assert_eq!(parse_u64(b"12a"), None); assert_eq!(parse_u64(b"18446744073709551616"), None); // one past u64::MAX } }
From an example to a published package
- Name your namespace in
godspeed.toml:publisher = "alice", and alicense. godspeed packagebuilds every architecture and shows exactly what would be published, with every file's size and SHA-256 - before anything leaves your machine.godspeed publishsigns it and publishes it asalice/heartbeat. Publishing covers the one-time setup.
What you cannot build yet, and why
| You want | What is missing in GodspeedOS |
|---|---|
| An app with windows or graphics | An app has the console's grid of text, and nothing beyond it. |
| A network client or server | gs::net exists and an example would compile, but nothing yet grants an outside program access to the network stack. |
| A program in C, C++ or Go | A non-Rust interface to the OS. More. |
Each is the OS's to build, and the CLI will follow. Until then it makes no template that pretends.