File I/O and *at() Family
trona_posix::file and trona_posix::at together cover the POSIX file system surface that basaltc’s C wrappers call into.
file.rs (753 lines) owns the traditional path-based API plus the fd-based operations.
at.rs (441 lines) owns the *at() variants that resolve paths relative to an dirfd.
Both modules target the VFS endpoint (caps::vfs_ep()) and use labels from the VFS label catalog.
Neither module implements anything substantive on its own — the actual filesystem semantics live in the vfs server.
Path-based file I/O — file.rs
Open, close, read, write, lseek
The five core POSIX operations each map to a single VFS label:
| Function | VFS label |
|---|---|
|
|
|
|
|
|
|
|
|
|
The bulk upgrade is the biggest non-trivial thing file.rs does.
Short reads and writes fit in the IPC register window (up to ~16 × 8 bytes = 128 bytes of inline payload) and use the plain label.
Larger ones call into bulk.rs (see Poll, Pipe, and Bulk I/O) which sets up a shared memory region with the VFS server and transfers through it.
The threshold is tuned so that the one-time cost of setting up a shared memory region amortizes across the transfer. For small reads the IPC-register path is always faster; for large reads the bulk path dominates.
Positional variants
| Function | VFS label |
|---|---|
|
|
|
|
pread/pwrite are the thread-safe alternatives to an lseek-then-read pair — the seek is atomic with the transfer at the VFS server.
trona_posix just exposes them; the atomicity is the server’s responsibility.
Stat family
| Function | VFS label |
|---|---|
|
|
|
|
|
|
Each one writes a PosixStat struct (defined in uapi/types/posix.rs) through the IPC buffer — VFS fills in inode, mode, nlink, uid, gid, size, atime / mtime / ctime, and block counts.
There is no caching in trona_posix itself; every call is a round-trip to VFS.
Most hot-path code that needs stat-like info uses VFS_POSIX_FSTAT with a cached fd rather than the path-based variants.
Remove, rename, permissions
| Function | VFS label |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
Forwarded via VFS (via |
|
|
|
|
|
|
|
|
Note the asymmetry: posix_chmod is implemented as an openat-with-AT_FDCWD against VFS_POSIX_FCHMODAT rather than having its own label, because adding a dedicated plain-path chmod label would duplicate logic that already exists in the *at() dispatcher.
Opendir and readdir
Directory enumeration is two calls:
let dirfd = posix_opendir(path); // → VFS_POSIX_OPENDIR (13)
loop {
let entry = posix_readdir(dirfd); // → VFS_POSIX_READDIR (14)
if entry.is_null() { break; }
// use entry
}
posix_close(dirfd);
Each readdir returns exactly one entry, so the client is in control of pagination.
basaltc’s C readdir() wraps this with a small per-fd cache to avoid one IPC per entry.
*at() family — at.rs
The *at() functions take an extra dirfd argument that is the base for relative paths.
AT_FDCWD (defined as -100) is a special value that means "use the current working directory", which lets the same dispatcher handle both plain and relative paths on the VFS side.
| Function | VFS label |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
The at_flags argument carries AT_SYMLINK_NOFOLLOW, AT_SYMLINK_FOLLOW, AT_REMOVEDIR, and similar — trona_posix passes these through unchanged; VFS interprets them.
The renameat/linkat variants take two dirfd arguments because the source and destination can be in different directories.
Both dirfds travel as register fields; the path strings follow as inline bytes in the IPC buffer overflow.
Path encoding
Paths in VFS IPC are always UTF-8 byte sequences, NUL-terminated, with a maximum length of 4,096 bytes (PATH_MAX).
trona_posix packs them as follows:
-
Short paths (fit in register window + overflow): all inline in the
TronaMsgregisters. -
Long paths: via bulk SHM transfer, same mechanism as
posix_read/posix_write.
VFS validates the NUL termination and the length on the server side; trona_posix does not. Passing a non-NUL-terminated path is an undefined-behavior-leading bug.
What is deliberately not here
A few operations that might belong in file.rs live elsewhere:
-
dup/dup2/dup3— inpipe.rs, because they are descriptor-table operations rather than file operations. -
mmap— inmm.rs, because it targets mmsrv, not VFS. -
ioctl— inmisc.rs, because it is a catch-all for everything that is not a standard fd operation. -
fcntl— inmisc.rs, same reason. -
shm_open/shm_unlink— inmisc.rs; they use theVFS_POSIX_SHM_*labels but are conventionally grouped with SHM operations rather than file operations.
Error translation
Every function in file.rs and at.rs calls trona_err_to_posix (defined in posix/lib.rs — see trona_posix Overview) to translate the TRONA_* reply into a negative POSIX errno.
A few error cases are mapped differently than the generic table:
-
TRONA_NOT_FOUNDfrom a path lookup →-ENOENT. -
TRONA_NOT_FOUNDfrom areaddirthat has reached the end →0(end-of-directory, not an error). -
TRONA_IS_DIRECTORYfrom anopen(O_WRONLY)on a directory →-EISDIR. -
TRONA_READONLYfrom a write to a read-only file →-EROFS.
basaltc’s C ABI layer flips the sign, stores the positive value in per-thread errno, and returns -1.
Related pages
-
VFS Protocol Labels — the full list of labels referenced here.
-
Poll, Pipe, and Bulk I/O — the bulk SHM transfer path used for large reads and writes.
-
Sockets and DNS — another major consumer of the VFS endpoint.
-
basalt: Files and Directories — the C wrappers on top of these functions.