Rust Cross-Compilation for ostoo Userspace
Build Rust userspace programs natively on macOS, producing static x86_64 ELF binaries that run on the ostoo kernel.
Architecture
user-rs/ # Separate Cargo workspace
├── Cargo.toml # workspace: rt, hello-rs, hello-std
├── .cargo/config.toml # custom target, build-std
├── x86_64-ostoo-user.json # custom target spec (no CRT objects)
├── rt/ # ostoo-rt runtime crate
│ ├── Cargo.toml # features: no_std (default)
│ └── src/
│ ├── lib.rs # _start, panic handler, global allocator
│ ├── syscall.rs # inline-asm SYSCALL wrappers
│ ├── io.rs # print!/println!/eprint!/eprintln! macros
│ └── alloc_impl.rs # brk-based bump allocator
├── hello-rs/ # no_std + alloc example (~5 KiB)
│ ├── Cargo.toml
│ └── src/main.rs
└── hello-std/ # full std example (~54 KiB)
├── Cargo.toml
└── src/main.rs
sysroot/ # musl sysroot (extracted, gitignored)
└── x86_64-ostoo-user/
├── lib/ # libc.a, crt*.o, libunwind.a stub
└── include/ # C headers
Why a separate workspace?
The kernel uses a custom target (x86_64-os.json) that disables SSE and the
red zone. Userspace needs standard x86_64 ABI with SSE and red zone enabled.
A separate workspace with its own .cargo/config.toml avoids target conflicts.
Custom target (x86_64-ostoo-user.json)
Based on x86_64-unknown-linux-musl but with empty pre-link-objects and
post-link-objects — we provide our own _start in ostoo-rt instead of
using musl’s CRT startup files. Has crt-static-default: true so that the
libc crate links libc.a statically when building std.
Building
# One-time: extract musl sysroot from the ostoo-compiler Docker image
scripts/extract-musl-sysroot.sh
# Build and deploy to user/ (visible at /host/ in guest via virtio-9p)
# (automatically calls extract-musl-sysroot.sh if needed)
scripts/user-rs-build.sh
# Or manually:
cd user-rs
cargo build --release
Uses build-std to compile std and panic_abort (and transitively core,
alloc, compiler_builtins, libc, unwind) from source. Links against
libc.a from the musl sysroot. Requires the nightly toolchain with rust-src
component (already in rust-toolchain.toml).
Note: packages must be built separately (-p hello-rs, then -p hello-std)
because Cargo feature unification would otherwise merge ostoo-rt’s no_std
feature across the workspace, causing duplicate #[panic_handler] errors. The
build script handles this automatically.
Release profile
opt-level = "s", lto = true, panic = "abort", strip = true — produces
small binaries (the hello world example is ~4.6 KiB).
Runtime crate (ostoo-rt)
ostoo-rt has a no_std feature (enabled by default). With no_std, it
provides a panic handler, global allocator, and OOM handler. Without it
(for std programs), it provides only _start and syscall wrappers.
Tier 1: no_std + alloc programs
#![no_std]
#![no_main]
extern crate ostoo_rt;
use ostoo_rt::println;
#[no_mangle]
fn main() -> i32 {
println!("Hello from Rust on ostoo!");
0
}
Depend on ostoo-rt with default features (includes no_std).
Tier 2: std programs
#![feature(restricted_std)]
#![no_main]
extern crate ostoo_rt;
use std::collections::HashMap;
#[no_mangle]
fn main() -> i32 {
println!("Hello from Rust std on ostoo!");
let mut map = HashMap::new();
map.insert("key", 42);
println!("HashMap works: {:?}", map);
0
}
Depend on ostoo-rt with default-features = false (disables no_std so
std’s panic handler and allocator are used instead). The
#![feature(restricted_std)] attribute is required for custom JSON targets.
What ostoo-rt provides
-
_startentry point (always, but behaviour differs by mode):no_std: readsargc/argvfrom the stack, calls_start_rust→ user’smain() -> i32directly.std: extractsargc/argvfrom the stack and calls musl’s__libc_start_main(main, argc, argv, ...)which initializes libc (TLS viaarch_prctl, stdio, locale, auxvec parsing) before callingmain(argc, argv, envp). This is essential — without libc init, musl’swrite()and other functions fault on uninitialised TLS.
-
Syscall wrappers (always) —
syscall0throughsyscall4via inline asm (SYSCALL instruction). Typed wrappers:write,read,open,close,exit,brk,getcwd,chdir,getdents64,wait4. -
print!/println!/eprint!/eprintln!macros (always) — write to fd 1/2 viacore::fmt::Write. Instdmode, preferstd::println!instead. -
Global allocator (
no_stdonly) — brk-based bump allocator. -
Panic handler (
no_stdonly) — prints panic info to stderr, exits 101.
Adding new programs
- Create
user-rs/<name>/Cargo.tomlwithostoo-rtdependency - Add
"<name>"to workspace members inuser-rs/Cargo.toml - Add the binary name to the deploy loop in
scripts/user-rs-build.sh
Verification
# Binary format check
file user-rs/target/x86_64-ostoo-user/release/hello-rs
# → ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, stripped
# Entry point is _start (verify disassembly shows mov rdi,[rsp]; lea rsi,[rsp+8])
llvm-objdump -d --start-address=<entry> target/x86_64-ostoo-user/release/hello-rs
# No .interp section (no dynamic linker)
llvm-readobj -S target/x86_64-ostoo-user/release/hello-rs | grep -c .interp # → 0
Boot ostoo, then:
> spawn /host/hello-rs
Hello from Rust on ostoo!
Heap works: 42
Musl sysroot
The musl sysroot provides libc.a for Rust’s std to link against. It is
extracted from the ostoo-compiler Docker image (which builds musl 1.2.5 via
crosstool-ng).
# Extract sysroot (skips if already present)
scripts/extract-musl-sysroot.sh
The sysroot is placed at sysroot/x86_64-ostoo-user/ and is gitignored. It
contains:
lib/libc.a— musl static C librarylib/crt1.o,crti.o,crtn.o— CRT objects (not linked by default; our target spec has emptypre-link-objects)lib/libunwind.a— empty stub (satisfiesunwindcrate’s#[link]; musl’s unwinder is inlibc.a, and withpanic=abortunwinding is never invoked)include/— C headers
The cargo config passes -Lnative=../sysroot/x86_64-ostoo-user/lib to the
linker so it can find libc.a and libunwind.a.
Binary sizes
| Program | Mode | Size |
|---|---|---|
| hello-rs | no_std + alloc | ~5 KiB |
| hello-std | full std | ~55 KiB |
Both use opt-level = "s", LTO, panic = "abort", and stripping.