The trona / trona_posix Boundary
basaltc is a thin shim over trona_posix for almost everything that touches kernel state, and a self-contained Rust implementation for everything that is pure data manipulation.
The decision is consistent across the 44 top-level modules but is not obvious from the directory layout.
This page enumerates the boundary explicitly: which modules talk to trona_posix, which talk only to trona, and which depend on neither and are pure Rust.
It also documents the errno thread-local storage layout, the pre-TLS fallback, and the constraint that this layout imposes on early startup code.
The Three Categories
Every basaltc module falls into exactly one of three buckets:
| Bucket | Why |
|---|---|
Delegates to |
The function operates on kernel state — file descriptors, process state, signal masks, timers, sockets, memory mappings, futexes — that lives in a userland server reached via IPC. basaltc translates the C ABI to a |
Uses only |
The function needs synchronization or substrate-level primitives (mutex, futex, IPC buffer, slot allocator) but does not need any service. Examples: |
Pure Rust, no dependency |
The function transforms in-process data only. Examples: |
The third bucket is larger than people typically expect of a libc. Roughly a third of basaltc’s code never touches anything outside the process, because the operations involved are computational rather than systemic.
Module Dependency Table
The 44 top-level modules in lib/basalt/c/src/lib.rs map to the buckets as follows:
| Module | Bucket | Notes |
|---|---|---|
|
Pure Rust |
Trait definitions and per-architecture impls. SIMD intrinsics from |
|
trona_posix + trona |
Calls |
|
Pure Rust |
Reads |
|
Pure Rust |
Manipulates the global |
|
trona_posix |
Reads thread-local |
|
trona_posix + trona |
Calls |
|
Pure Rust |
Calls into |
|
Pure Rust |
Same as |
|
trona_posix |
Every |
|
trona_posix |
|
|
trona_posix + trona |
|
|
trona_posix |
|
|
trona_posix |
|
|
trona_posix |
|
|
trona_posix |
Single function |
|
trona_posix |
Wraps |
|
Pure Rust |
Self-contained terminfo database lookups. No external state. |
|
Pure Rust |
NFA compilation and matching. ~900 lines, zero external deps. |
|
trona_posix |
|
|
trona_posix (mkstemp/realpath only) |
|
|
Pure Rust + locale tables |
Wide character conversion uses the rune tables only. |
|
trona_posix |
|
|
trona_posix |
|
|
Pure Rust |
C-locale only. |
|
trona_posix (via dirent) |
Pattern expansion calls |
|
trona_posix |
Translates |
|
Pure Rust + Arch |
|
|
trona_posix (getrandom) |
|
|
trona_posix |
Almost entirely a thin C ABI wrapper over |
|
Pure Rust |
|
|
trona_posix + trona |
|
|
trona_posix (via dirent) |
File tree walker. Uses |
|
Mostly pure Rust |
Sub-modules vary; see FreeBSD Compatibility for the breakdown. |
|
trona_posix |
|
|
trona_posix |
|
|
Pure Rust |
|
|
Pure Rust |
Argv parser. POSIX |
|
trona_posix |
|
|
Pure Rust |
Encoding conversion tables compiled in. |
|
Pure Rust |
SHA-512 transform. No state outside the caller’s context. |
|
Pure Rust + sha512 |
Password hashing. Uses sha512 internally. |
|
trona_posix |
|
|
Pure Rust |
|
|
trona panic |
|
Counts:
| Bucket | Modules |
|---|---|
Delegates to |
24 ( |
|
3 (some uses overlap with the row above): |
Pure Rust (no external dependency) |
17 ( |
The "trona substrate only" row is small because most modules that need substrate primitives also need a trona_posix system call somewhere.
errno: Thread-Local with Pre-TLS Fallback
errno is the canonical example of how basaltc bridges thread-local state into the C ABI.
The C standard requires errno to be a per-thread location reachable via the macro errno, which expands to (*__errno_location()).
basaltc implements __errno_location() like this:
static mut ERRNO: i32 = 0;
#[unsafe(no_mangle)]
pub unsafe extern "C" fn __errno_location() -> *mut i32 {
let tls_ptr = trona_posix::tls::current_errno();
if !tls_ptr.is_null() {
tls_ptr
} else {
&raw mut ERRNO
}
}
Two paths:
-
If TLS is initialized,
trona_posix::tls::current_errno()returns a pointer into the calling thread’s TLS block — specifically into a field namederrnoinside theTlsstruct. -
If TLS is not yet initialized (the brief window between the kernel handing control to
_startandinit_main_thread_tls()returning),current_errno()returns null and basaltc falls back to a single globalERRNO.
The fallback is safe because the only code that runs before TLS is up is single-threaded (the main thread) and runs sequentially. There are no concurrent writers to the global.
Once init_main_thread_tls() returns, every __errno_location() call goes to the per-thread slot, and the global ERRNO is never read again.
The TLS Block
trona_posix::tls::Tls is the per-thread struct that holds reentrancy buffers for basaltc:
| Field | Purpose |
|---|---|
|
The C |
|
A |
|
A 26-byte buffer for |
|
A 26-byte buffer for |
Additional reentrancy fields |
|
The struct lives in trona_posix rather than basaltc because the trona substrate needs to know the layout to allocate and initialize it during thread creation.
The architecture thread pointer (%fs on x86_64, TPIDR_EL0 on aarch64) points at the start of the struct, so current_errno() is just a load with a fixed offset from the thread pointer.
basaltc functions that need any of these reentrancy buffers ask trona_posix::tls::* for the field address, then write into it.
This means a thread that does not call any TLS-touching basaltc function never pays for the buffers — they are allocated lazily on first use.
Linux Errno Numbering
basaltc uses Linux errno numbers (EPERM=1, ENOENT=2, EIO=5, ENOMEM=12, EINVAL=22, etc.), not BSD or POSIX-canonical numbers.
This is a deliberate choice for compatibility with ports that hardcode error values, since the bulk of the SaltyOS port set is glibc-style software (cmake, perl, openssl) rather than pure FreeBSD code.
The constants are defined in lib/basalt/c/src/errno.rs and mirrored in lib/basalt/c/include/errno.h.
FreeBSD ports that test for BSD-numbered errnos (ENOMEM=12 is the same on both, but EAGAIN is 35 on BSD and 11 on Linux/SaltyOS) need patches in their port files; the active SaltyOS ports already have these patches.
This is a documented difference from FreeBSD that ports authors should be aware of — see FreeBSD Compatibility.
Constraints Imposed by the Boundary
Three rules follow from the layered architecture:
-
No basaltc module can call into kernite directly. All system calls go through
trona. There is noextern "C"block in basaltc declaringsyscallor any kernite function. -
basaltc cannot allocate trona objects.
trona_posixprovides every primitive (TLS block, futex, IPC buffer, slot allocator entry); basaltc never types over atrona::types::Capabilityor constructs one. -
Errors propagate as integer return codes. trona_posix returns negative values for failure (a negated errno). basaltc’s wrapper translates: if the return is negative, it sets
errno = -resultand returns-1to the C caller. Successful results are passed through unchanged.
Related Pages
-
Architecture — overview of the layered build
-
CRT Startup — when the TLS block becomes valid
-
Heap and malloc —
trona::sync::Mutexandposix_sbrkusage -
Dynamic Linking — substrate use without trona_posix delegation
-
FreeBSD Compatibility — Linux errno numbering and other BSD/Linux deltas