syscall
import std::sys::syscall; · source
Two surfaces, gated by target
// Linux x86-64
type enum Syscall : i64 {
read = 0;
write = 1;
open = 2;
close = 3;
stat = 4;
// ... the full <asm/unistd_64.h> table
}
![target(linux)] function sys_call0(num: Syscall) -> i64;
![target(linux)] function sys_call1(num: Syscall, a: i64) -> i64;
// ... through
![target(linux)] function sys_call6(num: Syscall, a: i64, b: i64, c: i64, d: i64, e: i64, f: i64) -> i64;
![target(linux)] function sys_read(fd: i32, buf: u8*, count: u64) -> i64;
![target(linux)] function sys_write(fd: i32, buf: u8*, count: u64) -> i64;
![target(linux)] function sys_open(path: u8*, flags: i32, mode: u32) -> i32;
![target(linux)] function sys_close(fd: i32) -> i32;
![target(linux)] function sys_mmap(addr: u8*, length: u64, prot: i32, flags: i32, fd: i32, offset: i64) -> u8*;
![target(linux)] function sys_fork() -> i32;
![target(linux)] function sys_execve(path: u8*, argv: u8**, envp: u8**) -> i32;
![target(linux)] function sys_gettid() -> i32;
![target(linux)] function sys_pidfd_open(pid: i32, flags: u32) -> i32;
![target(linux)] function sys_clone3(args: u8*, size: u64) -> i64;
![target(linux)] function sys_io_uring_setup(entries: u32, params: u8*) -> i32;
// ... ~120 typed wrappers
// Windows x86-64
type struct FILETIME { ... }
type struct SYSTEMTIME { ... }
type struct SECURITY_ATTRIBUTES { ... }
type struct OVERLAPPED { ... }
type struct STARTUPINFOA { ... }
type struct PROCESS_INFORMATION { ... }
type struct LARGE_INTEGER { ... }
![target(windows)] ![link("kernel32")]
extern "C" {
function GetLastError() -> u32;
function CreateFileA(...) -> void*;
function VirtualAlloc(...) -> void*;
// ... kernel32, user32, advapi32, ws2_32, ntdll
}
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 as the Syscall enum, 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. The typed wrappers return whatever the kernel returns — -errno on failure — with no normalisation beyond the cast to the natural return type.
Windows x86-64 gets the Win32 system surface — kernel32, user32, advapi32, ws2_32 — plus a small NT API surface from ntdll for programs that want to skip Win32. The handful of structs the calls need (FILETIME, STARTUPINFOA, PROCESS_INFORMATION, ...) are declared with their real layouts; the ![link(...)] attribute names the import library each block resolves against.
Constants have no link-time presence, so the syscall numbers and the Win32 constants are declared unconditionally. Only the extern "C" blocks and the typed wrappers that call into them carry ![target(...)] gates.
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, the Landlock family. - 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.