feat(rumpk): Pure Zig libc stubs - Freestanding Doctrine

THE GHOST IN THE MACHINE IS EXORCISED
======================================

Rumpk now builds with ZERO C source files.
All libc functions are implemented in pure Zig.

WHAT CHANGED
------------
- NEW: hal/stubs.zig - Pure Zig libc implementation
  - memcpy, memset, memmove, memcmp
  - strlen, strcmp, strcpy
  - malloc, free, realloc, calloc (bump allocator)
  - printf, puts, putchar (route to UART)
  - exit, abort (halt CPU)
  - signal, raise (no-op stubs)

- REMOVED: core/cstubs.c dependency from build
  - C code is now only Nim's generated IR

- UPDATED: kernel.nim
  - Removed malloc/free/realloc exports
  - Now imports from Zig stubs

- UPDATED: build.sh
  - Compiles hal/stubs.zig separately
  - Links stubs.o with hal.o and nimcache/*.o

DOCTRINE DOCUMENT
-----------------
- .agents/steering/FREESTANDING-DOCTRINE.md
  - Codifies the 'Pure Zig ABI' principle
  - Documents build requirements
  - Lists all exported symbols

VERIFICATION
------------
$ file build/rumpk.elf
ELF 64-bit LSB executable, ARM aarch64, statically linked

$ qemu-system-aarch64 -M virt -kernel build/rumpk.elf
[Rumpk L0] Zig HAL Initialized
[Rumpk L1] Nim Kernel Alive!
[Rumpk L1] The Rubicon is crossed.
[Rumpk L1] Zig + Nim = Sovereign Metal.

This proves:
- POSIX is optional
- GCC is optional
- glibc/musl is optional
- We are the standard library now
This commit is contained in:
Markus Maiwald 2025-12-30 07:44:25 +01:00
parent 3b755cac06
commit 5c3a8e3713
5 changed files with 229 additions and 55 deletions

View File

@ -1,8 +1,8 @@
0
12268 9459532 1767076443800031133 c56105a5a3a93a0d4d451c619dc2b349 0 /home/markus/zWork/_Git/Nexus/core/rumpk/build/nimcache/@mkernel.nim.c
10486 9461201 1767076985098986152 3747702445e0bcda6de1816ec7a47cdd 0 /home/markus/zWork/_Git/Nexus/core/rumpk/build/nimcache/@mkernel.nim.c
19164 69191110 1749873121000000000 fe5756ed84745fc96fd9dfb15050f599 0 /usr/lib/nim/nimbase.h
639 9459383 1767076381899751290 1b9448bcfa47e3161459266750e8ded4 0 /home/markus/zWork/_Git/Nexus/core/rumpk/core/include/limits.h
268 9459347 1767076422997272233 06a4c7da1c4987981a369ef3e003bda3 0 /home/markus/zWork/_Git/Nexus/core/rumpk/core/include/stddef.h
1172 78925833 1761046366000000000 69b529ccb10bbb5d826c563cf9b929c1 1 include/stdbool.h
31054 78925836 1761046366000000000 1df950c62cbc96dd5d9790733bbe6016 1 include/stdint.h
155 9459777 1767076495338437553 9cc523d7a8a3a0bbc7c7af0fabeafc0b 0 /home/markus/zWork/_Git/Nexus/core/rumpk/core/include/stdbool.h
924 9459799 1767076530759032485 73bc6834aef9958f6652470b63d7814b 0 /home/markus/zWork/_Git/Nexus/core/rumpk/core/include/stdint.h
499 9459330 1767076360432003062 357ccd6329b0128cce0610c1443c600d 0 /home/markus/zWork/_Git/Nexus/core/rumpk/core/include/string.h

Binary file not shown.

View File

@ -1,6 +1,8 @@
#!/bin/bash
# Rumpk Build Script
# Builds Zig L0 + Nim L1 into a single ELF
# Rumpk Build Script - Freestanding Doctrine
# Pure Zig L0 + Nim L1, no C stubs, no glibc
#
# We are the standard library now.
set -e
@ -8,7 +10,8 @@ RUMPK_DIR="$(cd "$(dirname "$0")" && pwd)"
BUILD_DIR="$RUMPK_DIR/build"
echo "╔═══════════════════════════════════════╗"
echo "║ RUMPK BUILD SYSTEM v0.1 ║"
echo "║ RUMPK FREESTANDING BUILD v0.2 ║"
echo "║ No glibc. Pure Sovereignty. ║"
echo "╚═══════════════════════════════════════╝"
echo ""
@ -17,25 +20,31 @@ mkdir -p "$BUILD_DIR"
mkdir -p "$BUILD_DIR/nimcache"
# =========================================================
# Step 1: Compile Zig L0 (HAL)
# Step 1: Compile Zig L0 (HAL + Stubs)
# =========================================================
echo "[1/3] Compiling Zig L0 (HAL)..."
echo "[1/4] Compiling Zig L0 (HAL + libc stubs)..."
# Compile main.zig
zig build-obj \
"$RUMPK_DIR/hal/main.zig" \
-target aarch64-freestanding-none \
-O ReleaseSmall \
-femit-bin="$BUILD_DIR/hal.o"
# Compile stubs.zig (our libc replacement)
zig build-obj \
"$RUMPK_DIR/hal/stubs.zig" \
-target aarch64-freestanding-none \
-O ReleaseSmall \
-femit-bin="$BUILD_DIR/stubs.o"
echo "$BUILD_DIR/hal.o"
echo "$BUILD_DIR/stubs.o"
# =========================================================
# Step 2: Compile Nim L1 (Kernel)
# =========================================================
echo "[2/3] Compiling Nim L1 (Kernel)..."
# Note: This requires careful Nim configuration for freestanding
# For now, we'll try direct compilation with clang
echo "[2/4] Compiling Nim L1 (Kernel)..."
nim c \
--cpu:arm64 \
@ -61,17 +70,6 @@ echo " → $BUILD_DIR/nimcache/*.c"
# =========================================================
echo "[3/4] Compiling Nim C files..."
# First compile cstubs.c
echo " Compiling cstubs.c..."
zig cc \
-target aarch64-freestanding-none \
-ffreestanding \
-fno-builtin \
-I"$RUMPK_DIR/core/include" \
-c "$RUMPK_DIR/core/cstubs.c" \
-o "$BUILD_DIR/cstubs.o"
# Now compile Nim C files
for cfile in "$BUILD_DIR/nimcache"/*.c; do
ofile="${cfile%.c}.o"
zig cc \
@ -89,9 +87,9 @@ done
echo "$BUILD_DIR/nimcache/*.o"
# =========================================================
# Step 4: Link Everything
# Step 4: Link Everything (NO LIBC!)
# =========================================================
echo "[4/4] Linking..."
echo "[4/4] Linking (freestanding, no libc)..."
# Collect all Nim object files
NIM_OBJS=$(find "$BUILD_DIR/nimcache" -name "*.o" 2>/dev/null | tr '\n' ' ')
@ -106,7 +104,7 @@ zig cc \
-nostdlib \
-T "$RUMPK_DIR/boot/linker.ld" \
"$BUILD_DIR/hal.o" \
"$BUILD_DIR/cstubs.o" \
"$BUILD_DIR/stubs.o" \
$NIM_OBJS \
-o "$BUILD_DIR/rumpk.elf"
@ -116,7 +114,10 @@ echo " → $BUILD_DIR/rumpk.elf"
# Done
# =========================================================
echo ""
echo "✅ Build complete!"
echo "✅ Freestanding build complete!"
echo ""
echo "Verification:"
echo " file $BUILD_DIR/rumpk.elf"
echo ""
echo "Run with:"
echo " qemu-system-aarch64 -M virt -cpu cortex-a57 -nographic -kernel $BUILD_DIR/rumpk.elf"

View File

@ -42,36 +42,14 @@ proc nimPanic(msg: cstring) {.exportc: "panic", cdecl, noreturn.} =
rumpk_halt()
# =========================================================
# Memory Allocator Stubs (Required for ARC on freestanding)
# Memory Allocator - Provided by Zig L0 (hal/stubs.zig)
# =========================================================
# Static heap for bare metal (64KB)
var heapBase {.exportc.}: array[64 * 1024, byte]
var heapOffset {.exportc.}: csize_t = 0
proc allocImpl(size: csize_t): pointer {.exportc: "malloc", cdecl.} =
if heapOffset + size > csize_t(heapBase.len):
return nil
result = addr heapBase[heapOffset]
heapOffset += size
proc deallocImpl(p: pointer) {.exportc: "free", cdecl.} =
# Bump allocator - no dealloc
discard
proc reallocImpl(p: pointer, size: csize_t): pointer {.exportc: "realloc", cdecl.} =
# Simple realloc - just allocate new (wasteful but works)
result = allocImpl(size)
# Nim's internal allocation hooks
proc rawAlloc(size: Natural): pointer {.exportc: "rawAlloc", cdecl.} =
result = allocImpl(csize_t(size))
proc rawDealloc(p: pointer) {.exportc: "rawDealloc", cdecl.} =
deallocImpl(p)
proc rawRealloc(p: pointer, size: Natural): pointer {.exportc: "rawRealloc", cdecl.} =
result = reallocImpl(p, csize_t(size))
# Zig exports: malloc, free, realloc, calloc
# We just import them for any explicit usage
proc malloc(size: csize_t): pointer {.importc, cdecl.}
proc free(p: pointer) {.importc, cdecl.}
proc realloc(p: pointer, size: csize_t): pointer {.importc, cdecl.}
# =========================================================
# Kernel Main Entry

195
hal/stubs.zig Normal file
View File

@ -0,0 +1,195 @@
// VOXIS FORGE // RUMPK L0
// libc_stubs.zig
// We are the standard library now.
//
// These C ABI functions are exported so Nim's generated C code
// can link against them. No glibc. No musl. Pure sovereignty.
// =========================================================
// Memory Operations (Nim needs these)
// =========================================================
export fn memcpy(dest: [*]u8, src: [*]const u8, len: usize) [*]u8 {
var i: usize = 0;
while (i < len) : (i += 1) {
dest[i] = src[i];
}
return dest;
}
export fn memset(dest: [*]u8, val: c_int, len: usize) [*]u8 {
const v: u8 = @intCast(val & 0xFF);
var i: usize = 0;
while (i < len) : (i += 1) {
dest[i] = v;
}
return dest;
}
export fn memmove(dest: [*]u8, src: [*]const u8, len: usize) [*]u8 {
if (@intFromPtr(dest) < @intFromPtr(src)) {
return memcpy(dest, src, len);
}
// Copy backwards for overlapping regions
var i: usize = len;
while (i > 0) {
i -= 1;
dest[i] = src[i];
}
return dest;
}
export fn memcmp(s1: [*]const u8, s2: [*]const u8, len: usize) c_int {
var i: usize = 0;
while (i < len) : (i += 1) {
if (s1[i] != s2[i]) {
return if (s1[i] < s2[i]) @as(c_int, -1) else @as(c_int, 1);
}
}
return 0;
}
// =========================================================
// String Operations
// =========================================================
export fn strlen(s: [*]const u8) usize {
var len: usize = 0;
while (s[len] != 0) : (len += 1) {}
return len;
}
export fn strcpy(dest: [*]u8, src: [*]const u8) [*]u8 {
var i: usize = 0;
while (src[i] != 0) : (i += 1) {
dest[i] = src[i];
}
dest[i] = 0;
return dest;
}
export fn strcmp(s1: [*]const u8, s2: [*]const u8) c_int {
var i: usize = 0;
while (s1[i] != 0 and s1[i] == s2[i]) : (i += 1) {}
if (s1[i] == s2[i]) return 0;
return if (s1[i] < s2[i]) @as(c_int, -1) else @as(c_int, 1);
}
// =========================================================
// Heap Stubs (Bump Allocator)
// =========================================================
var heap_base: [64 * 1024]u8 = undefined;
var heap_offset: usize = 0;
export fn malloc(size: usize) ?*anyopaque {
if (heap_offset + size > heap_base.len) {
return null;
}
const ptr = &heap_base[heap_offset];
heap_offset += size;
// Align to 8 bytes
heap_offset = (heap_offset + 7) & ~@as(usize, 7);
return ptr;
}
export fn free(ptr: ?*anyopaque) void {
// Bump allocator - no deallocation
_ = ptr;
}
export fn realloc(ptr: ?*anyopaque, size: usize) ?*anyopaque {
// Simple realloc - just allocate new (wasteful but works for bootstrap)
_ = ptr;
return malloc(size);
}
export fn calloc(nmemb: usize, size: usize) ?*anyopaque {
const total = nmemb * size;
const ptr = malloc(total);
if (ptr) |p| {
_ = memset(@ptrCast(p), 0, total);
}
return ptr;
}
// =========================================================
// Stdio Stubs (Route to UART)
// =========================================================
const uart = @import("uart.zig");
export fn puts(s: [*]const u8) c_int {
const len = strlen(s);
uart.write_bytes(s[0..len]);
uart.putc('\n');
return 0;
}
export fn putchar(c: c_int) c_int {
uart.putc(@intCast(c & 0xFF));
return c;
}
export fn printf(fmt: [*]const u8, ...) c_int {
// Minimal printf - just output format string
const len = strlen(fmt);
uart.write_bytes(fmt[0..len]);
return @intCast(len);
}
export fn fprintf(stream: ?*anyopaque, fmt: [*]const u8, ...) c_int {
_ = stream;
return printf(fmt);
}
export fn fflush(stream: ?*anyopaque) c_int {
_ = stream;
return 0;
}
export fn fwrite(ptr: [*]const u8, size: usize, nmemb: usize, stream: ?*anyopaque) usize {
_ = stream;
uart.write_bytes(ptr[0 .. size * nmemb]);
return nmemb;
}
// =========================================================
// Signal Stubs (No signals in freestanding)
// =========================================================
export fn signal(signum: c_int, handler: ?*anyopaque) ?*anyopaque {
_ = signum;
_ = handler;
return null;
}
export fn raise(sig: c_int) c_int {
_ = sig;
return 0;
}
// =========================================================
// Exit Stubs
// =========================================================
fn halt() noreturn {
while (true) {
asm volatile ("wfi");
}
}
export fn exit(status: c_int) noreturn {
_ = status;
uart.puts("[RUMPK] exit() called - halt\n");
halt();
}
export fn abort() noreturn {
uart.puts("[RUMPK] abort() called - halt\n");
halt();
}
export fn _Exit(status: c_int) noreturn {
exit(status);
}