DocsRuntime and tasks

libkore

Runtime, tasks and time

Entry points, tasks and SPMD, the executor, clocks and timeouts, atomic-word waits, exit codes and crash reports in libkore.

Source: docs/runtime.md

Entry points

A Kore executable starts in one of two ways:

  • kore-entry gives an ordinary fn main() that returns (), an ExitCode, or Result<T, ExitCode>. When main returns, the runtime requests the process shutdown barrier: the kernel closes task admission, stops every peer task in the process and reaps them. See Getting started.
  • #[kore::main] marks an async fn main(rt: Rt) -> ! that runs on Kore's executor and never returns; it ends with kore::thread::exit. Its tasks(...) option admits a bounded set of tasks before the first poll.

There is one executor per process. libkore never starts a second one, and its foreign-language surfaces run their callbacks on the same executor.

Tasks, threads and SPMD

#[kore::task] turns an async fn into a task with its own static storage. Calling it returns a spawn token; a second live instance of the same declaration is refused rather than allocated. kore::task also names the executor module.

kore::thread is the native thread API. On Kore a thread is a task that shares its parent's address space; a process is a task in a fresh one. Both are the same kernel object.

#[kore::spmd(x, y, z)] declares a data-parallel launch over a tile grid. The grid is three integer literals, so the worker count, min(x·y·z, MAX_THREADS − 1), is a compile-time fact. Calling the function launches the gang, all or nothing:

Rust
use kore::runtime::spmd::SpmdCtx;

/// Sixteen tiles; each worker runs this body for the tiles it claims.
#[kore::spmd(1, 1, 16)]
fn tile_pass(ctx: &'static SpmdCtx, worker: u32) {
    let _ = (ctx.tiles(), worker);
}

pub fn run() -> bool {
    // Launch is all or nothing: `None` means no worker started.
    match tile_pass() {
        Some(gang) => gang.join_all().is_ok(),
        None => false,
    }
}

A gang is placed on a single core class, so its workers run at comparable speed. Features never grant worker capabilities: a gang can do only what its process can.

Time

Rust
use kore::prelude::*;
use kore::time::TimerResult;

/// Wait a quarter second, then read the monotonic clock.
pub async fn pause() -> TimerResult<Uptime> {
    kore::time::sleep(Duration::from_millis(250)).await?;
    kore::time::uptime()
}

sleep refuses durations outside u64 nanoseconds instead of truncating them. sleep_until takes a typed monotonic instant, Uptime; wall time is a separate type, UnixTime, and the two never convert implicitly. Reading time costs no system call: the runtime projects a user-mode counter through the kernel's read-only clock page. The executor owns timers and parking, so these futures need Kore's executor.

Waiting on an atomic word

kore::futex is a process-local wait and wake on a 32-bit atomic word. Publish the condition before waking, and re-check it after every wake:

Rust
use core::sync::atomic::{AtomicU32, Ordering};
use kore::futex::{self, WaitOutcome};

static READY: AtomicU32 = AtomicU32::new(0);

/// In an executor task: wait until another task publishes READY.
pub async fn wait_until_ready() -> bool {
    loop {
        if READY.load(Ordering::SeqCst) != 0 {
            return true;
        }
        match futex::wait_async(&READY, 0, None).await {
            WaitOutcome::Retry | WaitOutcome::Woken => continue,
            WaitOutcome::TimedOut | WaitOutcome::Unsupported => return false,
        }
    }
}

/// Publish the condition before waking, then wake every waiter.
pub fn publish_ready() {
    READY.store(1, Ordering::SeqCst);
    futex::wake_all(&READY);
}

Use wait_async inside executor tasks; the blocking wait parks a native thread. Unsupported is a refusal, not a timeout to spin on. The wake results count posted notices: they are telemetry, not waiter counts or proof that the condition holds. This is neither Linux's futex system call nor a cross-process primitive.

Exit codes

ExitCode is a closed vocabulary. Programs pick a named standard outcome (OK, USAGE, NOT_FOUND, DENIED, EXHAUSTED, TIMED_OUT, CANCELLED and others) or one of sixteen application slots, APPLICATION_0 to APPLICATION_15 (256–271). There is no integer constructor; exit_codes! names slots:

Rust
use kore::ExitCode;
kore::exit_codes! {
    /// A distinction specific to this application's document format.
    const BAD_DOCUMENT = ExitCode::APPLICATION_0;
}
pub fn fail() -> ! {
    kore::thread::exit(BAD_DOCUMENT)
}

Only zero means success. Native exit and join preserve the full signed 32-bit status, including statuses from foreign code.

Crash reports

A fatal CPU exception or Rust panic ends its whole process. Nothing unwinds and no user finalizer runs; the kernel never unwinds through its own code. Evidence goes to the parent:

  1. Before starting the child, the parent registers a writable, page-aligned RAM lease with crash::register. The child never sees that buffer.
  2. After the child stops, the parent collects the 1024-byte record through the owning task capability. Collection needs ACCESS and REVOKE, so a join-only observer can't read panic text or registers.
  3. The parent reaps the stopped process, then renders the record or persists it with PreparedDump.

A record holds the observed registers, decoded fault data, up to 16 frame-pointer return addresses, up to 256 bytes of panic text, the task's name and its opaque 128-bit identity. An identity names a task for diagnostics; it grants nothing.

Random values

Rust
use kore::random::{random, SystemRng, RandomError};
pub fn example() -> Result<(), RandomError> {
    let id: u128 = random()?;
    let token: [u8; 32] = random()?;
    let mut rng = SystemRng::new();
    let sample: f64 = rng.random()?;
    let index = rng.random_range(0..100)?;
    let mut bytes = [0; 128];
    rng.fill(&mut bytes)?;
    let _ = (id, token, sample, index);
    Ok(())
}

SystemRng keeps a private ChaCha20 generator, seeded from the hardware entropy source and reseeded before 64 KiB of output. Any error leaves the output buffer unchanged, and an unavailable entropy source is reported, never replaced with a fabricated seed. RISC-V userland entropy currently refuses.

Collections and the heap

kore::collections re-exports hashbrown's HashMap, HashSet and HashTable, also in the prelude:

Rust
use kore::prelude::*;

pub fn count() {
    let mut counts = HashMap::new();
    counts.insert("requests", 1u64);
    assert_eq!(counts.get("requests"), Some(&1));
}

The default hasher is fast but not resistant to hash flooding; use a keyed hasher for untrusted keys. Collections allocate through the program's global allocator. A kore-entry executable already has one, the native heap. A #[kore::main] program enables kore-std's heap feature and installs it itself; installing it in a kore-entry executable too would be a second global allocator, which fails to compile:

Rust
use kore::runtime::heap::{Heap, HeapSupplySource};
#[global_allocator]
static HEAP: Heap<HeapSupplySource> = Heap::new_in(HeapSupplySource);

The heap draws from the process's founded heap supply. An absent or exhausted supply can't create memory authority; allocation simply fails.