Files and Directories
This page covers the basaltc modules that operate on file descriptors and directory entries: unistd.rs, dirent.rs, fts.rs, glob.rs, select.rs, ioctl.rs, plus the fcntl/stat related entries from unistd.rs.
Every function listed is a #[unsafe(no_mangle)] pub extern "C" fn thin wrapper around a trona_posix::* system call; the wrappers translate C ABI conventions (errno set, return -1 on failure) to the trona convention (negative result is the negated errno) and back.
Module Overview
| Module | Functions |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
The functional split is purely by API surface; underlying every call is the same VFS server reachable through trona_posix::posix_*.
File Descriptor Operations (unistd.rs)
The unistd surface is the most heavily used part of basaltc.
Every wrapper has the same shape: convert C arguments to Rust types, call trona_posix::posix_*, translate the result.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn open(path: *const u8, flags: i32, mode: u32) -> i32 {
let result = unsafe { trona_posix::posix_open(path, flags as u32, mode) };
if result < 0 {
errno::set_errno(-result as i32);
return -1;
}
result as i32
}
The wrapper does five things:
-
Pass through the C string pointer untranslated (
trona_posix::posix_openaccepts*const u8). -
Cast
flagsto the wider unsigned type thattrona_posixexpects. -
Forward
mode(the create mode forO_CREAT). -
Convert a negative
trona_posixresult to the C convention: store-resultinerrno, return-1. -
Pass a non-negative result through unchanged.
This pattern repeats across all unistd functions. There are no clever optimizations or aggressive caching — every call is one IPC round-trip.
File Descriptor Routing in trona_posix
trona_posix::posix_* does not dispatch on a per-server basis based on fd type.
Instead, every fd in a SaltyOS process maps to a capability stored in a per-process fd table maintained by trona_posix.
The capability points at one of several userland servers:
| fd backend | Server reached |
|---|---|
Regular file |
The vfsserver (which in turn talks to a filesystem server like saltyfs). |
Pipe |
The vfsserver pipe channel. |
Socket |
netsrv (TCP/UDP/ICMP) or the AF_UNIX server. |
TTY / pty |
posix_ttysrv. |
Shared memory ( |
The vfsserver’s shm region. |
Special device ( |
The console server / |
trona_posix knows which capability backs each fd, so calling posix_read(fd, …) always reaches the right server without basaltc having to dispatch.
This means basaltc’s unistd.rs is uniformly thin — there is no special case for "read from a socket" versus "read from a regular file".
Path Translation
basaltc passes paths through verbatim.
There is no path canonicalization, no symlink resolution, no working-directory expansion in basaltc itself; the VFS server does all of that.
Calling open("../foo/bar", O_RDWR, 0) sends the literal "../foo/bar" string to vfsserver, which resolves it relative to the calling process’s current working directory (also tracked server-side).
The one exception is getcwd(buf, size):
pub unsafe extern "C" fn getcwd(buf: *mut u8, size: usize) -> *mut u8 {
let len = unsafe { trona_posix::posix_getcwd(buf, size) };
if len < 0 {
errno::set_errno(-len as i32);
return core::ptr::null_mut();
}
buf
}
getcwd returns the buffer pointer on success, NULL on failure — the C convention. No string copy in basaltc; trona_posix writes directly into the user buffer.
realpath (in stdlib.rs rather than unistd.rs) is the one path-resolution function that runs partly in basaltc.
It calls posix_stat repeatedly while walking the path components, allocating a result buffer with malloc.
Stat Family
stat, lstat, fstat populate a struct stat:
pub unsafe extern "C" fn stat(path: *const u8, buf: *mut Stat) -> i32 {
let result = unsafe { trona_posix::posix_stat(path, buf) };
if result < 0 {
errno::set_errno(-result as i32);
return -1;
}
0
}
The Stat type is #[repr©] and matches the layout declared in lib/basalt/c/include/sys/stat.h.
basaltc and trona_posix share the type from trona::types so there is no field-by-field copy.
fstatat is implemented (Linux extension widely used by ports) and routes through posix_fstatat.
Directories (dirent.rs)
#[repr(C)]
pub struct DIR {
fd: i32,
// ... cookie state for trona_posix
}
pub unsafe extern "C" fn opendir(path: *const u8) -> *mut DIR {
let dir = trona_posix::dirent::posix_opendir(path);
// ... allocate DIR struct, store the cookie ...
}
pub unsafe extern "C" fn readdir(dir: *mut DIR) -> *mut Dirent {
let entry_ptr = trona_posix::dirent::posix_readdir(...);
// ... return pointer to next entry ...
}
The DIR struct is opaque to user code (declared in <dirent.h> as a forward declaration).
Inside basaltc it carries the underlying fd and the trona_posix cookie used to position within the directory stream.
scandir(path, namelist, filter, compar) allocates an array of dirent* pointers via malloc, walks the directory with readdir, applies the filter callback, and sorts with qsort using the user-supplied comparator.
alphasort and versionsort are stock comparators provided as helpers.
readdir_r (the reentrant variant) is implemented but is deprecated in glibc. It still works on basaltc; ports that use it do not need to switch.
fcntl
fcntl(fd, cmd, …) is the swiss-army-knife operation: duplicate fd, set/clear flags, set file lock, query owner.
basaltc supports the common subset:
| Command | Action |
|---|---|
|
Duplicate fd, optionally with |
|
Get/set close-on-exec flag |
|
Get/set file status flags ( |
|
Get/set fd owner for |
|
Advisory file locking |
|
Pipe buffer size (Linux extension widely used) |
Each command routes through trona_posix::posix_fcntl, which forwards the request to the appropriate server.
File locking is handled by vfsserver; advisory locks are per-process and do not cross fork boundaries.
readv / writev
Vectored I/O is supported for fds that the underlying server supports it on:
pub unsafe extern "C" fn readv(fd: i32, iov: *const Iovec, iovcnt: i32) -> isize {
let result = unsafe { trona_posix::posix_readv(fd, iov, iovcnt) };
if result < 0 {
errno::set_errno(-result as i32);
return -1;
}
result as isize
}
Iovec is #[repr©] and matches the standard struct.
basaltc passes the iov array through to trona_posix, which constructs the appropriate IPC payload.
select / pselect
select.rs reduces select() to poll():
pub unsafe extern "C" fn select(
nfds: i32, readfds: *mut FdSet, writefds: *mut FdSet,
exceptfds: *mut FdSet, timeout: *mut Timeval,
) -> i32 {
// 1. Convert (readfds, writefds, exceptfds) to a pollfd[] up to nfds.
// 2. Convert timeout from struct timeval to milliseconds.
// 3. Call trona_posix::poll::posix_poll(pollfds, count, ms).
// 4. Convert the pollfd events back to the three fd_sets.
// 5. Return the count of fds with any event.
}
The reduction is straightforward because basaltc has no select-specific kernel support — every fd is pollable, and the pollable bitmap on each fd carries the read/write/error events that select wants.
pselect adds an atomic signal mask switch around the wait, which trona_posix supports natively via a separate posix_ppoll entry point.
fts and glob
fts_open(paths, options, compar) returns an FTS* cursor:
pub struct FTS {
// root paths array
// current state
// options (FTS_PHYSICAL | FTS_LOGICAL | FTS_NOCHDIR | FTS_COMFOLLOW | ...)
// compar callback
}
Each call to fts_read(fts) returns the next FTSENT (a struct with the path, depth, file type, stat result, and flags).
fts handles directory entry sorting (via the user comparator), depth tracking, post-order vs pre-order traversal, and physical vs logical (symlink-following) walks.
The implementation is a faithful port of the FreeBSD fts(3) interface and uses posix_opendir/posix_readdir/posix_stat underneath.
About 900 lines of basaltc code; one of the largest single modules outside of stdio and wchar.
glob(pattern, flags, errfunc, pglob) performs shell-style pattern expansion (*, ?, […], {…}):
pub unsafe extern "C" fn glob(
pattern: *const u8, flags: i32,
errfunc: Option<unsafe extern "C" fn(*const u8, i32) -> i32>,
pglob: *mut Glob,
) -> i32 {
// 1. Parse the pattern into segments.
// 2. For each directory level, opendir + readdir, match each entry against the segment.
// 3. Recurse for the next segment.
// 4. Sort the result (unless GLOB_NOSORT).
// 5. Allocate the result array via malloc and store in pglob.
}
globfree(pglob) frees the result array.
fnmatch(pattern, string, flags) lives in glob.rs as well, since it shares the pattern matcher.
ioctl
ioctl.rs is one function:
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ioctl(fd: i32, request: u64, arg: *mut u8) -> i32 {
let result = unsafe { trona_posix::ioctl::posix_ioctl(fd, request, arg) };
if result < 0 {
errno::set_errno(-result as i32);
return -1;
}
result
}
The request is a 32-bit ioctl number with embedded direction, type, and command bits.
trona_posix decodes it and routes to the right server (terminal control to posix_ttysrv, socket control to netsrv, etc.).
basaltc does not interpret the request — it is just a tunnel.
Higher-level wrappers like tcgetattr (in termios.rs) build the appropriate TCGETS request and call this function.
Common Pitfalls
-
No buffering —
read()andwrite()are direct, unlikefread()/fwrite(). Aread(fd, buf, 1)issues one IPC call. -
O_DIRECTORYis honored —open()-ing a path that is not a directory withO_DIRECTORYreturnsENOTDIR. -
O_CLOEXECmust be passed at open time. There is nofcntl(F_SETFD, FD_CLOEXEC)shortcut — well, the shortcut works, but a fork betweenopenandfcntlwould inherit the fd unflagged. -
getcwd(NULL, 0)is the GNU extension that returns a `malloc’d buffer. basaltc supports it. -
ttyname(fd)allocates from a per-thread TLS buffer — see trona Boundary. The pointer becomes invalid on the next call from the same thread.
Related Pages
-
Buffered I/O (stdio) —
FILE*layer built on top of these functions -
Processes and Signals —
fork,exec,waitare also inprocess.rs -
trona Boundary —
trona_posixmapping for every function above -
Sockets —
socket()returns an fd usable withread/write