feat: Rust std target + ruststd tests; SYS_EXIT_GROUP; fix mkdir/create under missing parent

This commit is contained in:
daniel committed 2026-10-17 10:30:24 +02:00
1 parent 9e66c3b249
commit e87bd358a6
20 files changed
+657 -97

No files matched your search

+1 -1
View File
@@ -12,4 +12,4 @@
#pragma once
#define MONTAUK_BUILD_NUMBER 1
#define MONTAUK_BUILD_NUMBER 3
+19
View File
@@ -27,6 +27,23 @@ namespace montauk::abi {
return required == 0 || Sched::HasCapability(required);
}
// Drivers fail a create under a missing parent with a bare -1; report it
// as NoSuchPath so callers can tell it apart (Rust's create_dir_all only
// creates the missing ancestors when mkdir says NotFound).
static bool ParentDirExists(const char* resolved) {
char parent[256];
int len = Lib::strlen(resolved);
if (len >= (int)sizeof(parent)) return false;
memcpy(parent, resolved, len + 1);
while (len > 0 && parent[len - 1] == '/') len--;
while (len > 0 && parent[len - 1] != '/') len--;
while (len > 0 && parent[len - 1] == '/') len--;
if (len == 0 || parent[len - 1] == ':') return true; // drive root
parent[len] = '\0';
Fs::Vfs::StatInfo info;
return Fs::Vfs::VfsStat(parent, info) == 0 && info.isDir;
}
static int Sys_Open(const char* path) {
char resolved[256];
if (!ResolveProcessPath(path, resolved, sizeof(resolved))) return InvalidArgument;
@@ -117,6 +134,7 @@ namespace montauk::abi {
char resolved[256];
if (!ResolveProcessPath(path, resolved, sizeof(resolved))) return InvalidArgument;
if (!CanModifyFilePath(resolved)) return SystemIntegrityError;
if (!ParentDirExists(resolved)) return NoSuchPath;
return Ipc::CreateFileHandle(resolved);
}
@@ -171,6 +189,7 @@ namespace montauk::abi {
char resolved[256];
if (!ResolveProcessPath(path, resolved, sizeof(resolved))) return InvalidArgument;
if (!CanModifyFilePath(resolved)) return SystemIntegrityError;
if (!ParentDirExists(resolved)) return NoSuchPath;
return Fs::Vfs::VfsMkdir(resolved);
}
+8
View File
@@ -228,6 +228,14 @@ namespace montauk::abi {
Sched::ExitProcess();
}
// Like Sys_Abort, but a normal exit with the caller's code: every thread
// goes, and waiters see exitCode & 0xFF, the same as SYS_EXIT on the main
// thread.
static void Sys_ExitGroup(int exitCode) {
Sched::KillProcess(Sched::GetCurrentPid(), exitCode & 0xFF);
Sched::ExitProcess();
}
static int Sys_Kill(int pid) {
// A process may always kill itself (kill(getpid(), ...)); waiters
// see TerminatedKilled. abort() uses SYS_ABORT instead.
+3
View File
@@ -403,6 +403,9 @@ namespace montauk::abi {
case SYS_ABORT:
Sys_Abort();
return Success;
case SYS_EXIT_GROUP:
Sys_ExitGroup((int)frame->arg1);
return Success;
case SYS_SETSESSION:
return (int64_t)Sys_SetSession();
case SYS_KILLSESSION:
+6
View File
@@ -380,6 +380,12 @@ namespace montauk::abi {
sibling thread ends only that thread, so abort() needs its own call. */
static constexpr uint64_t SYS_ABORT = 198; // () -> does not return
/* Normal termination of the whole process from any thread, with an exit
code (0..255) instead of SYS_ABORT's fixed TerminatedAborted. libc
exit() and Rust std::process::exit() use it, so exiting from a sibling
thread ends the process rather than only that thread. */
static constexpr uint64_t SYS_EXIT_GROUP = 199; // (exitCode) -> does not return
/* Kernel-owned process capabilities. User identities may namespace
per-user resources, but never participate in authorization decisions. */
static constexpr uint64_t CAP_PROCESS_ADMIN = 1ULL << 0;
+18
View File
@@ -89,6 +89,20 @@ namespace Fs::Ramdisk {
return -1;
}
// Mkdir/Create must not conjure entries under a missing parent: the
// table is flat, so "a/b/" with no "a/" entry is invisible to ReadDir of
// the parent and makes "a" fail Stat, Delete and recursive removal.
static bool ParentExists(const char* path) {
char normalized[MaxNameLen];
int len = NormalizePath(path, normalized, MaxNameLen);
if (len < 0) return false;
while (len > 0 && normalized[len - 1] != '/') len--;
if (len <= 1) return true; // parent is the root
normalized[len - 1] = '\0';
int idx = FindEntryByPath(normalized);
return idx >= 0 && fileTable[idx].isDirectory;
}
void Initialize(void* moduleData, uint64_t moduleSize) {
Kt::KernelLogStream(Kt::OK, "Ramdisk") << "Parsing USTAR archive (" << moduleSize << " bytes)";
@@ -442,6 +456,8 @@ namespace Fs::Ramdisk {
}
}
if (!ParentExists(path)) return -1;
// Create new file entry
FileEntry& entry = fileTable[fileCount];
@@ -531,6 +547,8 @@ namespace Fs::Ramdisk {
}
}
if (!ParentExists(path)) return -1;
// Create directory entry (stored with trailing slash for tar convention)
FileEntry& entry = fileTable[fileCount];
+18 -8
View File
@@ -320,22 +320,32 @@ mandelbrot: libc
screenshot: libc libjpegwrite libloader dialogs
$(MAKE) -C src/screenshot
# Rust programs (programs/rust, Phase 1: no_std + alloc via -Zbuild-std).
# Optional: skipped with a notice when no rustup-managed cargo with the
# pinned nightly is installed, so the default build never requires Rust.
# RUSTUP_AUTO_INSTALL=0 keeps 'make' from downloading a toolchain itself.
# Rust programs (programs/rust): std and no_std programs for the built-in
# x86_64-unknown-montauk target of the forked rustc (Phase 2), registered with
# rustup as the 'montauk' toolchain (see programs/rust/README.md). Optional:
# skipped with a notice when that toolchain is missing, so the default build
# never requires Rust. RUSTUP_AUTO_INSTALL=0 keeps 'make' from downloading one.
# Rust links the toolchain sysroot libc (like ports), not lib/libc directly:
# re-run build-montauk-toolchain.sh after libc changes.
# re-run build-montauk-toolchain.sh after libc changes. Cargo tracks neither
# that archive nor a rebuilt std in the linked toolchain, so the programs are
# cleaned (and rebuilt) when either is newer than the last build.
CARGO ?= $(firstword $(wildcard $(HOME)/.cargo/bin/cargo) $(shell command -v cargo 2>/dev/null))
RUST_DIR := rust
RUST_OUT := $(RUST_DIR)/target/x86_64-unknown-montauk/release
RUST_PROGRAMS := rusthello
RUST_PROGRAMS := rusthello ruststd
RUST_LIBC := ../toolchain/sysroot/usr/lib/libc.a
rust:
@if [ -z "$(CARGO)" ] || ! (cd $(RUST_DIR) && RUSTUP_AUTO_INSTALL=0 $(CARGO) --version >/dev/null 2>&1); then \
echo "rust: no nightly cargo found - skipping Rust programs (see programs/rust/README.md)"; \
@if [ -z "$(CARGO)" ] || ! (cd $(RUST_DIR) && RUSTUP_AUTO_INSTALL=0 rustc --print target-list 2>/dev/null | grep -qx x86_64-unknown-montauk); then \
echo "rust: no 'montauk' rustup toolchain - skipping Rust programs (see programs/rust/README.md)"; \
else \
stdlib=$$(cd $(RUST_DIR) && RUSTUP_AUTO_INSTALL=0 rustc --print sysroot)/lib/rustlib/x86_64-unknown-montauk/lib; \
if [ ! -f $(RUST_OUT)/.link-stamp ] || [ $(RUST_LIBC) -nt $(RUST_OUT)/.link-stamp ] || \
[ -n "$$(find $$stdlib -name '*.rlib' -newer $(RUST_OUT)/.link-stamp 2>/dev/null | head -1)" ]; then \
(cd $(RUST_DIR) && RUSTUP_AUTO_INSTALL=0 $(CARGO) clean --release --quiet $(addprefix -p ,$(RUST_PROGRAMS))) || exit 1; \
fi; \
(cd $(RUST_DIR) && RUSTUP_AUTO_INSTALL=0 $(CARGO) build --release) || exit 1; \
touch $(RUST_OUT)/.link-stamp; \
mkdir -p $(BINDIR)/os; \
for p in $(RUST_PROGRAMS); do cp $(RUST_OUT)/$$p $(BINDIR)/os/$$p.elf || exit 1; done; \
fi
+6
View File
@@ -293,6 +293,12 @@ namespace montauk::abi {
sibling thread ends only that thread, so abort() needs its own call. */
static constexpr uint64_t SYS_ABORT = 198; // () -> does not return
/* Normal termination of the whole process from any thread, with an exit
code (0..255) instead of SYS_ABORT's fixed TerminatedAborted. libc
exit() and Rust std::process::exit() use it, so exiting from a sibling
thread ends the process rather than only that thread. */
static constexpr uint64_t SYS_EXIT_GROUP = 199; // (exitCode) -> does not return
/* Kernel-owned process capabilities. User identities may namespace
per-user resources, but never participate in authorization decisions. */
static constexpr uint64_t CAP_PROCESS_ADMIN = 1ULL << 0;
+1
View File
@@ -222,6 +222,7 @@ extern "C" {
#define MTK_SYS_FUTEX_WAIT 196
#define MTK_SYS_FUTEX_WAKE 197
#define MTK_SYS_ABORT 198
#define MTK_SYS_EXIT_GROUP 199
/* @SYSCALLS-END */
/* Syscall status codes (montauk/statuscodes.h for C++). */
+4 -2
View File
@@ -131,6 +131,7 @@ static inline long _mtk_syscall4(long nr, long a1, long a2, long a3, long a4) {
#define SYS_SETENVIRON 172
#define SYS_SPAWN_ENV 173
#define SYS_ABORT 198
#define SYS_EXIT_GROUP 199
/* ========================================================================
errno
@@ -1240,7 +1241,8 @@ int atexit(void (*func)(void)) {
void exit(int status) {
for (int i = _atexit_count - 1; i >= 0; i--)
_atexit_funcs[i]();
_mtk_syscall1(SYS_EXIT, (long)status);
/* SYS_EXIT from a sibling thread would end only that thread. */
_mtk_syscall1(SYS_EXIT_GROUP, (long)status);
__builtin_unreachable();
}
@@ -4066,7 +4068,7 @@ pid_t waitpid(pid_t pid, int *status, int options) {
}
void _exit(int status) {
_mtk_syscall1(SYS_EXIT, (long)status);
_mtk_syscall1(SYS_EXIT_GROUP, (long)status);
__builtin_unreachable();
}
+1 -7
View File
@@ -1,13 +1,7 @@
# Phase 1 Rust support: no_std + alloc on a stock nightly, built with
# -Zbuild-std against the custom target spec next to this directory.
# Paths below are relative to programs/rust/ (the parent of .cargo/).
[build]
target = "x86_64-unknown-montauk.json"
[unstable]
build-std = ["core", "alloc"]
json-target-spec = true
target = "x86_64-unknown-montauk"
[target.x86_64-unknown-montauk]
# The montauk GCC driver supplies crt1/crti/crtbegin, -lc and libgcc, and
+4
View File
@@ -12,3 +12,7 @@ version = "0.1.0"
dependencies = [
"montauk-rt",
]
[[package]]
name = "ruststd"
version = "0.1.0"
+1 -1
View File
@@ -1,6 +1,6 @@
[workspace]
resolver = "2"
members = ["montauk-rt", "rusthello"]
members = ["montauk-rt", "rusthello", "ruststd"]
[profile.dev]
panic = "abort"
+74 -39
View File
@@ -1,18 +1,31 @@
# Rust on MontaukOS
Phase 1: `no_std` + `alloc` programs, cross-compiled from Linux with a
stock nightly toolchain and a custom target spec. No compiler fork.
Rust programs are built for `x86_64-unknown-montauk`, a built-in target of a
forked rustc with a native `std` (`target_os = "montauk"`, not a Unix). Both
`std` programs and `no_std` programs (on `montauk-rt`) are supported.
## Setup (once)
```bash
# rustup, plus a nightly whose cargo the linked toolchain borrows
curl --proto '=https' -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain none
~/.cargo/bin/rustup toolchain install nightly --component rust-src
~/.cargo/bin/rustup toolchain install nightly
# The fork: rust-lang/rust, branch `montauk` (bootstrap.toml is set up)
cd ~/dev/mtk/rust
./x build library --stage 1 --target x86_64-unknown-linux-gnu,x86_64-unknown-montauk
rustup toolchain link montauk ~/dev/mtk/rust/build/host/stage1
```
`rust-toolchain.toml` selects nightly + `rust-src` for this directory.
`make programs` builds the Rust programs when that toolchain is present
and prints a notice and skips them otherwise. It never downloads a toolchain
The stage1 build takes about five minutes (it downloads CI LLVM). After
changing only `library/`, rebuild with
`./x build library --stage 1 --target x86_64-unknown-montauk` (seconds); the
link stays valid.
`rust-toolchain.toml` selects the `montauk` toolchain for this directory. It
has no cargo of its own; rustup falls back to the nightly's cargo.
`make programs` builds the Rust programs when that toolchain is linked, and
prints a notice and skips them otherwise. It never downloads a toolchain
(`RUSTUP_AUTO_INSTALL=0`).
## Building
@@ -26,45 +39,67 @@ Output: `target/x86_64-unknown-montauk/release/<name>`, installed as
`programs/bin/os/<name>.elf`. Add new binaries to `RUST_PROGRAMS` in
`programs/GNUmakefile` and to the workspace `members`.
The GCC driver links crt1/crti/crtbegin, the libc and libgcc from
`toolchain/sysroot`, the same as ports. After changing `programs/lib/libc`,
re-run `toolchain/build-montauk-toolchain.sh` so Rust programs pick it up.
`make rust` relinks the programs when the sysroot libc or the toolchain's std
is newer than the last build (cargo tracks neither).
## Layout
| Path | Purpose |
|------|---------|
| `x86_64-unknown-montauk.json` | Target spec: static non-PIE ET_EXEC, no red zone, local-exec TLS, panic=abort |
| `.cargo/config.toml` | `-Zbuild-std=core,alloc`, linker = `x86_64-montauk-gcc` |
| `montauk-rt/` | Runtime: syscalls, allocator, `print!`, argv, threads, futex `Mutex`, panic handler |
| `rusthello/` | Smoke test (`rusthello`, `rusthello panic`, `rusthello panic-thread`) |
| `.cargo/config.toml` | target = `x86_64-unknown-montauk`, linker = `x86_64-montauk-gcc` |
| `rust-toolchain.toml` | the linked `montauk` toolchain |
| `ruststd/` | `std` smoke test (`ruststd`, `ruststd net <host> [port]`) |
| `montauk-rt/` | `no_std` runtime: syscalls, allocator, `print!`, argv, threads, futex `Mutex`, panic handler |
| `rusthello/` | `no_std` smoke test (`rusthello`, `rusthello panic`, `rusthello panic-thread`, `rusthello fault-thread`) |
The GCC driver links crt1/crti/crtbegin, the libc and libgcc from
`toolchain/sysroot`, the same as ports. After changing `programs/lib/libc`,
re-run `toolchain/build-montauk-toolchain.sh` so Rust programs pick it up.
## The std port (in the fork)
Syscall numbers and status codes are generated at build time by
`montauk-rt/build.rs` from `programs/include/Api/Syscall.hpp` and
`kernel/src/Common/StatusCodes.hpp`, so there is nothing to keep in sync by hand.
| Fork path | What |
|-----------|------|
| `compiler/rustc_target/src/spec/base/montauk.rs`, `targets/x86_64_unknown_montauk.rs` | Target: static non-PIE ET_EXEC, no red zone, local-exec TLS, panic=abort, linker `x86_64-montauk-gcc` |
| `library/std/src/sys/pal/montauk/` | Platform layer: `abi.rs` (syscall numbers, status codes, `SYSCALL` wrappers) |
| `library/std/src/sys/*/montauk.rs` | alloc, env, fs, io errors, net, paths, process, random, stdio, futex, thread, time |
| `library/std/src/os/montauk/` | `std::os::montauk::ffi::{OsStrExt, OsStringExt}` |
## Writing a program
std calls the kernel directly. The libc is used only where C code in the same
process must agree: the heap (`malloc`), the environment table (`setenv`),
`exit` (atexit handlers) and line-buffered terminal input (`read(0)`). The
syscall numbers in `abi.rs` are a hand-kept subset of `Api/Syscall.hpp`.
```rust
#![no_std]
#![no_main]
extern crate alloc;
## Platform behaviour
montauk_rt::entry!(main);
fn main() -> i32 {
montauk_rt::println!("hello from Rust");
0
}
```
## Limits (Phase 1)
- No `std`. No `std::fs`, `std::net`, `std::process` or `std::thread`; use
`montauk_rt` equivalents where they exist.
- `panic = "abort"`. A panic on any thread prints the message and kills the
whole process.
- Thread stacks are owned by the `JoinHandle`. An unjoined thread leaks its stack.
Phase 2 (a forked rustc at `~/dev/mtk/rust` with a native `target_os =
"montauk"` std) replaces the JSON spec with a built-in target.
- Errors: the raw OS error of an `io::Error` is the kernel status code
(positive, `montauk/statuscodes.h`); `ErrorKind` is mapped from it.
- Paths: `0:/os/x.elf` is absolute; joining onto it replaces the base. Search
paths (`PATH`, `env::split_paths`) are separated by `;`, not `:`.
- Files: kernel handles have no position, so `File` keeps its own cursor.
`OpenOptions::truncate` deletes and recreates the file; `set_len` can only
grow a file. No links, permissions are read-only, `fsync` is a no-op.
- Threads: stacks are mapped with `SYS_MMAP_ANON` (no guard page). A detached
thread is reaped (joined, stack freed) by the next spawn or join, so its
kernel slot does not leak. `available_parallelism()` is an error (the kernel
does not report a CPU count). `std::process::exit` from any thread ends the
process (libc `exit` -> `SYS_EXIT_GROUP`).
- Sync: `Mutex`, `RwLock`, `Condvar`, `Once` and thread parking use the
generic futex implementations over `SYS_FUTEX_WAIT`/`SYS_FUTEX_WAKE`.
- `thread_local!` uses native `#[thread_local]` (PT_TLS); destructors run when
a spawned thread exits (not on the main thread).
- Time: `Instant` has 1 ms resolution; `SystemTime` comes from the wall clock
(local time minus the timezone offset) with 1 s resolution.
- Processes: `Command` supports arguments, environment, `current_dir` (entered
in the parent for the duration of the spawn), `wait`, `try_wait` and `kill`.
Only inherited stdio is supported; piped/null stdio and `output()` return
`Unsupported`, because a redirected child's output stream disappears with
the child. A bare program name is searched in `PATH`, else `0:/os` and
`0:/games`; `.elf` may be omitted. `ExitStatus` shows abnormal ends as
"terminated by signal N".
- Net: IPv4 only. TCP connect/accept/read/write with read timeouts and
non-blocking mode, UDP send_to/recv_from/connect, DNS via `SYS_RESOLVE`.
No socket options, no `peek`, no `shutdown(Write)`, and accepted
connections do not know the peer's address.
- `panic = "abort"`: a panic on any thread prints the message and kills the
whole process (exit status "signal 6").
- `HashMap`'s random keys come from `SYS_GETRANDOM`, which is not a CSPRNG.
+4 -4
View File
@@ -1,9 +1,9 @@
//! Minimal `no_std` runtime for Rust programs on MontaukOS.
//!
//! Phase 1 of Rust support: `core` + `alloc` on a stock nightly with a
//! custom target spec. Programs link against the Montauk libc through the
//! x86_64-montauk GCC driver, so crt1.o owns `_start` (argv, environment,
//! .init_array) and calls the `main` that [`entry!`] defines.
//! For programs that use `core` + `alloc` without `std` (most programs can use
//! `std` directly; see ../README.md). Programs link against the Montauk libc
//! through the x86_64-montauk GCC driver, so crt1.o owns `_start` (argv,
//! environment, .init_array) and calls the `main` that [`entry!`] defines.
//!
//! ```ignore
//! #![no_std]
+7 -2
View File
@@ -1,3 +1,8 @@
# Phase 2 Rust support: the forked rustc with the built-in
# x86_64-unknown-montauk target and a native std. Build it in ~/dev/mtk/rust,
# then register it with:
# rustup toolchain link montauk ~/dev/mtk/rust/build/host/stage1
# The linked toolchain has no cargo of its own; rustup falls back to the
# nightly toolchain's cargo.
[toolchain]
channel = "nightly"
components = ["rust-src"]
channel = "montauk"
+1 -1
View File
@@ -1,4 +1,4 @@
//! rusthello - Phase 1 Rust smoke test for MontaukOS.
//! rusthello - `no_std` Rust smoke test for MontaukOS (montauk-rt).
//!
//! Exercises alloc collections, formatting, argv, a #[thread_local]
//! (PT_TLS + per-thread %fs blocks), threads, and the futex mutex.
+4
View File
@@ -0,0 +1,4 @@
[package]
name = "ruststd"
version = "0.1.0"
edition = "2024"
+477
View File
@@ -0,0 +1,477 @@
//! ruststd - Phase 2 Rust smoke test for MontaukOS: the native `std`.
//!
//! Runs a set of PASS/FAIL checks over the std platform layer: collections and
//! formatting, args and environment, the filesystem, threads (joined, scoped
//! and detached), the futex-backed sync primitives, thread_local! destructors,
//! time, and child processes (exit codes, arguments, environment, working
//! directory, std::process::exit from a worker thread, panics).
//!
//! `ruststd net <host> [port]` additionally fetches `http://<host>/` over TCP.
//! The `child-*` arguments are the child-process halves of the checks.
use std::collections::{BTreeMap, HashMap};
use std::fs::{self, File, OpenOptions};
use std::io::{BufRead, BufReader, Read, Seek, SeekFrom, Write};
use std::path::{Path, PathBuf};
use std::process::{Command, ExitCode};
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, Barrier, Condvar, Mutex, OnceLock, RwLock, mpsc};
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
use std::{env, thread};
static FAILURES: AtomicUsize = AtomicUsize::new(0);
fn check(ok: bool, name: &str) {
println!("{} {}", if ok { "PASS" } else { "FAIL" }, name);
if !ok {
FAILURES.fetch_add(1, Ordering::Relaxed);
}
}
fn main() -> ExitCode {
let args: Vec<String> = env::args().collect();
match args.get(1).map(String::as_str) {
Some(mode) if mode.starts_with("child-") => return ExitCode::from(child(mode, &args)),
Some("net") => return net(&args),
_ => {}
}
println!("ruststd: Rust std on MontaukOS");
println!("argv = {args:?}");
basics();
environment();
filesystem();
threads();
sync();
thread_locals();
time();
processes(&args);
let failures = FAILURES.load(Ordering::Relaxed);
if failures == 0 {
println!("ruststd: all passed");
ExitCode::SUCCESS
} else {
println!("ruststd: {failures} FAILED");
ExitCode::FAILURE
}
}
// ====================================================================
// Child-process halves
// ====================================================================
fn child(mode: &str, args: &[String]) -> u8 {
match mode {
"child-exit" => 7,
"child-args" => {
let ok = args[2..] == ["with space", "", "it's", "say \"hi\"", "plain"];
if ok { 8 } else { 1 }
}
"child-env" => {
if env::var("RUSTSTD_VAR").as_deref() == Ok("hello there") { 9 } else { 1 }
}
"child-cwd" => {
let cwd = env::current_dir().unwrap_or_default();
if cwd.ends_with("os") { 10 } else { 1 }
}
"child-exit-thread" => {
// std::process::exit from a worker must end the whole process.
let h = thread::spawn(|| std::process::exit(11));
let _ = h.join();
thread::sleep(Duration::from_secs(5));
1
}
"child-panic-thread" => {
let h = thread::spawn(|| panic!("requested panic on a worker thread"));
let _ = h.join();
thread::sleep(Duration::from_secs(5));
1
}
_ => 2,
}
}
// ====================================================================
// Checks
// ====================================================================
fn basics() {
let mut map = HashMap::new();
for (i, w) in "Montauk speaks Rust with std".split(' ').enumerate() {
map.insert(w.to_string(), i);
}
check(map["Rust"] == 2 && map.len() == 5, "HashMap (RandomState keys from SYS_GETRANDOM)");
let sorted: BTreeMap<_, _> = map.iter().map(|(k, v)| (*v, k.clone())).collect();
check(sorted.values().next().map(String::as_str) == Some("Montauk"), "BTreeMap");
check(
format!("{:>8.3}|{:e}|{:#06x}", std::f64::consts::PI, 1234.5f64, 255) == " 3.142|1.2345e3|0x00ff",
"float and integer formatting",
);
let mut out = std::io::stdout().lock();
check(writeln!(out, " (stdout lock + writeln!)").is_ok(), "io::stdout().lock()");
eprintln!(" (eprintln! goes to the terminal too)");
}
fn environment() {
let exe = env::current_exe();
check(exe.as_ref().is_ok_and(|p| p.is_absolute()), "env::current_exe() is absolute");
println!(" current_exe = {exe:?}");
let cwd = env::current_dir();
check(cwd.is_ok(), "env::current_dir()");
unsafe { env::set_var("RUSTSTD_TEST", "value=with=equals") };
check(env::var("RUSTSTD_TEST").as_deref() == Ok("value=with=equals"), "set_var / var");
check(env::vars().any(|(k, v)| k == "RUSTSTD_TEST" && v == "value=with=equals"), "env::vars() lists it");
unsafe { env::remove_var("RUSTSTD_TEST") };
check(env::var_os("RUSTSTD_TEST").is_none(), "remove_var");
let joined = env::join_paths(["0:/os", "0:/games"]).unwrap();
let split: Vec<PathBuf> = env::split_paths(&joined).collect();
check(joined == "0:/os;0:/games" && split.len() == 2, "join_paths / split_paths use ';'");
check(Path::new("0:/os/shell.elf").is_absolute(), "drive paths are absolute");
check(Path::new("0:/os").join("0:/apps") == Path::new("0:/apps"), "joining a drive path replaces");
check(Path::new("0:/os/shell.elf").parent() == Some(Path::new("0:/os")), "Path::parent");
}
fn filesystem() {
let base = env::temp_dir().join(format!("ruststd-{}", std::process::id()));
let _ = fs::remove_dir_all(&base);
check(fs::create_dir_all(base.join("sub/deeper")).is_ok(), "create_dir_all");
check(fs::create_dir(&base).is_err_and(|e| e.kind() == std::io::ErrorKind::AlreadyExists),
"create_dir on an existing dir fails with AlreadyExists");
let file = base.join("hello.txt");
check(fs::write(&file, b"line one\nline two\n").is_ok(), "fs::write");
check(fs::read_to_string(&file).ok().as_deref() == Some("line one\nline two\n"), "fs::read_to_string");
let appended = OpenOptions::new().append(true).open(&file).and_then(|mut f| f.write_all(b"line three\n"));
check(appended.is_ok(), "append");
let lines: Vec<String> = BufReader::new(File::open(&file).unwrap()).lines().map(Result::unwrap).collect();
check(lines == ["line one", "line two", "line three"], "BufReader::lines after append");
let meta = fs::metadata(&file);
check(meta.as_ref().is_ok_and(|m| m.is_file() && m.len() == 29), "metadata: is_file, len");
if let Ok(m) = &meta {
println!(" modified = {:?}", m.modified().ok().and_then(|t| t.duration_since(UNIX_EPOCH).ok()));
}
check(fs::metadata(&base).is_ok_and(|m| m.is_dir()), "metadata: is_dir");
// Seek + overwrite in place.
{
let mut f = OpenOptions::new().read(true).write(true).open(&file).unwrap();
f.seek(SeekFrom::Start(5)).unwrap();
f.write_all(b"ONE").unwrap();
f.seek(SeekFrom::End(-6)).unwrap();
let mut tail = String::new();
f.read_to_string(&mut tail).unwrap();
check(tail == "three\n", "seek from end + read");
}
check(fs::read_to_string(&file).unwrap().starts_with("line ONE\n"), "seek + overwrite");
// Truncate on open.
fs::write(&file, b"short").unwrap();
check(fs::read(&file).ok().as_deref() == Some(&b"short"[..]), "File::create truncates");
check(fs::copy(&file, base.join("sub/copy.txt")).ok() == Some(5), "fs::copy");
check(fs::rename(base.join("sub/copy.txt"), base.join("sub/moved.txt")).is_ok(), "fs::rename");
let mut names: Vec<String> = fs::read_dir(&base)
.map(|rd| rd.filter_map(|e| e.ok()).map(|e| e.file_name().to_string_lossy().into_owned()).collect())
.unwrap_or_default();
names.sort();
check(names == ["hello.txt", "sub"], "read_dir lists entries");
let sub_types: Vec<(String, bool)> = fs::read_dir(base.join("sub"))
.unwrap()
.map(|e| e.unwrap())
.map(|e| (e.file_name().to_string_lossy().into_owned(), e.file_type().unwrap().is_dir()))
.collect();
check(sub_types.contains(&("deeper".into(), true)) && sub_types.contains(&("moved.txt".into(), false)),
"DirEntry::file_type");
check(File::open(base.join("missing")).is_err_and(|e| e.kind() == std::io::ErrorKind::NotFound),
"open missing file -> NotFound");
check(fs::remove_file(&file).is_ok() && !file.exists(), "remove_file");
check(fs::remove_dir_all(&base).is_ok() && !base.exists(), "remove_dir_all");
}
fn threads() {
let handles: Vec<_> = (0..4).map(|i| thread::spawn(move || i * 10)).collect();
let results: Vec<i32> = handles.into_iter().map(|h| h.join().unwrap()).collect();
check(results == [0, 10, 20, 30], "spawn + join return values");
let named = thread::Builder::new().name("worker".into()).spawn(|| {
thread::current().name().map(str::to_owned)
});
check(named.unwrap().join().unwrap().as_deref() == Some("worker"), "named thread");
let mut data = vec![1, 2, 3, 4];
thread::scope(|s| {
for x in data.iter_mut() {
s.spawn(move || *x *= 2);
}
});
check(data == [2, 4, 6, 8], "thread::scope borrows");
// Detached threads hold a kernel slot until reaped; 300 of them would
// exhaust the 256-slot table if they were not.
let done = Arc::new(AtomicUsize::new(0));
for _ in 0..15 {
for _ in 0..20 {
let done = done.clone();
drop(thread::spawn(move || {
done.fetch_add(1, Ordering::Relaxed);
}));
}
thread::sleep(Duration::from_millis(20));
}
let deadline = Instant::now() + Duration::from_secs(5);
while done.load(Ordering::Relaxed) < 300 && Instant::now() < deadline {
thread::sleep(Duration::from_millis(10));
}
check(done.load(Ordering::Relaxed) == 300, "300 detached threads are reaped");
let big = thread::Builder::new().stack_size(4 << 20).spawn(|| {
let buf = [7u8; 2 << 20];
buf.iter().map(|&b| b as usize).sum::<usize>()
});
check(big.unwrap().join().unwrap() == 7 * (2 << 20), "4 MiB thread stack");
}
fn sync() {
const THREADS: usize = 4;
const ITERS: usize = 10_000;
let counter = Arc::new(Mutex::new(0usize));
let handles: Vec<_> = (0..THREADS)
.map(|_| {
let counter = counter.clone();
thread::spawn(move || {
for i in 0..ITERS {
let mut g = counter.lock().unwrap();
if i % 1000 == 0 {
thread::yield_now();
}
*g += 1;
}
})
})
.collect();
handles.into_iter().for_each(|h| h.join().unwrap());
check(*counter.lock().unwrap() == THREADS * ITERS, "Mutex keeps the counter exact");
let pair = Arc::new((Mutex::new(false), Condvar::new()));
let pair2 = pair.clone();
let waiter = thread::spawn(move || {
let (lock, cvar) = &*pair2;
let guard = cvar.wait_while(lock.lock().unwrap(), |ready| !*ready).unwrap();
*guard
});
thread::sleep(Duration::from_millis(20));
*pair.0.lock().unwrap() = true;
pair.1.notify_all();
check(waiter.join().unwrap(), "Condvar wait/notify");
let (lock, cvar) = (Mutex::new(()), Condvar::new());
let t0 = Instant::now();
let (_g, res) = cvar.wait_timeout(lock.lock().unwrap(), Duration::from_millis(30)).unwrap();
check(res.timed_out() && t0.elapsed() >= Duration::from_millis(30), "Condvar wait_timeout");
let rw = Arc::new(RwLock::new(vec![1]));
let readers: Vec<_> = (0..3)
.map(|_| {
let rw = rw.clone();
thread::spawn(move || rw.read().unwrap().len())
})
.collect();
rw.write().unwrap().push(2);
let lens: Vec<usize> = readers.into_iter().map(|h| h.join().unwrap()).collect();
check(lens.iter().all(|&l| l == 1 || l == 2) && rw.read().unwrap().len() == 2, "RwLock");
static ONCE: OnceLock<usize> = OnceLock::new();
let inits = Arc::new(AtomicUsize::new(0));
let hs: Vec<_> = (0..4)
.map(|_| {
let inits = inits.clone();
thread::spawn(move || *ONCE.get_or_init(|| inits.fetch_add(1, Ordering::SeqCst) + 42))
})
.collect();
let vals: Vec<usize> = hs.into_iter().map(|h| h.join().unwrap()).collect();
check(vals.iter().all(|&v| v == 42) && inits.load(Ordering::SeqCst) == 1, "OnceLock initializes once");
let (tx, rx) = mpsc::channel();
for t in 0..3 {
let tx = tx.clone();
thread::spawn(move || {
for i in 0..100 {
tx.send(t * 1000 + i).unwrap();
}
});
}
drop(tx);
let received: Vec<i32> = rx.iter().collect();
check(received.len() == 300, "mpsc channel with 3 senders");
let barrier = Arc::new(Barrier::new(3));
let hs: Vec<_> = (0..3)
.map(|_| {
let b = barrier.clone();
thread::spawn(move || b.wait().is_leader())
})
.collect();
let leaders = hs.into_iter().map(|h| h.join().unwrap()).filter(|&l| l).count();
check(leaders == 1, "Barrier");
let parked = thread::spawn(|| {
thread::park();
true
});
thread::sleep(Duration::from_millis(10));
parked.thread().unpark();
check(parked.join().unwrap(), "park / unpark");
}
fn thread_locals() {
static DROPS: AtomicUsize = AtomicUsize::new(0);
struct Tracker(u32);
impl Drop for Tracker {
fn drop(&mut self) {
DROPS.fetch_add(1, Ordering::SeqCst);
}
}
thread_local! {
static COUNTER: std::cell::Cell<u32> = const { std::cell::Cell::new(5) };
static TRACKER: Tracker = Tracker(1);
}
COUNTER.with(|c| c.set(100));
let seen = thread::spawn(|| {
TRACKER.with(|t| assert_eq!(t.0, 1));
COUNTER.with(|c| c.get())
})
.join()
.unwrap();
check(seen == 5 && COUNTER.with(|c| c.get()) == 100, "thread_local! is per thread");
check(DROPS.load(Ordering::SeqCst) == 1, "thread_local! destructor runs at thread exit");
}
fn time() {
let t0 = Instant::now();
thread::sleep(Duration::from_millis(50));
let dt = t0.elapsed();
check(dt >= Duration::from_millis(50) && dt < Duration::from_secs(2), "Instant + sleep(50 ms)");
let now = SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(0);
// 2025-01-01T00:00:00Z
check(now > 1_735_689_600, "SystemTime::now() is after 2025");
println!(" unix time = {now}");
}
fn processes(args: &[String]) {
let Ok(me) = env::current_exe() else {
check(false, "current_exe for child processes");
return;
};
let _ = args;
let run = |cmd: &mut Command| cmd.status().map(|s| s.code());
check(run(Command::new(&me).arg("child-exit")).ok() == Some(Some(7)), "child exit code");
check(
run(Command::new(&me).args(["child-args", "with space", "", "it's", "say \"hi\"", "plain"])).ok() == Some(Some(8)),
"child arguments with spaces and quotes",
);
check(
run(Command::new(&me).arg("child-env").env("RUSTSTD_VAR", "hello there")).ok() == Some(Some(9)),
"child environment",
);
check(
run(Command::new(&me).arg("child-cwd").current_dir("0:/os")).ok() == Some(Some(10)),
"child working directory",
);
check(
run(Command::new(&me).arg("child-exit-thread")).ok() == Some(Some(11)),
"process::exit from a worker thread ends the process",
);
let panicked = Command::new(&me).arg("child-panic-thread").status();
check(
panicked.as_ref().is_ok_and(|s| s.code().is_none() && s.to_string().contains("signal 6")),
"panic on a worker thread aborts the process",
);
if let Ok(s) = panicked {
println!(" panicking child: {s}");
}
let mut child = Command::new(&me).arg("child-exit").spawn().unwrap();
let mut polled = None;
for _ in 0..200 {
if let Ok(Some(s)) = child.try_wait() {
polled = Some(s);
break;
}
thread::sleep(Duration::from_millis(10));
}
check(polled.and_then(|s| s.code()) == Some(7), "Child::try_wait");
check(Command::new("no-such-program").status().is_err(), "spawning a missing program fails");
check(
Command::new(&me).arg("child-exit").output().is_err_and(|e| e.kind() == std::io::ErrorKind::Unsupported),
"output() reports Unsupported (no stdio redirection yet)",
);
}
// ====================================================================
// Network (optional: needs a link)
// ====================================================================
fn net(args: &[String]) -> ExitCode {
use std::net::{TcpStream, ToSocketAddrs};
let Some(host) = args.get(2) else {
println!("usage: ruststd net <host> [port]");
return ExitCode::FAILURE;
};
let port: u16 = args.get(3).and_then(|p| p.parse().ok()).unwrap_or(80);
let addrs: Vec<_> = match (host.as_str(), port).to_socket_addrs() {
Ok(a) => a.collect(),
Err(e) => {
println!("resolve {host}: {e}");
return ExitCode::FAILURE;
}
};
println!("{host} -> {addrs:?}");
let mut stream = match TcpStream::connect((host.as_str(), port)) {
Ok(s) => s,
Err(e) => {
println!("connect: {e}");
return ExitCode::FAILURE;
}
};
stream.set_read_timeout(Some(Duration::from_secs(10))).unwrap();
let request = format!("GET / HTTP/1.0\r\nHost: {host}\r\nConnection: close\r\n\r\n");
if let Err(e) = stream.write_all(request.as_bytes()) {
println!("send: {e}");
return ExitCode::FAILURE;
}
let mut response = Vec::new();
match stream.read_to_end(&mut response) {
Ok(n) => {
let text = String::from_utf8_lossy(&response);
println!("received {n} bytes; status line: {:?}", text.lines().next().unwrap_or(""));
ExitCode::SUCCESS
}
Err(e) => {
println!("recv: {e}");
ExitCode::FAILURE
}
}
}
-32
View File
@@ -1,32 +0,0 @@
{
"arch": "x86_64",
"cpu": "x86-64",
"data-layout": "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128",
"llvm-target": "x86_64-unknown-none-elf",
"os": "montauk",
"vendor": "unknown",
"target-pointer-width": 64,
"max-atomic-width": 64,
"executables": true,
"linker-flavor": "gnu-cc",
"linker": "x86_64-montauk-gcc",
"no-default-libraries": false,
"crt-objects-fallback": "false",
"relocation-model": "static",
"code-model": "small",
"position-independent-executables": false,
"static-position-independent-executables": false,
"relro-level": "off",
"disable-redzone": true,
"has-thread-local": true,
"tls-model": "local-exec",
"panic-strategy": "abort",
"plt-by-default": false,
"stack-probes": { "kind": "inline" },
"metadata": {
"description": "MontaukOS userspace (static ET_EXEC, Montauk libc)",
"host_tools": false,
"std": false,
"tier": 3
}
}