/* * thread.h * Userspace threading primitives for MontaukOS * Copyright (c) 2026 Daniel Hammer * * Threads share their parent's address space, IPC handles, cwd, and * heap allocator. The caller owns the thread stack: thread_spawn * allocates one out of the user heap and the kernel uses it verbatim. * * Lifetimes: * - thread_spawn returns a positive TID on success. * - thread_join blocks until the target TID exits, frees its kernel * stack, then returns its exit code via *out_code. * - thread_exit terminates only the current thread. If the main thread * calls exit() (SYS_EXIT) the whole process tears down, killing any * surviving sibling threads. * - thread_self returns the calling thread's TID. */ #pragma once #include #include #include #include namespace montauk { // Default per-thread stack size (64 KiB). Enough for typical app work; // matches the main thread's 32 KiB lower bound with headroom for // TrueType rendering call chains. static constexpr uint64_t DEFAULT_THREAD_STACK_BYTES = 64 * 1024; using ThreadEntry = int (*)(void* arg); // Terminate the calling thread. Never returns. If called by the main // thread this is equivalent to exit() (the whole process exits). [[noreturn]] inline void thread_exit(int code = 0) { syscall1(montauk::abi::SYS_THREAD_EXIT, (uint64_t)code); __builtin_unreachable(); } namespace detail { struct ThreadCtx { ThreadEntry user_entry; void* user_arg; void* stack_base; }; // Userspace trampoline: bridges from the raw entry the kernel jumps // to into the typed entry, then funnels into SYS_THREAD_EXIT. We // route the exit through libc rather than relying on a kernel-side // exit stub on the user stack -- the kernel never writes to user // memory on this path. // // The thread's stack itself is intentionally not freed here: we are // still running on it. It is reclaimed when the process exits, or // the joiner may free it explicitly after thread_join. [[noreturn]] inline void thread_trampoline(detail::ThreadCtx* ctx) { int code = ctx->user_entry(ctx->user_arg); montauk::mfree(ctx); thread_exit(code); } } // Spawn a new thread that begins executing `entry(arg)`. Returns the // new TID on success, or -1 on failure. The thread's stack is // allocated from the user heap; it is leaked on thread exit (the // thread itself cannot free the stack it is running on). The kernel // reclaims it on process exit. Callers that need to spawn many short- // lived threads should pool stacks themselves. inline int thread_spawn(ThreadEntry entry, void* arg, uint64_t stack_bytes = 0) { if (entry == nullptr) return -1; if (stack_bytes == 0) stack_bytes = DEFAULT_THREAD_STACK_BYTES; stack_bytes = (stack_bytes + 15) & ~15ULL; void* stack = montauk::malloc(stack_bytes); if (stack == nullptr) return -1; auto* ctx = (detail::ThreadCtx*)montauk::malloc(sizeof(detail::ThreadCtx)); if (ctx == nullptr) { montauk::mfree(stack); return -1; } ctx->user_entry = entry; ctx->user_arg = arg; ctx->stack_base = stack; uint64_t stack_top = ((uint64_t)stack + stack_bytes) & ~0xFULL; int tid = (int)syscall3(montauk::abi::SYS_THREAD_SPAWN, (uint64_t)&detail::thread_trampoline, (uint64_t)ctx, stack_top); if (tid < 0) { montauk::mfree(ctx); montauk::mfree(stack); return -1; } return tid; } // Block until the thread identified by `tid` terminates. Returns 0 on // success (with the thread's exit code in *out_code if non-null) or // -1 if `tid` is not a joinable sibling. inline int thread_join(int tid, int* out_code = nullptr) { return (int)syscall2(montauk::abi::SYS_THREAD_JOIN, (uint64_t)tid, (uint64_t)out_code); } // Return the calling thread's TID (== getpid() for the main thread). inline int thread_self() { return (int)syscall0(montauk::abi::SYS_THREAD_SELF); } // Lightweight mutex backed by a single atomic word + the kernel // yield syscall. Adequate for short critical sections; a heavier // primitive can wrap mailbox_recv when blocking semantics are needed. struct Mutex { volatile uint32_t locked = 0; void lock() { while (__atomic_exchange_n(&locked, 1, __ATOMIC_ACQUIRE) != 0) { montauk::yield(); } } bool try_lock() { return __atomic_exchange_n(&locked, 1, __ATOMIC_ACQUIRE) == 0; } void unlock() { __atomic_store_n(&locked, 0, __ATOMIC_RELEASE); } }; }