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}