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, futexThe 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:
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 Zigextern 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
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
| Subsystem | Where | Role |
|---|---|---|
| Schemes (VFS) | src/scheme/ | uniform open/read/write/close surface |
| Memory | src/memory/, rmm/ | frames, paging, grants, allocators |
| Contexts | src/context/ | threads/processes, scheduling, per-context state |
| Syscall handler | src/syscall_handler/ | FFI, personality enum, ops callbacks |
| ELF loader | src/elf.rs | zero-copy ELF reader |
| btrfs | src/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.