feat: Rust std target + ruststd tests; SYS_EXIT_GROUP; fix mkdir/create under missing parent
This commit is contained in:
1 parent
9e66c3b249
commit
e87bd358a6
20 files changed
+657
-97
No files matched your search
@@ -12,4 +12,4 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#define MONTAUK_BUILD_NUMBER 1
|
#define MONTAUK_BUILD_NUMBER 3
|
||||||
@@ -27,6 +27,23 @@ namespace montauk::abi {
|
|||||||
return required == 0 || Sched::HasCapability(required);
|
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) {
|
static int Sys_Open(const char* path) {
|
||||||
char resolved[256];
|
char resolved[256];
|
||||||
if (!ResolveProcessPath(path, resolved, sizeof(resolved))) return InvalidArgument;
|
if (!ResolveProcessPath(path, resolved, sizeof(resolved))) return InvalidArgument;
|
||||||
@@ -117,6 +134,7 @@ namespace montauk::abi {
|
|||||||
char resolved[256];
|
char resolved[256];
|
||||||
if (!ResolveProcessPath(path, resolved, sizeof(resolved))) return InvalidArgument;
|
if (!ResolveProcessPath(path, resolved, sizeof(resolved))) return InvalidArgument;
|
||||||
if (!CanModifyFilePath(resolved)) return SystemIntegrityError;
|
if (!CanModifyFilePath(resolved)) return SystemIntegrityError;
|
||||||
|
if (!ParentDirExists(resolved)) return NoSuchPath;
|
||||||
return Ipc::CreateFileHandle(resolved);
|
return Ipc::CreateFileHandle(resolved);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,6 +189,7 @@ namespace montauk::abi {
|
|||||||
char resolved[256];
|
char resolved[256];
|
||||||
if (!ResolveProcessPath(path, resolved, sizeof(resolved))) return InvalidArgument;
|
if (!ResolveProcessPath(path, resolved, sizeof(resolved))) return InvalidArgument;
|
||||||
if (!CanModifyFilePath(resolved)) return SystemIntegrityError;
|
if (!CanModifyFilePath(resolved)) return SystemIntegrityError;
|
||||||
|
if (!ParentDirExists(resolved)) return NoSuchPath;
|
||||||
return Fs::Vfs::VfsMkdir(resolved);
|
return Fs::Vfs::VfsMkdir(resolved);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -228,6 +228,14 @@ namespace montauk::abi {
|
|||||||
Sched::ExitProcess();
|
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) {
|
static int Sys_Kill(int pid) {
|
||||||
// A process may always kill itself (kill(getpid(), ...)); waiters
|
// A process may always kill itself (kill(getpid(), ...)); waiters
|
||||||
// see TerminatedKilled. abort() uses SYS_ABORT instead.
|
// see TerminatedKilled. abort() uses SYS_ABORT instead.
|
||||||
|
|||||||
@@ -403,6 +403,9 @@ namespace montauk::abi {
|
|||||||
case SYS_ABORT:
|
case SYS_ABORT:
|
||||||
Sys_Abort();
|
Sys_Abort();
|
||||||
return Success;
|
return Success;
|
||||||
|
case SYS_EXIT_GROUP:
|
||||||
|
Sys_ExitGroup((int)frame->arg1);
|
||||||
|
return Success;
|
||||||
case SYS_SETSESSION:
|
case SYS_SETSESSION:
|
||||||
return (int64_t)Sys_SetSession();
|
return (int64_t)Sys_SetSession();
|
||||||
case SYS_KILLSESSION:
|
case SYS_KILLSESSION:
|
||||||
|
|||||||
@@ -380,6 +380,12 @@ namespace montauk::abi {
|
|||||||
sibling thread ends only that thread, so abort() needs its own call. */
|
sibling thread ends only that thread, so abort() needs its own call. */
|
||||||
static constexpr uint64_t SYS_ABORT = 198; // () -> does not return
|
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
|
/* Kernel-owned process capabilities. User identities may namespace
|
||||||
per-user resources, but never participate in authorization decisions. */
|
per-user resources, but never participate in authorization decisions. */
|
||||||
static constexpr uint64_t CAP_PROCESS_ADMIN = 1ULL << 0;
|
static constexpr uint64_t CAP_PROCESS_ADMIN = 1ULL << 0;
|
||||||
|
|||||||
@@ -89,6 +89,20 @@ namespace Fs::Ramdisk {
|
|||||||
return -1;
|
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) {
|
void Initialize(void* moduleData, uint64_t moduleSize) {
|
||||||
Kt::KernelLogStream(Kt::OK, "Ramdisk") << "Parsing USTAR archive (" << moduleSize << " bytes)";
|
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
|
// Create new file entry
|
||||||
FileEntry& entry = fileTable[fileCount];
|
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)
|
// Create directory entry (stored with trailing slash for tar convention)
|
||||||
FileEntry& entry = fileTable[fileCount];
|
FileEntry& entry = fileTable[fileCount];
|
||||||
|
|
||||||
|
|||||||
+18
-8
@@ -320,22 +320,32 @@ mandelbrot: libc
|
|||||||
screenshot: libc libjpegwrite libloader dialogs
|
screenshot: libc libjpegwrite libloader dialogs
|
||||||
$(MAKE) -C src/screenshot
|
$(MAKE) -C src/screenshot
|
||||||
|
|
||||||
# Rust programs (programs/rust, Phase 1: no_std + alloc via -Zbuild-std).
|
# Rust programs (programs/rust): std and no_std programs for the built-in
|
||||||
# Optional: skipped with a notice when no rustup-managed cargo with the
|
# x86_64-unknown-montauk target of the forked rustc (Phase 2), registered with
|
||||||
# pinned nightly is installed, so the default build never requires Rust.
|
# rustup as the 'montauk' toolchain (see programs/rust/README.md). Optional:
|
||||||
# RUSTUP_AUTO_INSTALL=0 keeps 'make' from downloading a toolchain itself.
|
# 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:
|
# 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))
|
CARGO ?= $(firstword $(wildcard $(HOME)/.cargo/bin/cargo) $(shell command -v cargo 2>/dev/null))
|
||||||
RUST_DIR := rust
|
RUST_DIR := rust
|
||||||
RUST_OUT := $(RUST_DIR)/target/x86_64-unknown-montauk/release
|
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:
|
rust:
|
||||||
@if [ -z "$(CARGO)" ] || ! (cd $(RUST_DIR) && RUSTUP_AUTO_INSTALL=0 $(CARGO) --version >/dev/null 2>&1); then \
|
@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 nightly cargo found - skipping Rust programs (see programs/rust/README.md)"; \
|
echo "rust: no 'montauk' rustup toolchain - skipping Rust programs (see programs/rust/README.md)"; \
|
||||||
else \
|
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; \
|
(cd $(RUST_DIR) && RUSTUP_AUTO_INSTALL=0 $(CARGO) build --release) || exit 1; \
|
||||||
|
touch $(RUST_OUT)/.link-stamp; \
|
||||||
mkdir -p $(BINDIR)/os; \
|
mkdir -p $(BINDIR)/os; \
|
||||||
for p in $(RUST_PROGRAMS); do cp $(RUST_OUT)/$$p $(BINDIR)/os/$$p.elf || exit 1; done; \
|
for p in $(RUST_PROGRAMS); do cp $(RUST_OUT)/$$p $(BINDIR)/os/$$p.elf || exit 1; done; \
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -293,6 +293,12 @@ namespace montauk::abi {
|
|||||||
sibling thread ends only that thread, so abort() needs its own call. */
|
sibling thread ends only that thread, so abort() needs its own call. */
|
||||||
static constexpr uint64_t SYS_ABORT = 198; // () -> does not return
|
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
|
/* Kernel-owned process capabilities. User identities may namespace
|
||||||
per-user resources, but never participate in authorization decisions. */
|
per-user resources, but never participate in authorization decisions. */
|
||||||
static constexpr uint64_t CAP_PROCESS_ADMIN = 1ULL << 0;
|
static constexpr uint64_t CAP_PROCESS_ADMIN = 1ULL << 0;
|
||||||
|
|||||||
@@ -222,6 +222,7 @@ extern "C" {
|
|||||||
#define MTK_SYS_FUTEX_WAIT 196
|
#define MTK_SYS_FUTEX_WAIT 196
|
||||||
#define MTK_SYS_FUTEX_WAKE 197
|
#define MTK_SYS_FUTEX_WAKE 197
|
||||||
#define MTK_SYS_ABORT 198
|
#define MTK_SYS_ABORT 198
|
||||||
|
#define MTK_SYS_EXIT_GROUP 199
|
||||||
/* @SYSCALLS-END */
|
/* @SYSCALLS-END */
|
||||||
|
|
||||||
/* Syscall status codes (montauk/statuscodes.h for C++). */
|
/* Syscall status codes (montauk/statuscodes.h for C++). */
|
||||||
|
|||||||
@@ -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_SETENVIRON 172
|
||||||
#define SYS_SPAWN_ENV 173
|
#define SYS_SPAWN_ENV 173
|
||||||
#define SYS_ABORT 198
|
#define SYS_ABORT 198
|
||||||
|
#define SYS_EXIT_GROUP 199
|
||||||
|
|
||||||
/* ========================================================================
|
/* ========================================================================
|
||||||
errno
|
errno
|
||||||
@@ -1240,7 +1241,8 @@ int atexit(void (*func)(void)) {
|
|||||||
void exit(int status) {
|
void exit(int status) {
|
||||||
for (int i = _atexit_count - 1; i >= 0; i--)
|
for (int i = _atexit_count - 1; i >= 0; i--)
|
||||||
_atexit_funcs[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();
|
__builtin_unreachable();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4066,7 +4068,7 @@ pid_t waitpid(pid_t pid, int *status, int options) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _exit(int status) {
|
void _exit(int status) {
|
||||||
_mtk_syscall1(SYS_EXIT, (long)status);
|
_mtk_syscall1(SYS_EXIT_GROUP, (long)status);
|
||||||
__builtin_unreachable();
|
__builtin_unreachable();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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/).
|
# Paths below are relative to programs/rust/ (the parent of .cargo/).
|
||||||
|
|
||||||
[build]
|
[build]
|
||||||
target = "x86_64-unknown-montauk.json"
|
target = "x86_64-unknown-montauk"
|
||||||
|
|
||||||
[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
|
# The montauk GCC driver supplies crt1/crti/crtbegin, -lc and libgcc, and
|
||||||
|
|||||||
Generated
+4
@@ -12,3 +12,7 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"montauk-rt",
|
"montauk-rt",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ruststd"
|
||||||
|
version = "0.1.0"
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
[workspace]
|
[workspace]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
members = ["montauk-rt", "rusthello"]
|
members = ["montauk-rt", "rusthello", "ruststd"]
|
||||||
|
|
||||||
[profile.dev]
|
[profile.dev]
|
||||||
panic = "abort"
|
panic = "abort"
|
||||||
|
|||||||
+74
-39
@@ -1,18 +1,31 @@
|
|||||||
# Rust on MontaukOS
|
# Rust on MontaukOS
|
||||||
|
|
||||||
Phase 1: `no_std` + `alloc` programs, cross-compiled from Linux with a
|
Rust programs are built for `x86_64-unknown-montauk`, a built-in target of a
|
||||||
stock nightly toolchain and a custom target spec. No compiler fork.
|
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)
|
## Setup (once)
|
||||||
|
|
||||||
```bash
|
```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
|
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.
|
The stage1 build takes about five minutes (it downloads CI LLVM). After
|
||||||
`make programs` builds the Rust programs when that toolchain is present
|
changing only `library/`, rebuild with
|
||||||
and prints a notice and skips them otherwise. It never downloads a toolchain
|
`./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`).
|
(`RUSTUP_AUTO_INSTALL=0`).
|
||||||
|
|
||||||
## Building
|
## 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/bin/os/<name>.elf`. Add new binaries to `RUST_PROGRAMS` in
|
||||||
`programs/GNUmakefile` and to the workspace `members`.
|
`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
|
## Layout
|
||||||
|
|
||||||
| Path | Purpose |
|
| 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` | target = `x86_64-unknown-montauk`, linker = `x86_64-montauk-gcc` |
|
||||||
| `.cargo/config.toml` | `-Zbuild-std=core,alloc`, linker = `x86_64-montauk-gcc` |
|
| `rust-toolchain.toml` | the linked `montauk` toolchain |
|
||||||
| `montauk-rt/` | Runtime: syscalls, allocator, `print!`, argv, threads, futex `Mutex`, panic handler |
|
| `ruststd/` | `std` smoke test (`ruststd`, `ruststd net <host> [port]`) |
|
||||||
| `rusthello/` | Smoke test (`rusthello`, `rusthello panic`, `rusthello panic-thread`) |
|
| `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
|
## The std port (in the fork)
|
||||||
`toolchain/sysroot`, the same as ports. After changing `programs/lib/libc`,
|
|
||||||
re-run `toolchain/build-montauk-toolchain.sh` so Rust programs pick it up.
|
|
||||||
|
|
||||||
Syscall numbers and status codes are generated at build time by
|
| Fork path | What |
|
||||||
`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.
|
| `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
|
## Platform behaviour
|
||||||
#![no_std]
|
|
||||||
#![no_main]
|
|
||||||
extern crate alloc;
|
|
||||||
|
|
||||||
montauk_rt::entry!(main);
|
- Errors: the raw OS error of an `io::Error` is the kernel status code
|
||||||
|
(positive, `montauk/statuscodes.h`); `ErrorKind` is mapped from it.
|
||||||
fn main() -> i32 {
|
- Paths: `0:/os/x.elf` is absolute; joining onto it replaces the base. Search
|
||||||
montauk_rt::println!("hello from Rust");
|
paths (`PATH`, `env::split_paths`) are separated by `;`, not `:`.
|
||||||
0
|
- 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
|
||||||
## Limits (Phase 1)
|
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
|
||||||
- No `std`. No `std::fs`, `std::net`, `std::process` or `std::thread`; use
|
does not report a CPU count). `std::process::exit` from any thread ends the
|
||||||
`montauk_rt` equivalents where they exist.
|
process (libc `exit` -> `SYS_EXIT_GROUP`).
|
||||||
- `panic = "abort"`. A panic on any thread prints the message and kills the
|
- Sync: `Mutex`, `RwLock`, `Condvar`, `Once` and thread parking use the
|
||||||
whole process.
|
generic futex implementations over `SYS_FUTEX_WAIT`/`SYS_FUTEX_WAKE`.
|
||||||
- Thread stacks are owned by the `JoinHandle`. An unjoined thread leaks its stack.
|
- `thread_local!` uses native `#[thread_local]` (PT_TLS); destructors run when
|
||||||
|
a spawned thread exits (not on the main thread).
|
||||||
Phase 2 (a forked rustc at `~/dev/mtk/rust` with a native `target_os =
|
- Time: `Instant` has 1 ms resolution; `SystemTime` comes from the wall clock
|
||||||
"montauk"` std) replaces the JSON spec with a built-in target.
|
(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.
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
//! Minimal `no_std` runtime for Rust programs on MontaukOS.
|
//! Minimal `no_std` runtime for Rust programs on MontaukOS.
|
||||||
//!
|
//!
|
||||||
//! Phase 1 of Rust support: `core` + `alloc` on a stock nightly with a
|
//! For programs that use `core` + `alloc` without `std` (most programs can use
|
||||||
//! custom target spec. Programs link against the Montauk libc through the
|
//! `std` directly; see ../README.md). Programs link against the Montauk libc
|
||||||
//! x86_64-montauk GCC driver, so crt1.o owns `_start` (argv, environment,
|
//! through the x86_64-montauk GCC driver, so crt1.o owns `_start` (argv,
|
||||||
//! .init_array) and calls the `main` that [`entry!`] defines.
|
//! environment, .init_array) and calls the `main` that [`entry!`] defines.
|
||||||
//!
|
//!
|
||||||
//! ```ignore
|
//! ```ignore
|
||||||
//! #![no_std]
|
//! #![no_std]
|
||||||
|
|||||||
@@ -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]
|
[toolchain]
|
||||||
channel = "nightly"
|
channel = "montauk"
|
||||||
components = ["rust-src"]
|
|
||||||
@@ -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]
|
//! Exercises alloc collections, formatting, argv, a #[thread_local]
|
||||||
//! (PT_TLS + per-thread %fs blocks), threads, and the futex mutex.
|
//! (PT_TLS + per-thread %fs blocks), threads, and the futex mutex.
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
[package]
|
||||||
|
name = "ruststd"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in new issue
Block a user