Skip to content

Architecture

Helctic is split into a Rust kernel core and a Zig personality layer. They are linked into a single kernel image and communicate across a flat C ABI.

The syscall path

Userspace (Linux / PE / Darwin / native binary)

    ▼  CPU trap (syscall / SVC / ECALL)
Arch trap handler (Rust)        — extracts (a, b, c, d, e, f) from registers

    ▼  personality_syscall(a..f, personality)   [C ABI / FFI]
Zig personality dispatch        — table lookup by personality, argument and
    │                             structure translation, return-value/errno
    │                             encoding per OS convention
    │  kernel-ops callback table

Rust kernel internals           — scheme ops, memory, process, time, futex

The arch handler does not interpret the syscall. It forwards the raw register values plus the current context's personality to personality_syscall() in the Zig layer, which decides everything else.

The FFI boundary

Two symbols cross the boundary:

rust
unsafe extern "C" {
    // Install the kernel-ops callback table (called once, early in boot).
    fn personality_init(ops: *const KernelOps);

    // Dispatch a syscall under a given personality.
    fn personality_syscall(
        a: usize, b: usize, c: usize, d: usize, e: usize, f: usize,
        personality: u32,
    ) -> usize;
}

The kernel-ops table

KernelOps is a #[repr(C)] struct of function pointers — one per primitive kernel operation (open, read, write, mmap, exit, getpid, …) plus reserved slots for future growth. The field order must match exactly across three places:

  • src/syscall_handler/mod.rs (the Rust struct)
  • personality/src/main.zig (the Zig extern struct)
  • personality/include/personality.h (the C reference header)

Each callback returns i32 using the kernel convention: non-negative on success, negative -errno on failure. The Zig side sign-extends and re-encodes this into whatever the target OS expects (e.g. negative-errno-in-rax for Linux, NTSTATUS for Windows, carry-flag for Darwin/BSD).

Lifetime

The ops table is allocated and Box::leak'd so it lives for the whole kernel lifetime, then handed to personality_init() during kmain — before any userspace code runs. If it isn't installed, every personality syscall returns ENOSYS.

Dispatch inside Zig

zig
fn dispatch(personality, ops, a, b, c, d, e, f) {
    switch (personality) {
        .kernel  => // native syscall table → invoke ops by index
        .linux   => // Linux x86_64 table, optional arg/struct translation
        .windows, .darwin, .freebsd, .netbsd, .openbsd => ENOSYS (stubs)
    }
}

For the native personality, dispatch is essentially a pass-through: look up the op index and call the corresponding kernel-ops callback. For Linux, entries may be marked as needing translation, in which case a shim rewrites arguments/flags before calling the kernel op.

The Rust core

SubsystemWhereRole
Schemes (VFS)src/scheme/uniform open/read/write/close surface
Memorysrc/memory/, rmm/frames, paging, grants, allocators
Contextssrc/context/threads/processes, scheduling, per-context state
Syscall handlersrc/syscall_handler/FFI, personality enum, ops callbacks
ELF loadersrc/elf.rszero-copy ELF reader
btrfssrc/btrfs/ + btrfs/ (Zig)filesystem via C ABI

Build-time integration

build.rs compiles the Zig libraries (libpersonality.a, libbtrfs.a) and links them into the kernel. The architecture is taken from CARGO_CFG_TARGET_ARCH so custom JSON target specs work without invoking rustc directly. See Building.

Released under the AWFixer Source Available License v0.4. #linuswasright