sys
sys is the one layer of the standard library that talks to the operating system directly. Everything above it — String, the collections, the I/O buffering, the upper net layers — is pure Cryo. Below it sits each platform's native system interface, selected per target.
| Item | Import |
|---|---|
read, write, open, close, mmap, munmap, exit | import std::sys; |
Syscall numbers, sys_callN, typed wrappers, Win32 / NT | import std::sys::syscall; |
This is a low-level seam, not the everyday I/O surface. Almost everything you want is one layer up in
io,fs, orffi::libc.
Backends
- Linux — the raw kernel syscall, issued through inline assembly with no libc involvement. On Linux the kernel syscall interface is the stable ABI; glibc is a wrapper over it.
- Everything else — a thin pass-through to
ffi::libc. On Windows the eventual native surface is kernel32 and ntdll, since there the DLLs are the stable ABI.
The error model
Every fallible sys function returns a signed integer.
- A negative result is
-errno— the negated POSIX error number, so-EINTRis-4and-EBADFis-9. - A non-negative result is the success value: a byte count, a file descriptor, or
0.
This is normalized at the seam, independent of the backend. A raw Linux syscall already returns -errno in the result register and is passed through as-is; the libc arms read errno after a -1 return and negate it.
Nothing here retries or builds a rich error. A caller that wants Result and IoError and EINTR retry builds that on top — that policy lives above sys, in io::fd.
The seam surface
| Function | Returns |
|---|---|
read(fd: i32, buf: u8*, count: u64) | Bytes read (0 is EOF), or -errno. |
write(fd: i32, buf: u8*, count: u64) | Bytes written — short writes are normal. |
open(path: u8*, flags: i32, mode: u32) | The new fd, or -errno. |
close(fd: i32) | 0, or -errno. |
mmap(addr, length, prot, flags, fd, offset) | The mapped address as a non-negative i64. |
munmap(addr: u8*, length: u64) | 0, or -errno. |
exit(code: i32) | Does not return. |
A few details that bite if you don't know them:
read and write are single raw calls with no EINTR retry — loop on -EINTR yourself if you need it.
open forces O_BINARY on the libc arms. The seam presents POSIX byte semantics, and MSVCRT would otherwise default the descriptor to text mode, where CRLF translation makes byte counts and contents stop matching what was passed. On POSIX targets O_BINARY is 0, so it costs nothing.
mmap is a Unix-only seam surface; Windows callers use VirtualAlloc through sys::syscall directly, which is per-DLL rather than per-libc so there is nothing to wean off. x86-64 user addresses fit comfortably in the positive i64 range, so the sign carries the success/failure distinction losslessly.
exit is exit_group(2) natively, ending every thread in the process — Syscall::exit alone would end only the calling thread. It runs no libc atexit handlers, and unflushed buffered writers lose their contents, so flush first.
sys::syscall
Two distinct surfaces live here, each gated by target so the other platform's symbols never reach the linker.
Linux x86-64 gets the full syscall number table, typed wrappers for the common cases (sys_read, sys_clone3, sys_io_uring_setup, and so on), and the generic sys_call0 through sys_call6 entry points that issue the raw syscall instruction via inline assembly.
Windows x86-64 gets the Win32 system surface — kernel32, user32, advapi32 — plus a small NT API surface from ntdll for programs that want to skip Win32.
When to reach for this instead of ffi::libc
Reach for ffi::libc first. Its functions go through glibc's well-tested wrappers and are portable across Unix variants. Drop down to sys::syscall only when:
- You need a syscall libc doesn't expose —
gettid,pidfd_open,io_uring_setup,bpf,userfaultfd,clone3. - You are writing a stub libc and want to avoid recursion.
- You need bit-exact control over which syscall a code path issues: kernel fuzzing, eBPF tracers, sandbox enforcement.
- You are on Windows and want the Win32 surface directly rather than through a libc personality.
ABI notes
The Linux numbers are the x86-64 ABI from <asm/unistd_64.h>. arm64, i386, and riscv64 use different numbers; a future per-arch split will gate that block.
The Windows bindings target the x64 ABI (the Microsoft x64 calling convention). x86 stdcall is not supported.