code wiki / (root) / nx_thread.nx

nx_thread.nx source

↩ module page · 137 lines · 6110 B

1// nx_thread.nx -- clone(2)-based thread creation (CLONE_VM, CLONE_FS, 2// CLONE_FILES, CLONE_SIGHAND, CLONE_THREAD). 3// 4// A "thread" on Linux is just a process that shares its parent's 5// address space + file table + signal handlers. clone() with the 6// right flags creates one. This module exposes a focused API on 7// top of the raw syscall. 8// 9// Key constraint: the new thread starts at the function specified 10// by `entry` with stack pointer set to `stack_top`. Caller must 11// allocate the stack themselves (typically 64 KiB - 8 MiB). 12// 13// We don't pass the function via a function pointer (NishiLang has 14// no first-class func type) -- caller passes the symbol address as 15// i64 they obtained out-of-band. When NishiLang grows func types, 16// the API takes a typed callback. 17// 18// Today the sim is single-threaded. Calling nx_thread_spawn 19// against the host kernel will succeed (real threads are created) 20// but the in-process simulator can't observe them. When SMP lands 21// in nx_rv64_sim, the simulated CPU array maps onto host threads 22// via this primitive. 23// 24// Pairs with nx_atom (atomic state across threads) + nx_mutex 25// (futex-based blocking) + nx_proc (parent process abstraction). 26 27// nx_safety_envelope: 28// intended_use: AUTO_APPLIED -- primitive-specific tuning queued 29// sil_target: SIL1 30// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail] 31// verdict: NOT_YET_EVALUATED 32 33import "nx_syscalls.nx" 34import "nx_proc.nx" 35const NX_MAGIC_65536: i64 = 65536 36const NX_MAGIC_262144: i64 = 262144 37 38// clone() flag masks for thread creation. Standard "pthread" 39// thread = VM | FS | FILES | SIGHAND | THREAD | SETTLS. 40const NX_THREAD_CLONE_FLAGS: i64 = 0x100 // CLONE_VM 41const NX_THREAD_FS: i64 = 0x200 // CLONE_FS 42const NX_THREAD_FILES: i64 = 0x400 // CLONE_FILES 43const NX_THREAD_SIGHAND: i64 = 0x800 // CLONE_SIGHAND 44const NX_THREAD_THREAD: i64 = 0x10000 // CLONE_THREAD 45const NX_THREAD_SETTLS: i64 = 0x80000 // CLONE_SETTLS 46const NX_THREAD_PARENT_TID: i64 = 0x100000 // CLONE_PARENT_SETTID 47const NX_THREAD_CHILD_TID: i64 = 0x200000 // CLONE_CHILD_CLEARTID 48 49// Default flag mask matching glibc's pthread_create. Excludes 50// SETTLS / TID since we don't manage TLS yet. 51const NX_THREAD_DEFAULT_FLAGS: i64 = 0x100 // CLONE_VM 52 53// Raw clone() escape hatch -- used when caller wants full control of 54// flags + child entry sequencing themselves. Returns 0 in the child 55// and the child TID in the parent (negative on error). Most callers 56// want nx_thread_spawn (below), which uses the __thread_clone builtin 57// for a correct child trampoline. 58func nx_thread_spawn_raw(flags: i64, stack_top: i64) -> i64 { 59 return __syscall(NX_SYS_CLONE, flags, stack_top, 0, 0, 0, 0) 60} 61 62// Spawn a real OS thread that calls `entry(ctx)` then exits. Returns 63// child TID (positive) on success, -errno on failure. Uses the 64// __thread_clone compiler builtin which embeds (entry, ctx) on the 65// new thread's stack and emits the child trampoline natively for the 66// current target (RV64 or x86_64). No libc, no pthread. 67// 68// `entry` is a raw function address (i64); callers can obtain it via 69// the unary `&fn_name` -> first-class function-pointer feature. 70// 71// Stack: 64 KiB minimum; caller may request larger. 4 KiB if user 72// passes a smaller value (paranoia clamp). 73func nx_thread_spawn(entry: i64, ctx: i64, stack_size: i64) -> i64 { 74 if stack_size < NX_MAGIC_65536 { stack_size = NX_MAGIC_65536 } 75 let stack: *u8 = sys_mmap(stack_size) 76 let stack_top: *u8 = ((stack as i64) + stack_size) as *u8 77 let ctx_p: *u8 = ctx as *u8 78 return __thread_clone(stack_top, entry, ctx_p) 79} 80 81// Typed fn-pointer wrapper: takes a `func(*u8) -> i64` directly so 82// callers don't need an unsafe cast from fn-pointer to i64. The 83// __thread_clone builtin reads the fn-pointer value as an address. 84func nx_thread_spawn_fn(entry: func(*u8) -> i64, ctx: *u8, stack_size: i64) -> i64 { 85 if stack_size < NX_MAGIC_65536 { stack_size = NX_MAGIC_65536 } 86 let stack: *u8 = sys_mmap(stack_size) 87 let stack_top: *u8 = ((stack as i64) + stack_size) as *u8 88 return __thread_clone(stack_top, entry, ctx) 89} 90 91// NAMED thread-spawn shim (patchable): argptr -> i64[2] = [fn_addr, ctx]. 92// Linux: real clone via nx_thread_spawn. On a native-PE target, nx_pe_natbw 93// OVERWRITES this entry with `jmp -> CreateThread thunk` (MS-ABI->SysV trampoline 94// reads {fn,ctx}). Used by the thread pool's native-worker path. 95func sys_thread_create(argptr: i64) -> i64 { 96 let a: *i64 = argptr as *i64 97 return nx_thread_spawn(a[0], a[1], NX_MAGIC_262144) 98} 99 100// Get current thread's TID. Linux RV64 syscall: 178 (gettid). 101func nx_thread_self() -> i64 { 102 return __syscall(178, 0, 0, 0, 0, 0, 0) 103} 104 105// Yield CPU to scheduler. Linux RV64 syscall: 124 (sched_yield). 106// Useful when spinning waiting for another thread to make progress. 107func nx_thread_yield() -> i64 { 108 return __syscall(156, 0, 0, 0, 0, 0, 0) // getsid: rv64 156 -> x86_64 124 109} 110 111// Exit the current thread. Calls sys_exit (not sys_exit_group -- 112// the latter terminates the whole process). 113func nx_thread_exit(code: i64) -> i64 { 114 return __syscall(NX_SYS_EXIT, code, 0, 0, 0, 0, 0) 115} 116 117// ---- self-test --------------------------------------------------- 118 119func main() -> i64 { 120 // Constants are within Linux range. 121 if NX_THREAD_CLONE_FLAGS != 0x100 { return __syscall(93, 1, 0, 0, 0, 0, 0) } 122 if NX_THREAD_FS != 0x200 { return __syscall(93, 2, 0, 0, 0, 0, 0) } 123 if NX_THREAD_THREAD != 0x10000 { return __syscall(93, 3, 0, 0, 0, 0, 0) } 124 125 // gettid should match getpid for the main thread (single-thread). 126 let tid: i64 = nx_thread_self() 127 let pid: i64 = nx_proc_getpid() 128 if tid <= 0 { return __syscall(93, 4, 0, 0, 0, 0, 0) } 129 if pid <= 0 { return __syscall(93, 5, 0, 0, 0, 0, 0) } 130 if tid != pid { return __syscall(93, 6, 0, 0, 0, 0, 0) } 131 132 // sched_yield should succeed (return 0). 133 let r: i64 = nx_thread_yield() 134 if r != 0 { return __syscall(93, 7, 0, 0, 0, 0, 0) } 135 136 return 0 137}