system/posix
Raw POSIX syscalls.
Types
Pollfd :: struct struct pollfd: one descriptor and what to watch it for.
Field names are the C ones because a mirror copies BY NAME.
Fields
fd: i32 events: i16 revents: i16 Winsize :: struct struct winsize: the terminal size TIOCGWINSZ fills in.
Field names are the C ones because a mirror copies BY NAME; the snake_case rule does not reach across the boundary.
Fields
ws_row: u16 ws_col: u16 ws_xpixel: u16 ws_ypixel: u16 Functions
sys_write :: fn(fd: i32, buf: u8[], count: i64) -> i64 write(2). Byte count written, or -1 with errno set.
i64, not i32: POSIX returns ssize_t, 64-bit on every target here.
@c_header hands cc the real prototype, so a width mismatch stops the
build rather than truncating silently.
Parameters
| fd | i32 | No description. |
| buf | u8[] | No description. |
| count | i64 | No description. |
Returns
i64 sys_opendir :: fn(path: string) -> CPointer | null opendir(3). NULL when path is not a directory, does not exist, or
cannot be searched.
It answers "is this a directory" only for a directory the caller can READ:
sys_path_kind is what std/io::is_dir asks, and says why.
Parameters
| path | string | the directory to open. |
Returns
CPointer | null a DIR*, or null.sys_closedir :: fn(dir: CPointer) -> i32 closedir(3). Pairs with sys_opendir.
Parameters
| dir | CPointer | the handle sys_opendir returned. |
Returns
i32 sys_last_errno :: fn() -> i32 The calling thread's errno, as the LAST foreign call left it.
errno is a macro, not a symbol: *__error() on Darwin,
*__errno_location() on glibc, so no @extern can name it directly and the
runtime reads it for us.
⚠️ Read it IMMEDIATELY after the call that failed. Any foreign call in between may overwrite it, this one included once it has returned.
Returns
i32 the raw platform value ; 0 when nothing has failed.sys_path_kind :: fn(path: string) -> i32 What KIND of thing path is, via stat(2).
struct stat is NOT mirrorable, not "ambiguous between arches", genuinely
inexpressible. A mirror is pinned field by field to __builtin_offsetof, so
it is only valid as a contiguous LEADING prefix ; st_mode leads on neither
arch (offset 24 on linux-x86_64, 16 on linux-arm64), and the two arches also
disagree on field ORDER, which no width or arch axis could express. So the
runtime resolves the member and hands back an ordinal.
See docs/adr/0013-what-the-ffi-boundary-cannot-express.md.
sys_opendir is NOT a substitute: it needs READ permission where stat
needs only traverse rights on the parent, so a directory with mode 0111
answers "not a directory".
An enum would name the answer set better, but @extern cannot carry one
(T3050), and a Vader wrapper here would give this module a BODY, which the
seed refuses: system/posix would then emit a unit on POSIX targets and none
on Windows, and --seed-targets requires one module set for every target.
Hence four constants and a declaration-only module.
Parameters
| path | string | the path to classify. |
Returns
i32 PATH_MISSING when stat fails, for any reason.sys_path_mtime :: fn(path: string) -> i64 When path was last modified, in nanoseconds since the Unix epoch, via
stat(2) : the runtime reads the member for the reason sys_path_kind
gives, and Darwin names it st_mtimespec where Linux says st_mtim.
Parameters
| path | string | the path to probe. |
Returns
i64 -1 when stat fails, errno saying why.sys_stream_size :: fn(stream: CPointer) -> i64 The size of the regular file stream reads, via fstat(2), for the reason
sys_path_kind gives.
Parameters
| stream | CPointer | an open FILE*. |
Returns
i64 -1 for anything but a regular file (a pipe, a device, whose size says nothing of what a read finds), or when fstat fails.sys_readdir :: fn(dir: CPointer) -> CPointer | null readdir(3). The next entry, or null at the end of the directory.
The struct dirent it returns is owned and REUSED by the C library: read the
name out before the next call and never keep the pointer.
Parameters
| dir | CPointer | the handle sys_opendir returned. |
Returns
CPointer | null a struct dirent*, or null at the end.dirent_name :: fn(ent: CPointer) -> string The d_name of a struct dirent, as a Vader string.
The one step a Vader FFI call cannot take for itself: reading a NAMED member
of a C struct. d_name sits at a different offset on every system, and only
cc can resolve it: from the real header, at the accessor in the runtime.
It has to be a CALL, not a read of the struct: the interpreter has no C
layout, but it resolves an @extern by name out of the running process the
way it reaches libc. That is what keeps read_dir working on both backends,
which anything in the stdlib has to.
The returned bytes belong to the C library and die at the next sys_readdir;
the shim interns them before then.
Parameters
| ent | CPointer | a struct dirent* from sys_readdir. |
Returns
string the entry name.sys_ferror :: fn(stream: CPointer) -> i32 ferror(3). Non-zero once the stream's error indicator is set.
A short fread means end of file OR an error, and the count cannot tell
them apart -- this is what separates them.
Parameters
| stream | CPointer | the FILE*. |
Returns
i32 non-zero if the stream is in error.sys_read :: fn(fd: i32, @c_out buf: u8[], count: i64) -> i64 read(2). Returns the byte count, 0 at end of file, -1 on error, and
a SHORT read is not an error: a pipe hands over what has arrived so far, so
a caller wanting n bytes loops until it has them.
Parameters
| fd | i32 | the descriptor; 0 is stdin. |
| buf | u8[] | receives the bytes. |
| count | i64 | how many to ask for, at most buf.len(). |
Returns
i64 bytes read, 0 at EOF, -1 on error.sys_pipe :: fn(@c_out fds: i32[]) -> i32 pipe(2). Fills fds with the read end then the write end.
Parameters
| fds | i32[] | a two-element array the call writes into. |
Returns
i32 0 on success, -1 on failure.sys_close :: fn(fd: i32) -> i32 close(2).
Parameters
| fd | i32 | the descriptor to close. |
Returns
i32 sys_spawn_actions_init :: fn(@c_out actions: u8[]) -> i32 posix_spawn_file_actions_init(3).
Parameters
| actions | u8[] | SPAWN_ACTIONS_BYTES of scratch. |
Returns
i32 sys_spawn_actions_destroy :: fn(@c_out actions: u8[]) -> i32 posix_spawn_file_actions_destroy(3). Pairs with the init.
Parameters
| actions | u8[] | the buffer the init filled. |
Returns
i32 sys_spawn_actions_addclose :: fn(@c_out actions: u8[], fd: i32) -> i32 posix_spawn_file_actions_addclose(3): close fd in the child.
Parameters
| actions | u8[] | the actions buffer. |
| fd | i32 | the descriptor to close after the fork. |
Returns
i32 sys_spawn_actions_adddup2 :: fn(@c_out actions: u8[], fd: i32, newfd: i32) -> i32 posix_spawn_file_actions_adddup2(3): dup2(fd, newfd) in the child.
Parameters
| actions | u8[] | the actions buffer. |
| fd | i32 | the source descriptor. |
| newfd | i32 | where it lands in the child. |
Returns
i32 sys_posix_spawnp :: fn(@c_out pid: i32[], file: string, actions: u8[], attrp: CPointer | null, argv: u64[], envp: CPointer) -> i32 posix_spawnp(3): launch file, resolved through PATH.
Parameters
| pid | i32[] | a one-element array receiving the child's pid. |
| file | string | the program name. |
| actions | u8[] | the file actions, or an empty buffer for none. |
| attrp | CPointer | null | null: no spawn attributes are used. |
| argv | u64[] | a NULL-terminated vector of char*, as i64 words. |
| envp | CPointer | sys_environ(). |
Returns
i32 0 on success, an errno value otherwise.sys_waitpid :: fn(pid: i32, @c_out status: i32[], options: i32) -> i32 waitpid(2).
Parameters
| pid | i32 | the child to wait for. |
| status | i32[] | a one-element array receiving the raw wait status. |
| options | i32 | WNOHANG to poll rather than block. |
Returns
i32 the pid when it changed state, 0 under WNOHANG while it still runs, -1 on error.sys_kill :: fn(pid: i32, sig: i32) -> i32 kill(2).
Parameters
| pid | i32 | the process to signal. |
| sig | i32 | the signal number. |
Returns
i32 sys_strdup :: fn(s: string) -> CPointer strdup(3): a heap copy of s, owned by the caller.
Used to build the char** a spawn wants: a i64[] of these lends as one.
Parameters
| s | string | the string to copy. |
Returns
CPointer a char* the caller frees with sys_free.sys_free :: fn(p: CPointer) -> void free(3). Releases what sys_strdup returned.
Parameters
| p | CPointer | the pointer to release. |
sys_environ :: fn() -> CPointer The process environment, for sys_posix_spawnp.
environ is a VARIABLE and the FFI calls functions, so the runtime names it.
Passing null instead gives the child an EMPTY environment: no PATH, which
every spawned tool needs.
Returns
CPointer the parent's char** environment.sys_poll_one :: fn(@c_pointer @c_out fds: Pollfd, nfds: u64, timeout_ms: i32) -> i32 poll(2) on a SINGLE descriptor.
poll takes an array; one element crosses as its address, which is the same
thing to C. A caller watching several would need the array form.
Parameters
| fds | Pollfd | the descriptor to watch; revents is filled in. |
| nfds | u64 | always 1 here. nfds_t is 4 bytes on Darwin and 8 on Linux, and neither is spellable per target: passing the wider one is ABI-correct on both, since an unsigned int argument travels in the low half of its register and 1 fits either way. It would not be, for a count that used the upper bits. |
| timeout_ms | i32 | milliseconds, negative to block. |
Returns
i32 how many descriptors are ready, 0 on timeout, -1 on error.sys_tcgetattr :: fn(fd: i32, @c_out buf: u8[]) -> i32 tcgetattr(3). Reads the terminal state into an OPAQUE buffer.
The buffer is never interpreted on the Vader side: it travels from here to
sys_tcsetattr and back, so nothing needs the layout of struct termios,
whose shape differs between systems (c_line is Linux-only) and whose c_cc
is an array. buf must hold at least TERMIOS_BYTES.
Parameters
| fd | i32 | the descriptor: 0 for stdin. |
| buf | u8[] | receives the state. |
Returns
i32 0 on success, -1 otherwise.sys_tcsetattr :: fn(fd: i32, action: i32, buf: u8[]) -> i32 tcsetattr(3). Applies a state read by sys_tcgetattr.
Parameters
| fd | i32 | the descriptor. |
| action | i32 | when to apply it: TCSAFLUSH here. |
| buf | u8[] | the state to install. |
Returns
i32 0 on success, -1 otherwise.sys_cfmakeraw :: fn(@c_out buf: u8[]) -> void cfmakeraw(3). Turns a state into the raw one, in place.
The whole reason Vader never has to name a termios member: the flag surgery
happens in C. It is MORE aggressive than a hand-rolled raw mode: it also
clears OPOST, so \n no longer returns the carriage and a caller must write
\r\n itself.
Parameters
| buf | u8[] | No description. |
sys_ioctl_winsize :: fn(fd: i32, request: u64, @c_pointer @c_out ws: Winsize) -> i32 ioctl(2) for TIOCGWINSZ. 0 on success, -1 otherwise.
ioctl is variadic; @c_header brings the real prototype, so the shim's
three-argument call is ordinary C rather than a redeclaration.
Parameters
| fd | i32 | the descriptor to ask about: 1 for stdout. |
| request | u64 | always TIOCGWINSZ here. |
| ws | Winsize | receives the size. |
Returns
i32 sys_sysconf :: fn(name: i32) -> i64 sysconf(3). The value of the configuration variable name, or -1 with
errno set: an unknown name is EINVAL.
Parameters
| name | i32 | one of the SC_* selectors. |
Returns
i64 sys_isatty :: fn(fd: i32) -> i32 isatty(3). Whether fd refers to a terminal.
Parameters
| fd | i32 | the descriptor: 1 for stdout, 2 for stderr. |
Returns
i32 1 when it is a terminal, 0 otherwise.sys_readlink :: fn(path: string, @c_out buf: u8[], size: i64) -> i64 readlink(2). Bytes placed in buf (NOT NUL-terminated) or -1.
Linux reads the running binary's path from /proc/self/exe.
Parameters
| path | string | No description. |
| buf | u8[] | No description. |
| size | i64 | No description. |
Returns
i64 sys_realpath :: fn(path: string, @c_out resolved: u8[]) -> CPointer | null realpath(3). Writes the canonical path into resolved and returns it, or
null on failure. resolved must hold at least PATH_MAX bytes.
Parameters
| path | string | No description. |
| resolved | u8[] | No description. |
Returns
CPointer | null sys_fopen :: fn(path: string, mode: string) -> CPointer | null fopen(3). A stream handle, or null when the file cannot be opened.
stdio rather than open(2): the mode is a STRING, so nothing here has to
know the numeric value of O_CREAT, which is 512 on Darwin and 64 on Linux.
Reading proceeds until end of file, so SEEK_END is not needed either.
Parameters
| path | string | the file to open. |
| mode | string | "rb" or "wb". |
Returns
CPointer | null sys_fclose :: fn(stream: CPointer) -> i32 fclose(3). 0 on success.
Parameters
| stream | CPointer | No description. |
Returns
i32 sys_fread :: fn(@c_out buf: u8[], size: i64, count: i64, stream: CPointer) -> i64 fread(3). The number of ITEMS read: short at end of file.
Parameters
| buf | u8[] | receives the bytes. |
| size | i64 | item size in bytes; 1 to read bytes. |
| count | i64 | how many items to read. |
| stream | CPointer | an open stream. |
Returns
i64 sys_fwrite :: fn(buf: u8[], size: i64, count: i64, stream: CPointer) -> i64 fwrite(3). The number of items written; short means failure.
Parameters
| buf | u8[] | No description. |
| size | i64 | No description. |
| count | i64 | No description. |
| stream | CPointer | No description. |
Returns
i64 sys_getcwd :: fn(@c_out buf: u8[], size: i64) -> CPointer | null getcwd(3). Fills buf with the working directory, NUL-terminated, and
returns it, or null when buf is too small or the directory is unreadable.
The length is not reported: read up to the NUL.
Parameters
| buf | u8[] | receives the path. |
| size | i64 | buf's capacity in bytes. |
Returns
CPointer | null sys_chdir :: fn(path: string) -> i32 chdir(2). 0 on success, -1 with errno set.
Parameters
| path | string | the new working directory. |
Returns
i32 sys_mkdir :: fn(path: string, mode: u32) -> i32 mkdir(2). 0 on success, -1 with errno set: EEXIST included, which is
why the caller re-tests the path rather than reading errno.
Parameters
| path | string | directory to create. |
| mode | u32 | permission bits, masked by the process umask. |
Returns
i32 sys_access :: fn(path: string, mode: i32) -> i32 access(2). 0 when the check passes, -1 with errno set otherwise.
F_OK answers existence without a struct stat, which is why exists uses
it: no C struct has to be mirrored to ask the only question being asked.
Parameters
| path | string | No description. |
| mode | i32 | No description. |
Returns
i32 sys_unlink :: fn(path: string) -> i32 unlink(2). 0 on success, -1 with errno set.
NOT remove(3): on POSIX that falls back to rmdir for a directory, so an
empty one would vanish through a call named remove_FILE.
Parameters
| path | string | No description. |
Returns
i32 sys_clock_gettime :: fn(clock_id: i32, @c_out ts: i64[]) -> i32 clock_gettime(2). Fills ts: two cells standing in for struct timespec,
both members being 64-bit here: with seconds then nanoseconds. -1 on failure.
Parameters
| clock_id | i32 | No description. |
| ts | i64[] | No description. |
Returns
i32 Constants
STDOUT: i32 :: 1 Standard output's file descriptor.
STDERR: i32 :: 2 Standard error's file descriptor.
CLOCK_REALTIME: i32 :: 0 CLOCK_REALTIME: the wall clock.
CLOCK_MONOTONIC: i32: if VADER_OS == .Darwin { 6 } else { 1 } CLOCK_MONOTONIC. The value is not portable: 6 on Darwin, 1 elsewhere.
EPERM: i32: 1 errno values std/io classifies.
The low numbers are fixed by history and identical on Darwin and Linux; the
ones that are not say so on their own line. A raw errno is a PLATFORM
value: compare against these, never against a literal.
ENOENT: i32: 2 EINTR: i32: 4 EIO: i32: 5 EBADF: i32: 9 EACCES: i32: 13 EEXIST: i32: 17 ENOTDIR: i32: 20 EISDIR: i32: 21 EINVAL: i32: 22 ENOSPC: i32: 28 EPIPE: i32: 32 EAGAIN: i32: if VADER_OS == .Darwin { 35 } else { 11 } Diverges: 35 on Darwin, 11 on Linux, where EDEADLK sits on the other.
ENOTEMPTY: i32: if VADER_OS == .Darwin { 66 } else { 39 } Diverges: 66 on Darwin, 39 on Linux.
PATH_MISSING: i32: 0 The four answers sys_path_kind gives. Values must match vader_path_kind.
PATH_REGULAR: i32: 1 PATH_DIRECTORY: i32: 2 PATH_OTHER: i32: 3 Device, FIFO, socket: anything with no size to speak of.
SPAWN_ACTIONS_BYTES: i64: 128 Bytes to reserve for a posix_spawn_file_actions_t.
An OPAQUE buffer: Vader never looks inside, the spawn_file_actions_* calls
fill it. Nothing checks this number, so it is taken HIGH: MEASURED at 8 on
Darwin (the type is a bare void*) and 80 on glibc: the same on x86_64,
aarch64 and musl.
⚠️ Do NOT narrow this to what one system reports.
WNOHANG: i32: 1 WNOHANG: return immediately rather than waiting. 1 on both systems.
SIGKILL: i32: 9 SIGKILL. 9 on both systems.
POLLIN: i16: 1 POLLIN: data is available to read. 1 on both systems.
TERMIOS_BYTES: i64: 72 Bytes to reserve for a struct termios. The LARGER of the measured sizes,
deliberately, and not a per-system value.
MEASURED: 72 on Darwin, 60 on Linux, x86_64 and arm64 alike, the type being
built from fixed-width members. A SIZE, not an offset, which is why no
architecture axis is needed where a struct stat mirror would have wanted one.
⚠️ Do NOT narrow this to the per-system figure. The risk is asymmetric:
over-reserving wastes 12 bytes on Linux, under-reserving is an out-of-bounds
write by tcgetattr, which nothing here checks. One constant, taken high, is
the shape that cannot be got wrong.
TCSAFLUSH: i32: 2 TCSAFLUSH: apply after draining output, DISCARDING pending input. That
last part is why raw mode uses it: keystrokes meant for the shell must not
land in the prompt. 2 on both systems.
TIOCGWINSZ: u64: if VADER_OS == .Darwin { 0x4008_7468 } else { 0x5413 } TIOCGWINSZ. A macro, and its value is not portable: it encodes the
direction and the payload size, which differ between the two ioctl schemes.
SC_NPROCESSORS_ONLN: i32: if VADER_OS == .Darwin { 58 } else { 84 } _SC_NPROCESSORS_ONLN: the processors currently online. The value is not
portable: 58 on Darwin, 84 on Linux.
F_OK: i32 :: 0 access(2) mode: does the entry exist at all.