gGodspeed CLI

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.

src/main.rs
//! 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.

src/main.rs
//! 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.

src/main.rs
//! 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.

src/main.rs
//! 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 files
Cargo.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

  1. Name your namespace in godspeed.toml: publisher = "alice", and a license.
  2. godspeed package builds every architecture and shows exactly what would be published, with every file's size and SHA-256 - before anything leaves your machine.
  3. godspeed publish signs it and publishes it as alice/heartbeat. Publishing covers the one-time setup.

What you cannot build yet, and why

You wantWhat is missing in GodspeedOS
An app with windows or graphicsAn app has the console's grid of text, and nothing beyond it.
A network client or servergs::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 GoA 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.