Threads and Synchronization
basaltc’s pthread.rs is the largest C ABI wrapper in the library: roughly 1,100 lines that translate the POSIX thread API into trona_posix::pthread::* calls.
Almost every function is a one-line forwarder; the interesting code is in trona_posix itself, which builds threads on top of trona TCB capabilities and futex synchronization on top of the trona substrate’s Futex syscall.
This page documents the C ABI surface, the locking primitives, the per-thread state model, the differences from glibc, and the open issues.
API Surface
| Group | Functions |
|---|---|
Thread lifecycle |
|
Mutex |
|
Mutex attribute |
|
Condition variable |
|
Read-write lock |
|
Barrier |
|
Spinlock |
|
Once |
|
TLS keys |
|
Attributes |
|
The full list is roughly 60 functions. Almost every entry is the same shape:
#[unsafe(no_mangle)]
pub unsafe extern "C" fn pthread_mutex_lock(m: *mut PthreadMutex) -> i32 {
unsafe { trona_posix::pthread::pthread_mutex_lock(m) }
}
The wrapper exists because the C symbol must be #[unsafe(no_mangle)] pub extern "C", and the public type must live in basaltc’s <pthread.h> header.
The actual implementation is in trona_posix, which has its own type aliases that match the basaltc-exported types.
Error Convention Difference
POSIX pthread functions are unusual: they return errno values directly, not through the global errno.
Returning 0 means success; returning a positive value means failure with that error code:
int err = pthread_mutex_lock(&m);
if (err != 0) {
fprintf(stderr, "lock failed: %s\n", strerror(err));
}
basaltc preserves this convention.
The wrappers do not call errno::set_errno and do not convert the return value to -1.
This is the only basaltc API family with this convention; every other module uses the standard "set errno, return -1" pattern.
If you forget and write if (pthread_mutex_lock(&m) == -1), the test will silently fail to detect errors because pthread errors are positive integers (EBUSY = 16, EINVAL = 22, etc.).
Mutex Implementation
Under the hood, every basaltc pthread mutex is a trona_posix::pthread::Mutex, which in turn wraps a futex word and a lock owner field.
The acquire path uses the same three-phase protocol as basaltc’s internal trona::sync::Mutex:
-
CAS — single compare-and-swap on the futex word from
UNLOCKEDtoLOCKED. Most uncontended cases succeed here. -
Spin — bounded spin loop checking the futex word, in case the holder is about to release.
-
Futex wait — only after spinning fails does the thread make a
Futexsyscall to block. This is the only path that enters the kernel.
The release path is symmetric: CAS the word back to UNLOCKED. If the result indicates a waiter, issue a Futex wake.
Mutex types supported via pthread_mutexattr_settype:
| Type | Behavior |
|---|---|
|
Default. Self-locking deadlocks (the same thread that holds the mutex blocks forever on second lock). |
|
The same thread can lock the mutex multiple times. Tracks an owner thread ID and a depth counter. Each |
|
Like normal but returns |
|
Aliased to |
PTHREAD_PROCESS_SHARED mutexes (mutexes living in shared memory between processes) are accepted but currently behave as process-private. This is a known limitation.
Condition Variable Implementation
pthread_cond_wait(cv, mutex) does the standard atomic-release-and-wait dance:
-
Read the cv’s wait counter (a 64-bit sequence number bumped on every signal).
-
Release
mutex. -
Futexwait on the cv’s wait counter, expecting the previously read value. -
On wake, re-acquire
mutex.
pthread_cond_signal(cv) increments the wait counter and issues a Futex wake for one waiter.
pthread_cond_broadcast(cv) increments the counter and wakes all waiters.
This is the canonical futex condvar pattern from the futex(7) manual page.
The implementation lives in trona_posix::pthread; basaltc’s wrapper does no additional work.
Read-Write Lock
pthread_rwlock_* is built on a 32-bit word with a reader count in the low bits and a writer flag in the high bit, plus a futex-based wait queue for blocked acquirers.
Reader fairness is "writer preference": once a writer arrives, new readers queue behind it. This prevents writer starvation under continuous reader load, at the cost of slightly worse read latency.
Barrier and Spinlock
pthread_barrier_* uses a counter and a generation number. When the count reaches the threshold, all waiters are released by incrementing the generation. Implemented via Futex wait/wake on the generation field.
pthread_spin_* is a pure user-space spinlock — no futex, no kernel involvement.
On uniprocessor configurations or when the holder is descheduled, the spinning thread can waste arbitrarily large amounts of CPU.
Use spinlocks only when you can guarantee that critical sections are very short.
pthread_once
pub unsafe extern "C" fn pthread_once(
once: *mut PthreadOnce, init: extern "C" fn(),
) -> i32 {
unsafe { trona_posix::pthread::pthread_once(once, init) }
}
Standard double-checked locking with a futex. The init function runs exactly once across all threads, even under concurrent calls.
TLS Keys
pthread_key_create(key, destructor) allocates a TLS key index from a per-process key table.
pthread_setspecific(key, value) writes the value into the calling thread’s per-key slot.
pthread_getspecific(key) reads it back.
The implementation supports up to PTHREAD_KEYS_MAX = 64 keys.
Each thread has a key array allocated lazily on first setspecific call.
The destructor (if registered) runs at thread exit time for any key whose value is non-NULL.
This is distinct from compiler _thread TLS storage, which uses the static TLS block established at thread creation.
pthread_key* is a runtime API for libraries that need TLS but cannot rely on compiler support; native Rust code uses thread_local! instead.
pthread_create
pub unsafe extern "C" fn pthread_create(
thread: *mut pthread_t,
attr: *const PthreadAttr,
start_routine: extern "C" fn(*mut c_void) -> *mut c_void,
arg: *mut c_void,
) -> i32 {
unsafe { trona_posix::pthread::pthread_create(thread, attr, start_routine, arg) }
}
Inside trona_posix, pthread_create performs the following steps:
-
Allocate a stack via
posix_mmap(with a guard page below the bottom). -
Allocate a TLS block matching the static TLS template (the
Tlsstruct and any__thread/thread_localslots from loaded DSOs). -
Allocate a TCB capability via the procmgr’s
create_threadIPC. -
Set the new TCB’s instruction pointer to a small assembly trampoline that loads the thread pointer, calls the start routine, calls
pthread_exitwith the return value. -
Resume the new TCB.
pthread_t is opaque to user code; basaltc declares it as a 16-byte struct in <pthread.h> matching the trona_posix Thread struct internally.
pthread_join
pub unsafe extern "C" fn pthread_join(thread: pthread_t, retval: *mut *mut c_void) -> i32 {
unsafe { trona_posix::pthread::pthread_join(thread, retval) }
}
pthread_join blocks until the target thread exits, then writes the return value into *retval and frees the thread’s stack and TCB.
The implementation uses a futex on a "thread done" flag inside the joined thread’s metadata block.
pthread_detach(thread) flips a flag in the metadata so that the thread frees its own resources at exit time, removing the need for pthread_join.
Cancellation
basaltc supports pthread_cancel(thread) and pthread_setcancelstate(state, oldstate) and pthread_setcanceltype(type, oldtype), but the cancellation model is deferred only (PTHREAD_CANCEL_DEFERRED).
Asynchronous cancellation (PTHREAD_CANCEL_ASYNCHRONOUS) is not supported.
A canceled thread sees the cancellation request at the next cancellation point — typically inside a pthread_testcancel() call or during a blocking syscall (which currently does not actually trigger cancellation in basaltc, see "Open Issues" below).
Open Issues
-
Cancellation in syscalls — basaltc’s wrappers do not check for cancellation before calling
trona_posix::*. Apthread_cancel’d thread blocked in `read()will not wake until the read completes naturally. POSIX requiresreadto be a cancellation point. -
pthread_atforkis implemented as a no-op. Fork handlers are not called. -
Thread-local destructors —
cxa_thread_atexitdelegates tocxa_atexit, so per-thread destructors run at process exit time, not at the owning thread’s exit time. See atexit and Process Exit. -
PTHREAD_PROCESS_SHAREDmutexes in shared memory are accepted but behave as process-private. This breaks cross-process locking schemes that rely on a shared mutex in mmap’d memory. -
Robust mutexes (
PTHREAD_MUTEX_ROBUST) are not implemented. -
Priority inheritance mutexes (
PTHREAD_PRIO_INHERIT) are accepted but treated as priority-none. The kernel scheduler does support priority inheritance for trona TCBs, but the userspace mutex protocol does not yet propagate that.
These limitations are acceptable for the current SaltyOS port set (most ports use only the basic mutex/condvar/rwlock surface) but are noted here for ports that need more advanced features.
Related Pages
-
Processes and Signals —
pthread_killandpthread_sigmask -
Heap and malloc — the same three-phase futex pattern is used for the heap mutex
-
trona Boundary —
trona_posix::pthread::*and the futex primitives introna::sync