Skip to content
CryoCryo home
Stdlibsys

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.