feat: recover M3-M4 untracked files, add .gitignore
- Add ARM64 support files never committed to monorepo: entry_aarch64.zig, gic.zig, virtio_mmio.zig, littlefs_hal.zig, linker_aarch64.ld, linker_user_aarch64.ld, run_aarch64.sh - Add build scripts: build_full.sh, build_nim.sh, build_lwip.sh - Add Libertaria LWF adapters: lwf_adapter.zig, lwf_membrane.zig - Add LittleFS bridge: lfs_bridge.nim, lfs_rumpk.h - Add freestanding headers: math.h, stdio.h, stdlib.h - Add .gitignore blocking build artifacts and internal dirs
This commit is contained in:
parent
5e476f76fa
commit
72caf911b1
|
|
@ -0,0 +1,54 @@
|
||||||
|
# Build artifacts
|
||||||
|
build/
|
||||||
|
zig-out/
|
||||||
|
.zig-cache/
|
||||||
|
nimcache/
|
||||||
|
*.o
|
||||||
|
*.a
|
||||||
|
*.elf
|
||||||
|
*.img
|
||||||
|
*.bin
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Kernel build intermediates
|
||||||
|
build_full.log
|
||||||
|
current_run.elf
|
||||||
|
kernel_cache.elf
|
||||||
|
kernel_final.elf
|
||||||
|
abi.o
|
||||||
|
|
||||||
|
# Nim cache
|
||||||
|
build/nimcache/
|
||||||
|
build/init_nimcache/
|
||||||
|
build/lwip_objs/
|
||||||
|
|
||||||
|
# InitRD build outputs (regenerated)
|
||||||
|
build/sysro/
|
||||||
|
build/initrd.tar
|
||||||
|
build/embed_initrd.S
|
||||||
|
build/init
|
||||||
|
build/head.o
|
||||||
|
build/head.S
|
||||||
|
build/head_user.o
|
||||||
|
build/head_user.S
|
||||||
|
build/disk.img
|
||||||
|
build/disk_aarch64.img
|
||||||
|
build/clib_user.o
|
||||||
|
build/dummy.c
|
||||||
|
|
||||||
|
# IDE / Editor
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# OS files
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Agent / internal (must never appear)
|
||||||
|
.agent/
|
||||||
|
.claude/
|
||||||
|
.kiro/
|
||||||
|
competitors/
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
/* Memory Layout — ARM64 Cellular Memory (M3.3):
|
||||||
|
* User RAM: 0x48000000 - 0x4FFFFFFF (128MB)
|
||||||
|
* Stack starts at 0x4BFFFFF0 and grows down
|
||||||
|
* QEMU virt: -m 512M ensures valid physical backing
|
||||||
|
*/
|
||||||
|
MEMORY
|
||||||
|
{
|
||||||
|
RAM (rwx) : ORIGIN = 0x48000000, LENGTH = 128M
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTIONS
|
||||||
|
{
|
||||||
|
. = 0x48000000;
|
||||||
|
|
||||||
|
.text : {
|
||||||
|
*(.text._start)
|
||||||
|
*(.text)
|
||||||
|
*(.text.*)
|
||||||
|
} > RAM
|
||||||
|
|
||||||
|
.rodata : {
|
||||||
|
*(.rodata)
|
||||||
|
*(.rodata.*)
|
||||||
|
} > RAM
|
||||||
|
|
||||||
|
.data : {
|
||||||
|
*(.data)
|
||||||
|
*(.data.*)
|
||||||
|
} > RAM
|
||||||
|
|
||||||
|
.nexus.manifest : {
|
||||||
|
KEEP(*(.nexus.manifest))
|
||||||
|
} > RAM
|
||||||
|
|
||||||
|
.bss : {
|
||||||
|
. = ALIGN(8);
|
||||||
|
__bss_start = .;
|
||||||
|
*(.bss)
|
||||||
|
*(.bss.*)
|
||||||
|
*(COMMON)
|
||||||
|
. = ALIGN(8);
|
||||||
|
__bss_end = .;
|
||||||
|
} > RAM
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,54 @@
|
||||||
|
/* Rumpk Linker Script (AArch64)
|
||||||
|
* For QEMU virt machine (ARM64)
|
||||||
|
* Load address: 0x40080000 (QEMU -kernel default for virt)
|
||||||
|
*/
|
||||||
|
|
||||||
|
ENTRY(_start)
|
||||||
|
|
||||||
|
SECTIONS
|
||||||
|
{
|
||||||
|
. = 0x40080000;
|
||||||
|
PROVIDE(__kernel_vbase = .);
|
||||||
|
PROVIDE(__kernel_pbase = .);
|
||||||
|
|
||||||
|
.text : {
|
||||||
|
*(.text._start)
|
||||||
|
*(.text*)
|
||||||
|
}
|
||||||
|
|
||||||
|
.rodata : {
|
||||||
|
*(.rodata*)
|
||||||
|
}
|
||||||
|
|
||||||
|
.data : {
|
||||||
|
. = ALIGN(16);
|
||||||
|
*(.sdata*)
|
||||||
|
*(.sdata.*)
|
||||||
|
*(.data*)
|
||||||
|
}
|
||||||
|
|
||||||
|
.initrd : {
|
||||||
|
_initrd_start = .;
|
||||||
|
KEEP(*(.initrd))
|
||||||
|
_initrd_end = .;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bss : {
|
||||||
|
__bss_start = .;
|
||||||
|
*(.bss*)
|
||||||
|
*(COMMON)
|
||||||
|
__bss_end = .;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stack (NOLOAD) : {
|
||||||
|
. = ALIGN(16);
|
||||||
|
. += 0x100000; /* 1MB Stack */
|
||||||
|
PROVIDE(__stack_top = .);
|
||||||
|
}
|
||||||
|
|
||||||
|
/DISCARD/ : {
|
||||||
|
*(.comment)
|
||||||
|
*(.note*)
|
||||||
|
*(.eh_frame*)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,125 @@
|
||||||
|
#!/usr/bin/env zsh
|
||||||
|
set -e
|
||||||
|
|
||||||
|
ARCH=${1:-riscv64}
|
||||||
|
|
||||||
|
# Architecture-specific settings
|
||||||
|
if [ "$ARCH" = "aarch64" ]; then
|
||||||
|
NIM_CPU="arm64"
|
||||||
|
ZIG_TARGET="aarch64-freestanding-none"
|
||||||
|
ZIG_CPU="baseline"
|
||||||
|
LINKER_SCRIPT="apps/linker_user_aarch64.ld"
|
||||||
|
BUILD_FLAG="-Darch=aarch64"
|
||||||
|
echo "=== Building NipBox Userland (aarch64) ==="
|
||||||
|
else
|
||||||
|
NIM_CPU="riscv64"
|
||||||
|
ZIG_TARGET="riscv64-freestanding-none"
|
||||||
|
ZIG_CPU="sifive_u54"
|
||||||
|
LINKER_SCRIPT="apps/linker_user.ld"
|
||||||
|
BUILD_FLAG=""
|
||||||
|
echo "=== Building NipBox Userland (riscv64) ==="
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Compile Nim sources to C
|
||||||
|
nim c --cpu:${NIM_CPU} --os:any --compileOnly --mm:arc --opt:size \
|
||||||
|
--stackTrace:off --lineDir:off --nomain --nimcache:build/init_nimcache \
|
||||||
|
-d:noSignalHandler -d:RUMPK_USER -d:nimAllocPagesViaMalloc -d:NIPBOX_LITE \
|
||||||
|
npl/nipbox/nipbox.nim
|
||||||
|
|
||||||
|
# Compile Nim-generated C (check if files exist first)
|
||||||
|
# Skip net_glue (needs LwIP headers not available in userland build)
|
||||||
|
EXTRA_CC_FLAGS=""
|
||||||
|
if [ "$ARCH" = "riscv64" ]; then
|
||||||
|
EXTRA_CC_FLAGS="-mcmodel=medany"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ls build/init_nimcache/*.c 1> /dev/null 2>&1; then
|
||||||
|
for f in build/init_nimcache/*.c; do
|
||||||
|
case "$f" in
|
||||||
|
*net_glue*) echo " [skip] $f (LwIP dependency)"; continue ;;
|
||||||
|
esac
|
||||||
|
zig cc -target ${ZIG_TARGET} -mcpu=${ZIG_CPU} ${EXTRA_CC_FLAGS} \
|
||||||
|
-fno-sanitize=all -fno-vectorize \
|
||||||
|
-I/usr/lib/nim/lib -Icore -Ilibs/membrane -Ilibs/membrane/include \
|
||||||
|
-include string.h \
|
||||||
|
-Os -c "$f" -o "${f%.c}.o"
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Compile clib
|
||||||
|
zig cc -target ${ZIG_TARGET} -mcpu=${ZIG_CPU} ${EXTRA_CC_FLAGS} \
|
||||||
|
-fno-sanitize=all \
|
||||||
|
-DNO_SYS=1 -DOMIT_EXIT -DRUMPK_USER -Ilibs/membrane/include -c libs/membrane/clib.c -o build/clib_user.o
|
||||||
|
|
||||||
|
# Create startup assembly
|
||||||
|
if [ "$ARCH" = "aarch64" ]; then
|
||||||
|
cat > build/head_user.S << 'EOF'
|
||||||
|
.section .text._start
|
||||||
|
.global _start
|
||||||
|
_start:
|
||||||
|
bl NimMain
|
||||||
|
1: wfi
|
||||||
|
b 1b
|
||||||
|
EOF
|
||||||
|
else
|
||||||
|
cat > build/head_user.S << 'EOF'
|
||||||
|
.section .text._start
|
||||||
|
.global _start
|
||||||
|
_start:
|
||||||
|
.option push
|
||||||
|
.option norelax
|
||||||
|
1:auipc gp, %pcrel_hi(__global_pointer$)
|
||||||
|
addi gp, gp, %pcrel_lo(1b)
|
||||||
|
.option pop
|
||||||
|
call NimMain
|
||||||
|
1: wfi
|
||||||
|
j 1b
|
||||||
|
EOF
|
||||||
|
fi
|
||||||
|
|
||||||
|
zig cc -target ${ZIG_TARGET} -mcpu=${ZIG_CPU} ${EXTRA_CC_FLAGS} \
|
||||||
|
-fno-sanitize=all \
|
||||||
|
-c build/head_user.S -o build/head_user.o
|
||||||
|
|
||||||
|
# Link init
|
||||||
|
zig cc -target ${ZIG_TARGET} -mcpu=${ZIG_CPU} ${EXTRA_CC_FLAGS} -nostdlib \
|
||||||
|
-fno-sanitize=all \
|
||||||
|
-T ${LINKER_SCRIPT} -Wl,--gc-sections \
|
||||||
|
build/head_user.o build/init_nimcache/*.o build/clib_user.o \
|
||||||
|
-o build/init
|
||||||
|
|
||||||
|
echo "✓ NipBox binary built (${ARCH})"
|
||||||
|
file build/init
|
||||||
|
|
||||||
|
# Create initrd
|
||||||
|
mkdir -p build/sysro/bin
|
||||||
|
cp build/init build/sysro/init
|
||||||
|
if [ "$ARCH" = "riscv64" ] && [ -f vendor/mksh/mksh.elf ]; then
|
||||||
|
cp vendor/mksh/mksh.elf build/sysro/bin/mksh
|
||||||
|
fi
|
||||||
|
cd build/sysro
|
||||||
|
tar --format=ustar -cf ../initrd.tar *
|
||||||
|
cd ../..
|
||||||
|
|
||||||
|
# Embed initrd
|
||||||
|
cat > build/embed_initrd.S << EOF
|
||||||
|
.section .rodata
|
||||||
|
.global _initrd_start
|
||||||
|
.global _initrd_end
|
||||||
|
.align 4
|
||||||
|
_initrd_start:
|
||||||
|
.incbin "$(pwd)/build/initrd.tar"
|
||||||
|
_initrd_end:
|
||||||
|
EOF
|
||||||
|
|
||||||
|
zig cc -target ${ZIG_TARGET} -mcpu=${ZIG_CPU} ${EXTRA_CC_FLAGS} \
|
||||||
|
-c build/embed_initrd.S -o build/initrd.o
|
||||||
|
|
||||||
|
cp build/initrd.tar hal/initrd.tar
|
||||||
|
|
||||||
|
# Build kernel
|
||||||
|
rm -f zig-out/lib/librumpk_hal.a
|
||||||
|
zig build ${BUILD_FLAG}
|
||||||
|
|
||||||
|
echo "=== BUILD COMPLETE (${ARCH}) ==="
|
||||||
|
ls -lh build/init zig-out/bin/rumpk.elf
|
||||||
|
|
@ -0,0 +1,61 @@
|
||||||
|
#!/usr/bin/env zsh
|
||||||
|
# Build LwIP as a pure C library without Zig runtime dependencies
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
mkdir -p build/lwip_objs
|
||||||
|
rm -f build/lwip_objs/*.o 2>/dev/null || true
|
||||||
|
|
||||||
|
echo "Building LwIP..."
|
||||||
|
|
||||||
|
# Compile each source file
|
||||||
|
compile() {
|
||||||
|
local src=$1
|
||||||
|
local obj="build/lwip_objs/$(basename ${src%.c}.o)"
|
||||||
|
echo " $src"
|
||||||
|
zig cc -target riscv64-freestanding-none -mcpu=sifive_u54 -mcmodel=medany \
|
||||||
|
-Os -fno-sanitize=all \
|
||||||
|
-DNO_SYS=1 -Icore -Ilibs/membrane -Ilibs/membrane/include \
|
||||||
|
-Ilibs/membrane/external/lwip/src/include \
|
||||||
|
-c "$src" -o "$obj"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Core sources
|
||||||
|
compile "libs/membrane/external/lwip/src/core/init.c"
|
||||||
|
compile "libs/membrane/external/lwip/src/core/def.c"
|
||||||
|
compile "libs/membrane/external/lwip/src/core/dns.c"
|
||||||
|
compile "libs/membrane/external/lwip/src/core/inet_chksum.c"
|
||||||
|
compile "libs/membrane/external/lwip/src/core/ip.c"
|
||||||
|
compile "libs/membrane/external/lwip/src/core/mem.c"
|
||||||
|
compile "libs/membrane/external/lwip/src/core/memp.c"
|
||||||
|
compile "libs/membrane/external/lwip/src/core/netif.c"
|
||||||
|
compile "libs/membrane/external/lwip/src/core/pbuf.c"
|
||||||
|
compile "libs/membrane/external/lwip/src/core/raw.c"
|
||||||
|
compile "libs/membrane/external/lwip/src/core/sys.c"
|
||||||
|
compile "libs/membrane/external/lwip/src/core/tcp.c"
|
||||||
|
compile "libs/membrane/external/lwip/src/core/tcp_in.c"
|
||||||
|
compile "libs/membrane/external/lwip/src/core/tcp_out.c"
|
||||||
|
compile "libs/membrane/external/lwip/src/core/timeouts.c"
|
||||||
|
compile "libs/membrane/external/lwip/src/core/udp.c"
|
||||||
|
|
||||||
|
# IPv4 sources
|
||||||
|
compile "libs/membrane/external/lwip/src/core/ipv4/autoip.c"
|
||||||
|
compile "libs/membrane/external/lwip/src/core/ipv4/dhcp.c"
|
||||||
|
compile "libs/membrane/external/lwip/src/core/ipv4/etharp.c"
|
||||||
|
compile "libs/membrane/external/lwip/src/core/ipv4/icmp.c"
|
||||||
|
compile "libs/membrane/external/lwip/src/core/ipv4/ip4.c"
|
||||||
|
compile "libs/membrane/external/lwip/src/core/ipv4/ip4_addr.c"
|
||||||
|
compile "libs/membrane/external/lwip/src/core/ipv4/ip4_frag.c"
|
||||||
|
|
||||||
|
# Netif sources
|
||||||
|
compile "libs/membrane/external/lwip/src/netif/ethernet.c"
|
||||||
|
|
||||||
|
# SysArch
|
||||||
|
compile "libs/membrane/sys_arch.c"
|
||||||
|
|
||||||
|
echo "Creating liblwip.a..."
|
||||||
|
mkdir -p zig-out/lib
|
||||||
|
rm -f zig-out/lib/liblwip.a
|
||||||
|
(cd build/lwip_objs && ar rcs ../../zig-out/lib/liblwip.a *.o)
|
||||||
|
echo "Done! liblwip.a created at zig-out/lib/liblwip.a"
|
||||||
|
ls -lh zig-out/lib/liblwip.a
|
||||||
|
|
@ -0,0 +1,126 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# ============================================================================
|
||||||
|
# Rumpk Nim Kernel Build — nim → C → .o (cross-compiled for target arch)
|
||||||
|
# ============================================================================
|
||||||
|
# Usage:
|
||||||
|
# ./build_nim.sh # Default: riscv64
|
||||||
|
# ./build_nim.sh riscv64 # RISC-V 64-bit
|
||||||
|
# ./build_nim.sh aarch64 # ARM64
|
||||||
|
# ./build_nim.sh x86_64 # AMD64
|
||||||
|
#
|
||||||
|
# This script:
|
||||||
|
# 1. Invokes nim c --compileOnly to generate C from Nim
|
||||||
|
# 2. Cross-compiles each .c to .o using zig cc
|
||||||
|
# 3. Outputs to build/nimcache/*.o (consumed by build.zig)
|
||||||
|
# ============================================================================
|
||||||
|
set -euo pipefail
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
|
||||||
|
ARCH="${1:-riscv64}"
|
||||||
|
|
||||||
|
# ---- Validate architecture ----
|
||||||
|
case "$ARCH" in
|
||||||
|
riscv64)
|
||||||
|
ZIG_TARGET="riscv64-freestanding-none"
|
||||||
|
ZIG_CPU="-mcpu=sifive_u54"
|
||||||
|
ZIG_MODEL="-mcmodel=medany"
|
||||||
|
NIM_CPU="riscv64"
|
||||||
|
;;
|
||||||
|
aarch64)
|
||||||
|
ZIG_TARGET="aarch64-freestanding-none"
|
||||||
|
ZIG_CPU=""
|
||||||
|
ZIG_MODEL="-fno-vectorize"
|
||||||
|
NIM_CPU="arm64"
|
||||||
|
;;
|
||||||
|
x86_64)
|
||||||
|
ZIG_TARGET="x86_64-freestanding-none"
|
||||||
|
ZIG_CPU=""
|
||||||
|
ZIG_MODEL="-mcmodel=kernel"
|
||||||
|
NIM_CPU="amd64"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "ERROR: Unknown architecture '$ARCH'"
|
||||||
|
echo "Supported: riscv64, aarch64, x86_64"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
NIMCACHE="build/nimcache"
|
||||||
|
|
||||||
|
echo "=== Rumpk Nim Build: $ARCH ==="
|
||||||
|
echo " Target: $ZIG_TARGET"
|
||||||
|
echo " Output: $NIMCACHE/"
|
||||||
|
|
||||||
|
# ---- Step 1: Nim → C ----
|
||||||
|
echo ""
|
||||||
|
echo "[1/2] nim c --compileOnly core/kernel.nim"
|
||||||
|
|
||||||
|
nim c \
|
||||||
|
--cpu:"$NIM_CPU" \
|
||||||
|
--os:any \
|
||||||
|
--compileOnly \
|
||||||
|
--mm:arc \
|
||||||
|
--opt:size \
|
||||||
|
--stackTrace:off \
|
||||||
|
--lineDir:off \
|
||||||
|
--nomain \
|
||||||
|
--nimcache:"$NIMCACHE" \
|
||||||
|
-d:noSignalHandler \
|
||||||
|
-d:RUMPK_KERNEL \
|
||||||
|
-d:nimAllocPagesViaMalloc \
|
||||||
|
core/kernel.nim
|
||||||
|
|
||||||
|
C_COUNT=$(ls -1 "$NIMCACHE"/*.c 2>/dev/null | wc -l)
|
||||||
|
echo " Generated $C_COUNT C files"
|
||||||
|
|
||||||
|
# ---- Step 2: C → .o (zig cc cross-compile) ----
|
||||||
|
echo ""
|
||||||
|
echo "[2/2] zig cc → $ZIG_TARGET"
|
||||||
|
|
||||||
|
COMPILED=0
|
||||||
|
FAILED=0
|
||||||
|
|
||||||
|
for cfile in "$NIMCACHE"/*.c; do
|
||||||
|
[ -f "$cfile" ] || continue
|
||||||
|
ofile="${cfile%.c}.o"
|
||||||
|
|
||||||
|
# Skip if .o is newer than .c (incremental)
|
||||||
|
if [ -f "$ofile" ] && [ "$ofile" -nt "$cfile" ]; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
if zig cc \
|
||||||
|
-target "$ZIG_TARGET" \
|
||||||
|
$ZIG_CPU \
|
||||||
|
$ZIG_MODEL \
|
||||||
|
-fno-sanitize=all \
|
||||||
|
-fvisibility=default \
|
||||||
|
-I/usr/lib/nim/lib \
|
||||||
|
-Icore \
|
||||||
|
-Icore/include \
|
||||||
|
-Ilibs/membrane \
|
||||||
|
-Ilibs/membrane/include \
|
||||||
|
-Ilibs/membrane/external/lwip/src/include \
|
||||||
|
-Os \
|
||||||
|
-c "$cfile" \
|
||||||
|
-o "$ofile" 2>/dev/null; then
|
||||||
|
COMPILED=$((COMPILED + 1))
|
||||||
|
else
|
||||||
|
echo " FAIL: $(basename "$cfile")"
|
||||||
|
FAILED=$((FAILED + 1))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
O_COUNT=$(ls -1 "$NIMCACHE"/*.o 2>/dev/null | wc -l)
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== Result ==="
|
||||||
|
echo " Arch: $ARCH"
|
||||||
|
echo " C files: $C_COUNT"
|
||||||
|
echo " Compiled: $COMPILED (incremental skip: $((O_COUNT - COMPILED)))"
|
||||||
|
echo " Objects: $O_COUNT"
|
||||||
|
if [ "$FAILED" -gt 0 ]; then
|
||||||
|
echo " FAILED: $FAILED"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo " Status: OK"
|
||||||
|
|
@ -0,0 +1,129 @@
|
||||||
|
# SPDX-License-Identifier: LSL-1.0
|
||||||
|
# Copyright (c) 2026 Markus Maiwald
|
||||||
|
# Stewardship: Self Sovereign Society Foundation
|
||||||
|
#
|
||||||
|
# This file is part of the Nexus Sovereign Core.
|
||||||
|
# See legal/LICENSE_SOVEREIGN.md for license terms.
|
||||||
|
|
||||||
|
## Rumpk Layer 1: LittleFS Bridge
|
||||||
|
##
|
||||||
|
## Nim FFI wrapper for the Zig-side LittleFS HAL (littlefs_hal.zig).
|
||||||
|
## Provides the API that VFS delegates to for /nexus mount point.
|
||||||
|
##
|
||||||
|
## All calls cross the Nim→Zig→C boundary:
|
||||||
|
## Nim (this file) → Zig (littlefs_hal.zig) → C (lfs.c) → VirtIO-Block
|
||||||
|
|
||||||
|
# --- FFI imports from littlefs_hal.zig (exported as C ABI) ---
|
||||||
|
proc nexus_lfs_format(): int32 {.importc, cdecl.}
|
||||||
|
proc nexus_lfs_mount(): int32 {.importc, cdecl.}
|
||||||
|
proc nexus_lfs_unmount(): int32 {.importc, cdecl.}
|
||||||
|
proc nexus_lfs_open(path: cstring, flags: int32): int32 {.importc, cdecl.}
|
||||||
|
proc nexus_lfs_read(handle: int32, buf: pointer, size: uint32): int32 {.importc, cdecl.}
|
||||||
|
proc nexus_lfs_write(handle: int32, buf: pointer, size: uint32): int32 {.importc, cdecl.}
|
||||||
|
proc nexus_lfs_close(handle: int32): int32 {.importc, cdecl.}
|
||||||
|
proc nexus_lfs_seek(handle: int32, off: int32, whence: int32): int32 {.importc, cdecl.}
|
||||||
|
proc nexus_lfs_size(handle: int32): int32 {.importc, cdecl.}
|
||||||
|
proc nexus_lfs_remove(path: cstring): int32 {.importc, cdecl.}
|
||||||
|
proc nexus_lfs_mkdir(path: cstring): int32 {.importc, cdecl.}
|
||||||
|
proc nexus_lfs_is_mounted(): int32 {.importc, cdecl.}
|
||||||
|
|
||||||
|
# --- LFS open flags (match lfs.h) ---
|
||||||
|
const
|
||||||
|
LFS_O_RDONLY* = 1'i32
|
||||||
|
LFS_O_WRONLY* = 2'i32
|
||||||
|
LFS_O_RDWR* = 3'i32
|
||||||
|
LFS_O_CREAT* = 0x0100'i32
|
||||||
|
LFS_O_EXCL* = 0x0200'i32
|
||||||
|
LFS_O_TRUNC* = 0x0400'i32
|
||||||
|
LFS_O_APPEND* = 0x0800'i32
|
||||||
|
|
||||||
|
# --- LFS seek flags ---
|
||||||
|
const
|
||||||
|
LFS_SEEK_SET* = 0'i32
|
||||||
|
LFS_SEEK_CUR* = 1'i32
|
||||||
|
LFS_SEEK_END* = 2'i32
|
||||||
|
|
||||||
|
# --- Public API for VFS ---
|
||||||
|
|
||||||
|
proc lfs_mount_fs*(): bool =
|
||||||
|
## Mount the LittleFS filesystem. Auto-formats on first boot.
|
||||||
|
return nexus_lfs_mount() == 0
|
||||||
|
|
||||||
|
proc lfs_unmount_fs*(): bool =
|
||||||
|
return nexus_lfs_unmount() == 0
|
||||||
|
|
||||||
|
proc lfs_format_fs*(): bool =
|
||||||
|
return nexus_lfs_format() == 0
|
||||||
|
|
||||||
|
proc lfs_is_mounted*(): bool =
|
||||||
|
return nexus_lfs_is_mounted() != 0
|
||||||
|
|
||||||
|
proc lfs_open_file*(path: cstring, flags: int32): int32 =
|
||||||
|
## Open a file. Returns handle >= 0 on success, < 0 on error.
|
||||||
|
return nexus_lfs_open(path, flags)
|
||||||
|
|
||||||
|
proc lfs_read_file*(handle: int32, buf: pointer, size: uint32): int32 =
|
||||||
|
## Read from file. Returns bytes read or negative error.
|
||||||
|
return nexus_lfs_read(handle, buf, size)
|
||||||
|
|
||||||
|
proc lfs_write_file*(handle: int32, buf: pointer, size: uint32): int32 =
|
||||||
|
## Write to file. Returns bytes written or negative error.
|
||||||
|
return nexus_lfs_write(handle, buf, size)
|
||||||
|
|
||||||
|
proc lfs_close_file*(handle: int32): int32 =
|
||||||
|
return nexus_lfs_close(handle)
|
||||||
|
|
||||||
|
proc lfs_seek_file*(handle: int32, off: int32, whence: int32): int32 =
|
||||||
|
return nexus_lfs_seek(handle, off, whence)
|
||||||
|
|
||||||
|
proc lfs_file_size*(handle: int32): int32 =
|
||||||
|
return nexus_lfs_size(handle)
|
||||||
|
|
||||||
|
proc lfs_remove_path*(path: cstring): int32 =
|
||||||
|
return nexus_lfs_remove(path)
|
||||||
|
|
||||||
|
proc lfs_mkdir_path*(path: cstring): int32 =
|
||||||
|
return nexus_lfs_mkdir(path)
|
||||||
|
|
||||||
|
# --- Convenience: VFS-compatible read/write (path-based, like SFS) ---
|
||||||
|
|
||||||
|
proc lfs_vfs_read*(path: cstring, buf: pointer, max_len: int): int =
|
||||||
|
## Read entire file into buffer. Returns bytes read or -1.
|
||||||
|
let h = nexus_lfs_open(path, LFS_O_RDONLY)
|
||||||
|
if h < 0: return -1
|
||||||
|
let n = nexus_lfs_read(h, buf, uint32(max_len))
|
||||||
|
discard nexus_lfs_close(h)
|
||||||
|
if n < 0: return -1
|
||||||
|
return int(n)
|
||||||
|
|
||||||
|
proc lfs_vfs_write*(path: cstring, buf: pointer, len: int) =
|
||||||
|
## Write buffer to file (create/truncate).
|
||||||
|
let h = nexus_lfs_open(path, LFS_O_WRONLY or LFS_O_CREAT or LFS_O_TRUNC)
|
||||||
|
if h < 0: return
|
||||||
|
discard nexus_lfs_write(h, buf, uint32(len))
|
||||||
|
discard nexus_lfs_close(h)
|
||||||
|
|
||||||
|
proc lfs_vfs_read_at*(path: cstring, buf: pointer, count: uint64,
|
||||||
|
offset: uint64): int64 =
|
||||||
|
## Read `count` bytes starting at `offset`. Returns bytes read.
|
||||||
|
let h = nexus_lfs_open(path, LFS_O_RDONLY)
|
||||||
|
if h < 0: return -1
|
||||||
|
if offset > 0:
|
||||||
|
discard nexus_lfs_seek(h, int32(offset), LFS_SEEK_SET)
|
||||||
|
let n = nexus_lfs_read(h, buf, uint32(count))
|
||||||
|
discard nexus_lfs_close(h)
|
||||||
|
if n < 0: return -1
|
||||||
|
return int64(n)
|
||||||
|
|
||||||
|
proc lfs_vfs_write_at*(path: cstring, buf: pointer, count: uint64,
|
||||||
|
offset: uint64): int64 =
|
||||||
|
## Write `count` bytes at `offset`. Returns bytes written.
|
||||||
|
let flags = LFS_O_WRONLY or LFS_O_CREAT
|
||||||
|
let h = nexus_lfs_open(path, flags)
|
||||||
|
if h < 0: return -1
|
||||||
|
if offset > 0:
|
||||||
|
discard nexus_lfs_seek(h, int32(offset), LFS_SEEK_SET)
|
||||||
|
let n = nexus_lfs_write(h, buf, uint32(count))
|
||||||
|
discard nexus_lfs_close(h)
|
||||||
|
if n < 0: return -1
|
||||||
|
return int64(n)
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,169 @@
|
||||||
|
// SPDX-License-Identifier: LCL-1.0
|
||||||
|
// Copyright (c) 2026 Markus Maiwald
|
||||||
|
// Stewardship: Self Sovereign Society Foundation
|
||||||
|
//
|
||||||
|
// This file is part of the Nexus Commonwealth.
|
||||||
|
// See legal/LICENSE_COMMONWEALTH.md for license terms.
|
||||||
|
|
||||||
|
//! Rumpk Layer 0: GICv2 Driver (ARM64)
|
||||||
|
//!
|
||||||
|
//! Minimal Generic Interrupt Controller v2 for QEMU virt machine.
|
||||||
|
//! Handles interrupt enable, claim, and complete for timer and device IRQs.
|
||||||
|
//!
|
||||||
|
//! SAFETY: All register accesses use volatile pointers to MMIO regions.
|
||||||
|
|
||||||
|
// =========================================================
|
||||||
|
// GICv2 MMIO Base Addresses (QEMU virt machine)
|
||||||
|
// =========================================================
|
||||||
|
|
||||||
|
const GICD_BASE: usize = 0x08000000; // Distributor
|
||||||
|
const GICC_BASE: usize = 0x08010000; // CPU Interface
|
||||||
|
|
||||||
|
// =========================================================
|
||||||
|
// Distributor Registers (GICD)
|
||||||
|
// =========================================================
|
||||||
|
|
||||||
|
const GICD_CTLR: usize = 0x000; // Control
|
||||||
|
const GICD_TYPER: usize = 0x004; // Type (read-only)
|
||||||
|
const GICD_ISENABLER: usize = 0x100; // Set-Enable (banked per 32 IRQs)
|
||||||
|
const GICD_ICENABLER: usize = 0x180; // Clear-Enable
|
||||||
|
const GICD_ISPENDR: usize = 0x200; // Set-Pending
|
||||||
|
const GICD_ICPENDR: usize = 0x280; // Clear-Pending
|
||||||
|
const GICD_IPRIORITYR: usize = 0x400; // Priority (byte-accessible)
|
||||||
|
const GICD_ITARGETSR: usize = 0x800; // Target (byte-accessible)
|
||||||
|
const GICD_ICFGR: usize = 0xC00; // Configuration
|
||||||
|
|
||||||
|
// =========================================================
|
||||||
|
// CPU Interface Registers (GICC)
|
||||||
|
// =========================================================
|
||||||
|
|
||||||
|
const GICC_CTLR: usize = 0x000; // Control
|
||||||
|
const GICC_PMR: usize = 0x004; // Priority Mask
|
||||||
|
const GICC_IAR: usize = 0x00C; // Interrupt Acknowledge
|
||||||
|
const GICC_EOIR: usize = 0x010; // End of Interrupt
|
||||||
|
|
||||||
|
// =========================================================
|
||||||
|
// IRQ Numbers (QEMU virt)
|
||||||
|
// =========================================================
|
||||||
|
|
||||||
|
/// Non-Secure Physical Timer PPI
|
||||||
|
pub const TIMER_IRQ: u32 = 30;
|
||||||
|
|
||||||
|
/// UART PL011 (SPI #1 = IRQ 33)
|
||||||
|
pub const UART_IRQ: u32 = 33;
|
||||||
|
|
||||||
|
/// VirtIO MMIO IRQ base (SPI #16 = IRQ 48)
|
||||||
|
/// QEMU virt assigns SPIs 48..79 to MMIO slots 0..31
|
||||||
|
pub const VIRTIO_MMIO_IRQ_BASE: u32 = 48;
|
||||||
|
|
||||||
|
// Spurious interrupt ID
|
||||||
|
const SPURIOUS_IRQ: u32 = 1023;
|
||||||
|
|
||||||
|
// =========================================================
|
||||||
|
// MMIO Helpers
|
||||||
|
// =========================================================
|
||||||
|
|
||||||
|
fn gicd_read(offset: usize) u32 {
|
||||||
|
const ptr: *volatile u32 = @ptrFromInt(GICD_BASE + offset);
|
||||||
|
return ptr.*;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn gicd_write(offset: usize, val: u32) void {
|
||||||
|
const ptr: *volatile u32 = @ptrFromInt(GICD_BASE + offset);
|
||||||
|
ptr.* = val;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn gicc_read(offset: usize) u32 {
|
||||||
|
const ptr: *volatile u32 = @ptrFromInt(GICC_BASE + offset);
|
||||||
|
return ptr.*;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn gicc_write(offset: usize, val: u32) void {
|
||||||
|
const ptr: *volatile u32 = @ptrFromInt(GICC_BASE + offset);
|
||||||
|
ptr.* = val;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================
|
||||||
|
// Public API
|
||||||
|
// =========================================================
|
||||||
|
|
||||||
|
/// Initialize GICv2 distributor and CPU interface.
|
||||||
|
pub fn gic_init() void {
|
||||||
|
// 1. Disable distributor during setup
|
||||||
|
gicd_write(GICD_CTLR, 0);
|
||||||
|
|
||||||
|
// 2. Set all SPIs to lowest priority (0xFF) and target CPU 0
|
||||||
|
// PPIs (0-31) are banked per-CPU, handled separately
|
||||||
|
const typer = gicd_read(GICD_TYPER);
|
||||||
|
const it_lines = (typer & 0x1F) + 1; // Number of 32-IRQ groups
|
||||||
|
var i: usize = 1; // Skip group 0 (SGIs/PPIs - banked)
|
||||||
|
while (i < it_lines) : (i += 1) {
|
||||||
|
// Disable all SPIs
|
||||||
|
gicd_write(GICD_ICENABLER + i * 4, 0xFFFFFFFF);
|
||||||
|
// Set priority to 0xA0 (low but not lowest)
|
||||||
|
var j: usize = 0;
|
||||||
|
while (j < 8) : (j += 1) {
|
||||||
|
gicd_write(GICD_IPRIORITYR + (i * 32 + j * 4), 0xA0A0A0A0);
|
||||||
|
}
|
||||||
|
// Target CPU 0 for all SPIs
|
||||||
|
j = 0;
|
||||||
|
while (j < 8) : (j += 1) {
|
||||||
|
gicd_write(GICD_ITARGETSR + (i * 32 + j * 4), 0x01010101);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Configure PPI priorities (group 0, banked)
|
||||||
|
// Timer IRQ 30: priority 0x20 (high)
|
||||||
|
const timer_prio_reg = GICD_IPRIORITYR + (TIMER_IRQ / 4) * 4;
|
||||||
|
const timer_prio_shift: u5 = @intCast((TIMER_IRQ % 4) * 8);
|
||||||
|
var prio_val = gicd_read(timer_prio_reg);
|
||||||
|
prio_val &= ~(@as(u32, 0xFF) << timer_prio_shift);
|
||||||
|
prio_val |= @as(u32, 0x20) << timer_prio_shift;
|
||||||
|
gicd_write(timer_prio_reg, prio_val);
|
||||||
|
|
||||||
|
// 4. Enable distributor (Group 0 + Group 1)
|
||||||
|
gicd_write(GICD_CTLR, 0x3);
|
||||||
|
|
||||||
|
// 5. Configure CPU interface
|
||||||
|
gicc_write(GICC_PMR, 0xFF); // Accept all priorities
|
||||||
|
gicc_write(GICC_CTLR, 0x1); // Enable CPU interface
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Enable a specific interrupt in the distributor.
|
||||||
|
pub fn gic_enable_irq(irq: u32) void {
|
||||||
|
const reg = GICD_ISENABLER + (irq / 32) * 4;
|
||||||
|
const bit: u5 = @intCast(irq % 32);
|
||||||
|
gicd_write(reg, @as(u32, 1) << bit);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Disable a specific interrupt in the distributor.
|
||||||
|
pub fn gic_disable_irq(irq: u32) void {
|
||||||
|
const reg = GICD_ICENABLER + (irq / 32) * 4;
|
||||||
|
const bit: u5 = @intCast(irq % 32);
|
||||||
|
gicd_write(reg, @as(u32, 1) << bit);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Acknowledge an interrupt (read IAR). Returns IRQ number or SPURIOUS_IRQ.
|
||||||
|
pub fn gic_claim() u32 {
|
||||||
|
return gicc_read(GICC_IAR) & 0x3FF;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Signal end of interrupt processing.
|
||||||
|
pub fn gic_complete(irq: u32) void {
|
||||||
|
gicc_write(GICC_EOIR, irq);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if a claimed IRQ is spurious.
|
||||||
|
pub fn is_spurious(irq: u32) bool {
|
||||||
|
return irq >= SPURIOUS_IRQ;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Enable the NS Physical Timer interrupt (IRQ 30).
|
||||||
|
pub fn gic_enable_timer_irq() void {
|
||||||
|
gic_enable_irq(TIMER_IRQ);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Enable a VirtIO MMIO slot interrupt in the GIC.
|
||||||
|
pub fn gic_enable_virtio_mmio_irq(slot: u32) void {
|
||||||
|
gic_enable_irq(VIRTIO_MMIO_IRQ_BASE + slot);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,362 @@
|
||||||
|
// SPDX-License-Identifier: LCL-1.0
|
||||||
|
// Copyright (c) 2026 Markus Maiwald
|
||||||
|
// Stewardship: Self Sovereign Society Foundation
|
||||||
|
//
|
||||||
|
// This file is part of the Nexus Commonwealth.
|
||||||
|
// See legal/LICENSE_SOVEREIGN.md for license terms.
|
||||||
|
|
||||||
|
//! Rumpk Layer 0: LittleFS ↔ VirtIO-Block HAL
|
||||||
|
//!
|
||||||
|
//! Translates LittleFS block operations into VirtIO-Block sector I/O.
|
||||||
|
//! Exports C-ABI functions for Nim L1 to call: nexus_lfs_mount, nexus_lfs_format,
|
||||||
|
//! nexus_lfs_open, nexus_lfs_read, nexus_lfs_write, nexus_lfs_close, etc.
|
||||||
|
//!
|
||||||
|
//! Block geometry:
|
||||||
|
//! - LFS block size: 4096 bytes (8 sectors)
|
||||||
|
//! - Sector size: 512 bytes (VirtIO standard)
|
||||||
|
//! - 32MB disk: 8192 blocks
|
||||||
|
|
||||||
|
const BLOCK_SIZE: u32 = 4096;
|
||||||
|
const SECTOR_SIZE: u32 = 512;
|
||||||
|
const SECTORS_PER_BLOCK: u32 = BLOCK_SIZE / SECTOR_SIZE;
|
||||||
|
const BLOCK_COUNT: u32 = 8192; // 32MB / 4096
|
||||||
|
const CACHE_SIZE: u32 = 512;
|
||||||
|
const LOOKAHEAD_SIZE: u32 = 64;
|
||||||
|
|
||||||
|
// --- VirtIO-Block FFI (from virtio_block.zig) ---
|
||||||
|
extern fn virtio_blk_read(sector: u64, buf: [*]u8) void;
|
||||||
|
extern fn virtio_blk_write(sector: u64, buf: [*]const u8) void;
|
||||||
|
|
||||||
|
// --- Kernel print (from Nim L1 kernel.nim, exported as C ABI) ---
|
||||||
|
extern fn kprint(s: [*:0]const u8) void;
|
||||||
|
|
||||||
|
// --- LittleFS C types (must match lfs.h layout exactly) ---
|
||||||
|
// We use opaque pointers and only declare what we need for the config struct.
|
||||||
|
|
||||||
|
const LfsConfig = extern struct {
|
||||||
|
context: ?*anyopaque,
|
||||||
|
read: *const fn (*LfsConfig, u32, u32, ?*anyopaque, u32) callconv(.c) i32,
|
||||||
|
prog: *const fn (*LfsConfig, u32, u32, ?*anyopaque, u32) callconv(.c) i32,
|
||||||
|
erase: *const fn (*LfsConfig, u32) callconv(.c) i32,
|
||||||
|
sync: *const fn (*LfsConfig) callconv(.c) i32,
|
||||||
|
read_size: u32,
|
||||||
|
prog_size: u32,
|
||||||
|
block_size: u32,
|
||||||
|
block_count: u32,
|
||||||
|
block_cycles: i32,
|
||||||
|
cache_size: u32,
|
||||||
|
lookahead_size: u32,
|
||||||
|
compact_thresh: u32,
|
||||||
|
read_buffer: ?*anyopaque,
|
||||||
|
prog_buffer: ?*anyopaque,
|
||||||
|
lookahead_buffer: ?*anyopaque,
|
||||||
|
name_max: u32,
|
||||||
|
file_max: u32,
|
||||||
|
attr_max: u32,
|
||||||
|
metadata_max: u32,
|
||||||
|
inline_max: u32,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Opaque LittleFS types — we let lfs.c manage the internals
|
||||||
|
const LfsT = opaque {};
|
||||||
|
const LfsFileT = opaque {};
|
||||||
|
const LfsInfo = opaque {};
|
||||||
|
|
||||||
|
// --- LittleFS C API (linked from lfs.o) ---
|
||||||
|
extern fn lfs_format(lfs: *LfsT, config: *LfsConfig) callconv(.c) i32;
|
||||||
|
extern fn lfs_mount(lfs: *LfsT, config: *LfsConfig) callconv(.c) i32;
|
||||||
|
extern fn lfs_unmount(lfs: *LfsT) callconv(.c) i32;
|
||||||
|
extern fn lfs_file_open(lfs: *LfsT, file: *LfsFileT, path: [*:0]const u8, flags: i32) callconv(.c) i32;
|
||||||
|
extern fn lfs_file_close(lfs: *LfsT, file: *LfsFileT) callconv(.c) i32;
|
||||||
|
extern fn lfs_file_read(lfs: *LfsT, file: *LfsFileT, buf: [*]u8, size: u32) callconv(.c) i32;
|
||||||
|
extern fn lfs_file_write(lfs: *LfsT, file: *LfsFileT, buf: [*]const u8, size: u32) callconv(.c) i32;
|
||||||
|
extern fn lfs_file_sync(lfs: *LfsT, file: *LfsFileT) callconv(.c) i32;
|
||||||
|
extern fn lfs_file_seek(lfs: *LfsT, file: *LfsFileT, off: i32, whence: i32) callconv(.c) i32;
|
||||||
|
extern fn lfs_file_size(lfs: *LfsT, file: *LfsFileT) callconv(.c) i32;
|
||||||
|
extern fn lfs_remove(lfs: *LfsT, path: [*:0]const u8) callconv(.c) i32;
|
||||||
|
extern fn lfs_mkdir(lfs: *LfsT, path: [*:0]const u8) callconv(.c) i32;
|
||||||
|
extern fn lfs_stat(lfs: *LfsT, path: [*:0]const u8, info: *LfsInfo) callconv(.c) i32;
|
||||||
|
|
||||||
|
// --- Static state ---
|
||||||
|
// LittleFS requires ~800 bytes for lfs_t. We over-allocate to be safe.
|
||||||
|
var lfs_state: [2048]u8 align(8) = [_]u8{0} ** 2048;
|
||||||
|
var lfs_mounted: bool = false;
|
||||||
|
|
||||||
|
// Static buffers to avoid malloc for cache/lookahead
|
||||||
|
var read_cache: [CACHE_SIZE]u8 = [_]u8{0} ** CACHE_SIZE;
|
||||||
|
var prog_cache: [CACHE_SIZE]u8 = [_]u8{0} ** CACHE_SIZE;
|
||||||
|
var lookahead_buf: [LOOKAHEAD_SIZE]u8 = [_]u8{0} ** LOOKAHEAD_SIZE;
|
||||||
|
|
||||||
|
// File handles: pre-allocated pool (LittleFS lfs_file_t is ~100 bytes, over-allocate)
|
||||||
|
const MAX_LFS_FILES = 8;
|
||||||
|
var file_slots: [MAX_LFS_FILES][512]u8 align(8) = [_][512]u8{[_]u8{0} ** 512} ** MAX_LFS_FILES;
|
||||||
|
var file_active: [MAX_LFS_FILES]bool = [_]bool{false} ** MAX_LFS_FILES;
|
||||||
|
|
||||||
|
var cfg: LfsConfig = .{
|
||||||
|
.context = null,
|
||||||
|
.read = &lfsRead,
|
||||||
|
.prog = &lfsProg,
|
||||||
|
.erase = &lfsErase,
|
||||||
|
.sync = &lfsSync,
|
||||||
|
.read_size = SECTOR_SIZE,
|
||||||
|
.prog_size = SECTOR_SIZE,
|
||||||
|
.block_size = BLOCK_SIZE,
|
||||||
|
.block_count = BLOCK_COUNT,
|
||||||
|
.block_cycles = 500,
|
||||||
|
.cache_size = CACHE_SIZE,
|
||||||
|
.lookahead_size = LOOKAHEAD_SIZE,
|
||||||
|
.compact_thresh = 0,
|
||||||
|
.read_buffer = &read_cache,
|
||||||
|
.prog_buffer = &prog_cache,
|
||||||
|
.lookahead_buffer = &lookahead_buf,
|
||||||
|
.name_max = 0,
|
||||||
|
.file_max = 0,
|
||||||
|
.attr_max = 0,
|
||||||
|
.metadata_max = 0,
|
||||||
|
.inline_max = 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
// =========================================================
|
||||||
|
// LittleFS Config Callbacks
|
||||||
|
// =========================================================
|
||||||
|
|
||||||
|
/// Read a region from a block via VirtIO-Block.
|
||||||
|
fn lfsRead(_: *LfsConfig, block: u32, off: u32, buffer: ?*anyopaque, size: u32) callconv(.c) i32 {
|
||||||
|
const buf: [*]u8 = @ptrCast(@alignCast(buffer orelse return -5));
|
||||||
|
const base_sector: u64 = @as(u64, block) * SECTORS_PER_BLOCK + @as(u64, off) / SECTOR_SIZE;
|
||||||
|
const sector_offset = off % SECTOR_SIZE;
|
||||||
|
|
||||||
|
if (sector_offset == 0 and size % SECTOR_SIZE == 0) {
|
||||||
|
// Aligned: direct sector reads
|
||||||
|
var i: u32 = 0;
|
||||||
|
while (i < size / SECTOR_SIZE) : (i += 1) {
|
||||||
|
virtio_blk_read(base_sector + i, buf + i * SECTOR_SIZE);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Unaligned: bounce buffer
|
||||||
|
var tmp: [SECTOR_SIZE]u8 = undefined;
|
||||||
|
var remaining: u32 = size;
|
||||||
|
var buf_off: u32 = 0;
|
||||||
|
var cur_off: u32 = off;
|
||||||
|
|
||||||
|
while (remaining > 0) {
|
||||||
|
const sec: u64 = @as(u64, block) * SECTORS_PER_BLOCK + @as(u64, cur_off) / SECTOR_SIZE;
|
||||||
|
const sec_off = cur_off % SECTOR_SIZE;
|
||||||
|
virtio_blk_read(sec, &tmp);
|
||||||
|
|
||||||
|
const avail = SECTOR_SIZE - sec_off;
|
||||||
|
const chunk = if (remaining < avail) remaining else avail;
|
||||||
|
for (0..chunk) |j| {
|
||||||
|
buf[buf_off + @as(u32, @intCast(j))] = tmp[sec_off + @as(u32, @intCast(j))];
|
||||||
|
}
|
||||||
|
buf_off += chunk;
|
||||||
|
cur_off += chunk;
|
||||||
|
remaining -= chunk;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Program (write) a region in a block via VirtIO-Block.
|
||||||
|
fn lfsProg(_: *LfsConfig, block: u32, off: u32, buffer: ?*anyopaque, size: u32) callconv(.c) i32 {
|
||||||
|
const buf: [*]const u8 = @ptrCast(@alignCast(buffer orelse return -5));
|
||||||
|
const base_sector: u64 = @as(u64, block) * SECTORS_PER_BLOCK + @as(u64, off) / SECTOR_SIZE;
|
||||||
|
const sector_offset = off % SECTOR_SIZE;
|
||||||
|
|
||||||
|
if (sector_offset == 0 and size % SECTOR_SIZE == 0) {
|
||||||
|
// Aligned: direct sector writes
|
||||||
|
var i: u32 = 0;
|
||||||
|
while (i < size / SECTOR_SIZE) : (i += 1) {
|
||||||
|
virtio_blk_write(base_sector + i, buf + i * SECTOR_SIZE);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Unaligned: read-modify-write via bounce buffer
|
||||||
|
var tmp: [SECTOR_SIZE]u8 = undefined;
|
||||||
|
var remaining: u32 = size;
|
||||||
|
var buf_off: u32 = 0;
|
||||||
|
var cur_off: u32 = off;
|
||||||
|
|
||||||
|
while (remaining > 0) {
|
||||||
|
const sec: u64 = @as(u64, block) * SECTORS_PER_BLOCK + @as(u64, cur_off) / SECTOR_SIZE;
|
||||||
|
const sec_off = cur_off % SECTOR_SIZE;
|
||||||
|
|
||||||
|
// Read existing sector if partial write
|
||||||
|
if (sec_off != 0 or remaining < SECTOR_SIZE) {
|
||||||
|
virtio_blk_read(sec, &tmp);
|
||||||
|
}
|
||||||
|
|
||||||
|
const avail = SECTOR_SIZE - sec_off;
|
||||||
|
const chunk = if (remaining < avail) remaining else avail;
|
||||||
|
for (0..chunk) |j| {
|
||||||
|
tmp[sec_off + @as(u32, @intCast(j))] = buf[buf_off + @as(u32, @intCast(j))];
|
||||||
|
}
|
||||||
|
virtio_blk_write(sec, &tmp);
|
||||||
|
buf_off += chunk;
|
||||||
|
cur_off += chunk;
|
||||||
|
remaining -= chunk;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Erase a block. VirtIO-Block has no erase concept, so we zero-fill.
|
||||||
|
fn lfsErase(_: *LfsConfig, block: u32) callconv(.c) i32 {
|
||||||
|
const zeros = [_]u8{0xFF} ** SECTOR_SIZE; // LFS expects 0xFF after erase
|
||||||
|
var i: u32 = 0;
|
||||||
|
while (i < SECTORS_PER_BLOCK) : (i += 1) {
|
||||||
|
const sec: u64 = @as(u64, block) * SECTORS_PER_BLOCK + i;
|
||||||
|
virtio_blk_write(sec, &zeros);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sync — VirtIO-Block is synchronous, nothing to flush.
|
||||||
|
fn lfsSync(_: *LfsConfig) callconv(.c) i32 {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================
|
||||||
|
// Exported C-ABI for Nim L1
|
||||||
|
// =========================================================
|
||||||
|
|
||||||
|
/// Format the block device with LittleFS.
|
||||||
|
export fn nexus_lfs_format() i32 {
|
||||||
|
kprint("[LFS] Formatting sovereign filesystem...\n");
|
||||||
|
const lfs_ptr: *LfsT = @ptrCast(@alignCast(&lfs_state));
|
||||||
|
const rc = lfs_format(lfs_ptr, &cfg);
|
||||||
|
if (rc == 0) {
|
||||||
|
kprint("[LFS] Format OK\n");
|
||||||
|
} else {
|
||||||
|
kprint("[LFS] Format FAILED\n");
|
||||||
|
}
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mount the LittleFS filesystem. Auto-formats if mount fails (first boot).
|
||||||
|
export fn nexus_lfs_mount() i32 {
|
||||||
|
const lfs_ptr: *LfsT = @ptrCast(@alignCast(&lfs_state));
|
||||||
|
var rc = lfs_mount(lfs_ptr, &cfg);
|
||||||
|
if (rc != 0) {
|
||||||
|
// First boot or corrupt — format and retry
|
||||||
|
kprint("[LFS] Mount failed, formatting (first boot)...\n");
|
||||||
|
rc = lfs_format(lfs_ptr, &cfg);
|
||||||
|
if (rc != 0) {
|
||||||
|
kprint("[LFS] Format FAILED\n");
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
rc = lfs_mount(lfs_ptr, &cfg);
|
||||||
|
}
|
||||||
|
if (rc == 0) {
|
||||||
|
lfs_mounted = true;
|
||||||
|
kprint("[LFS] Sovereign filesystem mounted on /nexus\n");
|
||||||
|
} else {
|
||||||
|
kprint("[LFS] Mount FAILED after format\n");
|
||||||
|
}
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Unmount the filesystem.
|
||||||
|
export fn nexus_lfs_unmount() i32 {
|
||||||
|
if (!lfs_mounted) return -1;
|
||||||
|
const lfs_ptr: *LfsT = @ptrCast(@alignCast(&lfs_state));
|
||||||
|
const rc = lfs_unmount(lfs_ptr);
|
||||||
|
lfs_mounted = false;
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Open a file. Returns a file handle index (0..MAX_LFS_FILES-1) or -1 on error.
|
||||||
|
/// flags: 1=RDONLY, 2=WRONLY, 3=RDWR, 0x0100=CREAT, 0x0400=TRUNC, 0x0800=APPEND
|
||||||
|
export fn nexus_lfs_open(path: [*:0]const u8, flags: i32) i32 {
|
||||||
|
if (!lfs_mounted) return -1;
|
||||||
|
// Find free slot
|
||||||
|
var slot: usize = 0;
|
||||||
|
while (slot < MAX_LFS_FILES) : (slot += 1) {
|
||||||
|
if (!file_active[slot]) break;
|
||||||
|
}
|
||||||
|
if (slot >= MAX_LFS_FILES) return -1; // No free handles
|
||||||
|
|
||||||
|
const lfs_ptr: *LfsT = @ptrCast(@alignCast(&lfs_state));
|
||||||
|
const file_ptr: *LfsFileT = @ptrCast(@alignCast(&file_slots[slot]));
|
||||||
|
const rc = lfs_file_open(lfs_ptr, file_ptr, path, flags);
|
||||||
|
if (rc == 0) {
|
||||||
|
file_active[slot] = true;
|
||||||
|
return @intCast(slot);
|
||||||
|
}
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read from a file. Returns bytes read or negative error.
|
||||||
|
export fn nexus_lfs_read(handle: i32, buf: [*]u8, size: u32) i32 {
|
||||||
|
if (!lfs_mounted) return -1;
|
||||||
|
const idx: usize = @intCast(handle);
|
||||||
|
if (idx >= MAX_LFS_FILES or !file_active[idx]) return -1;
|
||||||
|
|
||||||
|
const lfs_ptr: *LfsT = @ptrCast(@alignCast(&lfs_state));
|
||||||
|
const file_ptr: *LfsFileT = @ptrCast(@alignCast(&file_slots[idx]));
|
||||||
|
return lfs_file_read(lfs_ptr, file_ptr, buf, size);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write to a file. Returns bytes written or negative error.
|
||||||
|
export fn nexus_lfs_write(handle: i32, buf: [*]const u8, size: u32) i32 {
|
||||||
|
if (!lfs_mounted) return -1;
|
||||||
|
const idx: usize = @intCast(handle);
|
||||||
|
if (idx >= MAX_LFS_FILES or !file_active[idx]) return -1;
|
||||||
|
|
||||||
|
const lfs_ptr: *LfsT = @ptrCast(@alignCast(&lfs_state));
|
||||||
|
const file_ptr: *LfsFileT = @ptrCast(@alignCast(&file_slots[idx]));
|
||||||
|
return lfs_file_write(lfs_ptr, file_ptr, buf, size);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Close a file handle.
|
||||||
|
export fn nexus_lfs_close(handle: i32) i32 {
|
||||||
|
if (!lfs_mounted) return -1;
|
||||||
|
const idx: usize = @intCast(handle);
|
||||||
|
if (idx >= MAX_LFS_FILES or !file_active[idx]) return -1;
|
||||||
|
|
||||||
|
const lfs_ptr: *LfsT = @ptrCast(@alignCast(&lfs_state));
|
||||||
|
const file_ptr: *LfsFileT = @ptrCast(@alignCast(&file_slots[idx]));
|
||||||
|
const rc = lfs_file_close(lfs_ptr, file_ptr);
|
||||||
|
file_active[idx] = false;
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Seek within a file.
|
||||||
|
export fn nexus_lfs_seek(handle: i32, off: i32, whence: i32) i32 {
|
||||||
|
if (!lfs_mounted) return -1;
|
||||||
|
const idx: usize = @intCast(handle);
|
||||||
|
if (idx >= MAX_LFS_FILES or !file_active[idx]) return -1;
|
||||||
|
|
||||||
|
const lfs_ptr: *LfsT = @ptrCast(@alignCast(&lfs_state));
|
||||||
|
const file_ptr: *LfsFileT = @ptrCast(@alignCast(&file_slots[idx]));
|
||||||
|
return lfs_file_seek(lfs_ptr, file_ptr, off, whence);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get file size.
|
||||||
|
export fn nexus_lfs_size(handle: i32) i32 {
|
||||||
|
if (!lfs_mounted) return -1;
|
||||||
|
const idx: usize = @intCast(handle);
|
||||||
|
if (idx >= MAX_LFS_FILES or !file_active[idx]) return -1;
|
||||||
|
|
||||||
|
const lfs_ptr: *LfsT = @ptrCast(@alignCast(&lfs_state));
|
||||||
|
const file_ptr: *LfsFileT = @ptrCast(@alignCast(&file_slots[idx]));
|
||||||
|
return lfs_file_size(lfs_ptr, file_ptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Remove a file or empty directory.
|
||||||
|
export fn nexus_lfs_remove(path: [*:0]const u8) i32 {
|
||||||
|
if (!lfs_mounted) return -1;
|
||||||
|
const lfs_ptr: *LfsT = @ptrCast(@alignCast(&lfs_state));
|
||||||
|
return lfs_remove(lfs_ptr, path);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a directory.
|
||||||
|
export fn nexus_lfs_mkdir(path: [*:0]const u8) i32 {
|
||||||
|
if (!lfs_mounted) return -1;
|
||||||
|
const lfs_ptr: *LfsT = @ptrCast(@alignCast(&lfs_state));
|
||||||
|
return lfs_mkdir(lfs_ptr, path);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if mounted.
|
||||||
|
export fn nexus_lfs_is_mounted() i32 {
|
||||||
|
return if (lfs_mounted) @as(i32, 1) else @as(i32, 0);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,268 @@
|
||||||
|
// SPDX-License-Identifier: LCL-1.0
|
||||||
|
// Copyright (c) 2026 Markus Maiwald
|
||||||
|
// Stewardship: Self Sovereign Society Foundation
|
||||||
|
//
|
||||||
|
// This file is part of the Nexus Commonwealth.
|
||||||
|
// See legal/LICENSE_COMMONWEALTH.md for license terms.
|
||||||
|
|
||||||
|
//! Rumpk HAL: VirtIO MMIO Transport Layer (ARM64)
|
||||||
|
//!
|
||||||
|
//! Provides the same VirtioTransport API as virtio_pci.zig but for MMIO-based
|
||||||
|
//! VirtIO devices as found on QEMU aarch64 virt machine.
|
||||||
|
//!
|
||||||
|
//! QEMU virt MMIO layout: 32 slots starting at 0x0a000000, stride 0x200.
|
||||||
|
//! Each slot triggers GIC SPI (IRQ 48 + slot_index).
|
||||||
|
//!
|
||||||
|
//! Supports both legacy (v1) and modern (v2) MMIO transport.
|
||||||
|
//!
|
||||||
|
//! SAFETY: All hardware registers accessed via volatile pointers.
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const builtin = @import("builtin");
|
||||||
|
const uart = @import("uart.zig");
|
||||||
|
|
||||||
|
// =========================================================
|
||||||
|
// VirtIO MMIO Register Offsets (spec §4.2.2)
|
||||||
|
// =========================================================
|
||||||
|
|
||||||
|
const VIRTIO_MMIO_MAGIC_VALUE = 0x000;
|
||||||
|
const VIRTIO_MMIO_VERSION = 0x004;
|
||||||
|
const VIRTIO_MMIO_DEVICE_ID = 0x008;
|
||||||
|
const VIRTIO_MMIO_VENDOR_ID = 0x00C;
|
||||||
|
const VIRTIO_MMIO_DEVICE_FEATURES = 0x010;
|
||||||
|
const VIRTIO_MMIO_DEVICE_FEATURES_SEL = 0x014;
|
||||||
|
const VIRTIO_MMIO_DRIVER_FEATURES = 0x020;
|
||||||
|
const VIRTIO_MMIO_DRIVER_FEATURES_SEL = 0x024;
|
||||||
|
const VIRTIO_MMIO_QUEUE_SEL = 0x030;
|
||||||
|
const VIRTIO_MMIO_QUEUE_NUM_MAX = 0x034;
|
||||||
|
const VIRTIO_MMIO_QUEUE_NUM = 0x038;
|
||||||
|
const VIRTIO_MMIO_QUEUE_ALIGN = 0x03C;
|
||||||
|
const VIRTIO_MMIO_QUEUE_PFN = 0x040;
|
||||||
|
const VIRTIO_MMIO_QUEUE_READY = 0x044;
|
||||||
|
const VIRTIO_MMIO_QUEUE_NOTIFY = 0x050;
|
||||||
|
const VIRTIO_MMIO_INTERRUPT_STATUS = 0x060;
|
||||||
|
const VIRTIO_MMIO_INTERRUPT_ACK = 0x064;
|
||||||
|
const VIRTIO_MMIO_STATUS = 0x070;
|
||||||
|
const VIRTIO_MMIO_QUEUE_DESC_LOW = 0x080;
|
||||||
|
const VIRTIO_MMIO_QUEUE_DESC_HIGH = 0x084;
|
||||||
|
const VIRTIO_MMIO_QUEUE_AVAIL_LOW = 0x090;
|
||||||
|
const VIRTIO_MMIO_QUEUE_AVAIL_HIGH = 0x094;
|
||||||
|
const VIRTIO_MMIO_QUEUE_USED_LOW = 0x0A0;
|
||||||
|
const VIRTIO_MMIO_QUEUE_USED_HIGH = 0x0A4;
|
||||||
|
const VIRTIO_MMIO_CONFIG = 0x100; // Device-specific config starts here
|
||||||
|
|
||||||
|
// VirtIO magic value: "virt" in little-endian
|
||||||
|
const VIRTIO_MAGIC: u32 = 0x74726976;
|
||||||
|
|
||||||
|
// =========================================================
|
||||||
|
// QEMU virt MMIO Topology
|
||||||
|
// =========================================================
|
||||||
|
|
||||||
|
const MMIO_BASE: usize = 0x0a000000;
|
||||||
|
const MMIO_STRIDE: usize = 0x200;
|
||||||
|
const MMIO_SLOT_COUNT: usize = 32;
|
||||||
|
const MMIO_IRQ_BASE: u32 = 48; // GIC SPI base for VirtIO MMIO
|
||||||
|
|
||||||
|
// =========================================================
|
||||||
|
// MMIO Read/Write Helpers
|
||||||
|
// =========================================================
|
||||||
|
|
||||||
|
fn mmio_read(base: usize, offset: usize) u32 {
|
||||||
|
const ptr: *volatile u32 = @ptrFromInt(base + offset);
|
||||||
|
return ptr.*;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mmio_write(base: usize, offset: usize, val: u32) void {
|
||||||
|
const ptr: *volatile u32 = @ptrFromInt(base + offset);
|
||||||
|
ptr.* = val;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mmio_read_u8(base: usize, offset: usize) u8 {
|
||||||
|
const ptr: *volatile u8 = @ptrFromInt(base + offset);
|
||||||
|
return ptr.*;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================
|
||||||
|
// Arch-safe memory barrier
|
||||||
|
// =========================================================
|
||||||
|
|
||||||
|
pub inline fn io_barrier() void {
|
||||||
|
switch (builtin.cpu.arch) {
|
||||||
|
.aarch64 => asm volatile ("dmb sy" ::: .{ .memory = true }),
|
||||||
|
.riscv64 => asm volatile ("fence" ::: .{ .memory = true }),
|
||||||
|
else => @compileError("unsupported arch"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================
|
||||||
|
// VirtIO MMIO Transport (same API surface as PCI transport)
|
||||||
|
// =========================================================
|
||||||
|
|
||||||
|
pub const VirtioTransport = struct {
|
||||||
|
base_addr: usize,
|
||||||
|
is_modern: bool,
|
||||||
|
version: u32,
|
||||||
|
|
||||||
|
// Legacy compatibility fields (match PCI transport shape)
|
||||||
|
legacy_bar: usize,
|
||||||
|
|
||||||
|
// Modern interface placeholders (unused for MMIO but present for API compat)
|
||||||
|
common_cfg: ?*volatile anyopaque,
|
||||||
|
notify_cfg: ?usize,
|
||||||
|
notify_off_multiplier: u32,
|
||||||
|
isr_cfg: ?*volatile u8,
|
||||||
|
device_cfg: ?*volatile u8,
|
||||||
|
|
||||||
|
pub fn init(mmio_base: usize) VirtioTransport {
|
||||||
|
return .{
|
||||||
|
.base_addr = mmio_base,
|
||||||
|
.is_modern = false,
|
||||||
|
.version = 0,
|
||||||
|
.legacy_bar = 0,
|
||||||
|
.common_cfg = null,
|
||||||
|
.notify_cfg = null,
|
||||||
|
.notify_off_multiplier = 0,
|
||||||
|
.isr_cfg = null,
|
||||||
|
.device_cfg = null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn probe(self: *VirtioTransport) bool {
|
||||||
|
const magic = mmio_read(self.base_addr, VIRTIO_MMIO_MAGIC_VALUE);
|
||||||
|
if (magic != VIRTIO_MAGIC) return false;
|
||||||
|
|
||||||
|
self.version = mmio_read(self.base_addr, VIRTIO_MMIO_VERSION);
|
||||||
|
if (self.version != 1 and self.version != 2) return false;
|
||||||
|
|
||||||
|
const device_id = mmio_read(self.base_addr, VIRTIO_MMIO_DEVICE_ID);
|
||||||
|
if (device_id == 0) return false; // No device at this slot
|
||||||
|
|
||||||
|
self.is_modern = (self.version == 2);
|
||||||
|
|
||||||
|
uart.print("[VirtIO-MMIO] Probed 0x");
|
||||||
|
uart.print_hex(self.base_addr);
|
||||||
|
uart.print(" Ver=");
|
||||||
|
uart.print_hex(self.version);
|
||||||
|
uart.print(" DevID=");
|
||||||
|
uart.print_hex(device_id);
|
||||||
|
uart.print("\n");
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn reset(self: *VirtioTransport) void {
|
||||||
|
self.set_status(0);
|
||||||
|
// After reset, wait for device to reinitialize (spec §2.1.1)
|
||||||
|
io_barrier();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_status(self: *VirtioTransport) u8 {
|
||||||
|
return @truncate(mmio_read(self.base_addr, VIRTIO_MMIO_STATUS));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_status(self: *VirtioTransport, status: u8) void {
|
||||||
|
mmio_write(self.base_addr, VIRTIO_MMIO_STATUS, @as(u32, status));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_status(self: *VirtioTransport, status: u8) void {
|
||||||
|
self.set_status(self.get_status() | status);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn select_queue(self: *VirtioTransport, idx: u16) void {
|
||||||
|
mmio_write(self.base_addr, VIRTIO_MMIO_QUEUE_SEL, @as(u32, idx));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_queue_size(self: *VirtioTransport) u16 {
|
||||||
|
return @truncate(mmio_read(self.base_addr, VIRTIO_MMIO_QUEUE_NUM_MAX));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_queue_size(self: *VirtioTransport, size: u16) void {
|
||||||
|
mmio_write(self.base_addr, VIRTIO_MMIO_QUEUE_NUM, @as(u32, size));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn setup_legacy_queue(self: *VirtioTransport, pfn: u32) void {
|
||||||
|
mmio_write(self.base_addr, VIRTIO_MMIO_QUEUE_ALIGN, 4096);
|
||||||
|
mmio_write(self.base_addr, VIRTIO_MMIO_QUEUE_PFN, pfn);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn setup_modern_queue(self: *VirtioTransport, desc: u64, avail: u64, used: u64) void {
|
||||||
|
// Set queue size first
|
||||||
|
const max_size = mmio_read(self.base_addr, VIRTIO_MMIO_QUEUE_NUM_MAX);
|
||||||
|
mmio_write(self.base_addr, VIRTIO_MMIO_QUEUE_NUM, max_size);
|
||||||
|
|
||||||
|
mmio_write(self.base_addr, VIRTIO_MMIO_QUEUE_DESC_LOW, @truncate(desc));
|
||||||
|
mmio_write(self.base_addr, VIRTIO_MMIO_QUEUE_DESC_HIGH, @truncate(desc >> 32));
|
||||||
|
mmio_write(self.base_addr, VIRTIO_MMIO_QUEUE_AVAIL_LOW, @truncate(avail));
|
||||||
|
mmio_write(self.base_addr, VIRTIO_MMIO_QUEUE_AVAIL_HIGH, @truncate(avail >> 32));
|
||||||
|
mmio_write(self.base_addr, VIRTIO_MMIO_QUEUE_USED_LOW, @truncate(used));
|
||||||
|
mmio_write(self.base_addr, VIRTIO_MMIO_QUEUE_USED_HIGH, @truncate(used >> 32));
|
||||||
|
|
||||||
|
mmio_write(self.base_addr, VIRTIO_MMIO_QUEUE_READY, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn notify(self: *VirtioTransport, queue_idx: u16) void {
|
||||||
|
mmio_write(self.base_addr, VIRTIO_MMIO_QUEUE_NOTIFY, @as(u32, queue_idx));
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================
|
||||||
|
// Unified Accessor API (matches PCI transport extensions)
|
||||||
|
// =========================================================
|
||||||
|
|
||||||
|
pub fn get_device_features(self: *VirtioTransport) u64 {
|
||||||
|
mmio_write(self.base_addr, VIRTIO_MMIO_DEVICE_FEATURES_SEL, 0);
|
||||||
|
io_barrier();
|
||||||
|
const low: u64 = mmio_read(self.base_addr, VIRTIO_MMIO_DEVICE_FEATURES);
|
||||||
|
|
||||||
|
mmio_write(self.base_addr, VIRTIO_MMIO_DEVICE_FEATURES_SEL, 1);
|
||||||
|
io_barrier();
|
||||||
|
const high: u64 = mmio_read(self.base_addr, VIRTIO_MMIO_DEVICE_FEATURES);
|
||||||
|
|
||||||
|
return (high << 32) | low;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_driver_features(self: *VirtioTransport, features: u64) void {
|
||||||
|
mmio_write(self.base_addr, VIRTIO_MMIO_DRIVER_FEATURES_SEL, 0);
|
||||||
|
mmio_write(self.base_addr, VIRTIO_MMIO_DRIVER_FEATURES, @truncate(features));
|
||||||
|
io_barrier();
|
||||||
|
mmio_write(self.base_addr, VIRTIO_MMIO_DRIVER_FEATURES_SEL, 1);
|
||||||
|
mmio_write(self.base_addr, VIRTIO_MMIO_DRIVER_FEATURES, @truncate(features >> 32));
|
||||||
|
io_barrier();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_device_config_byte(self: *VirtioTransport, offset: usize) u8 {
|
||||||
|
return mmio_read_u8(self.base_addr, VIRTIO_MMIO_CONFIG + offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ack_interrupt(self: *VirtioTransport) u32 {
|
||||||
|
const status = mmio_read(self.base_addr, VIRTIO_MMIO_INTERRUPT_STATUS);
|
||||||
|
mmio_write(self.base_addr, VIRTIO_MMIO_INTERRUPT_ACK, status);
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// =========================================================
|
||||||
|
// Device Discovery
|
||||||
|
// =========================================================
|
||||||
|
|
||||||
|
/// Scan MMIO slots for a VirtIO device with the given device ID.
|
||||||
|
/// Returns MMIO base address or null if not found.
|
||||||
|
pub fn find_device(device_id: u32) ?usize {
|
||||||
|
var slot: usize = 0;
|
||||||
|
while (slot < MMIO_SLOT_COUNT) : (slot += 1) {
|
||||||
|
const base = MMIO_BASE + (slot * MMIO_STRIDE);
|
||||||
|
const magic = mmio_read(base, VIRTIO_MMIO_MAGIC_VALUE);
|
||||||
|
if (magic != VIRTIO_MAGIC) continue;
|
||||||
|
|
||||||
|
const dev_id = mmio_read(base, VIRTIO_MMIO_DEVICE_ID);
|
||||||
|
if (dev_id == device_id) {
|
||||||
|
return base;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the GIC SPI number for a given MMIO slot base address.
|
||||||
|
pub fn slot_irq(base: usize) u32 {
|
||||||
|
const slot = (base - MMIO_BASE) / MMIO_STRIDE;
|
||||||
|
return MMIO_IRQ_BASE + @as(u32, @intCast(slot));
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,512 @@
|
||||||
|
// SPDX-License-Identifier: LCL-1.0
|
||||||
|
// Copyright (c) 2026 Markus Maiwald
|
||||||
|
// Stewardship: Self Sovereign Society Foundation
|
||||||
|
//
|
||||||
|
// This file is part of the Nexus Commonwealth.
|
||||||
|
// See legal/LICENSE_COMMONWEALTH.md for license terms.
|
||||||
|
|
||||||
|
//! Project LibWeb: LWF Adapter for Rumpk Kernel
|
||||||
|
//!
|
||||||
|
//! Freestanding LWF header parser for kernel-side routing decisions.
|
||||||
|
//! Zero-copy — operates directly on ION slab buffers.
|
||||||
|
//! Does NOT use std.mem.Allocator — all parsing is in-place.
|
||||||
|
//!
|
||||||
|
//! The full LWF codec (with allocation, encode, checksum) runs in
|
||||||
|
//! the Membrane (userland) where std is available. The kernel only
|
||||||
|
//! needs to parse the header to decide routing.
|
||||||
|
//!
|
||||||
|
//! Wire Format (after Ethernet header):
|
||||||
|
//! [Eth 14B][LWF Header 88B][Payload ...][LWF Trailer 36B]
|
||||||
|
//!
|
||||||
|
//! Integration Points:
|
||||||
|
//! - NetSwitch: EtherType 0x4C57 ("LW") → route to chan_lwf_rx
|
||||||
|
//! - Membrane: Full LWF codec via upstream lwf.zig (uses std)
|
||||||
|
|
||||||
|
// =========================================================
|
||||||
|
// LWF Constants (RFC-0000 v0.3.1)
|
||||||
|
// =========================================================
|
||||||
|
|
||||||
|
pub const ETHERTYPE_LWF: u16 = 0x4C57; // "LW" in ASCII — Sovereign EtherType
|
||||||
|
pub const LWF_MAGIC = [4]u8{ 'L', 'W', 'F', 0 };
|
||||||
|
pub const LWF_VERSION: u8 = 0x02;
|
||||||
|
pub const HEADER_SIZE: usize = 88;
|
||||||
|
pub const TRAILER_SIZE: usize = 36;
|
||||||
|
pub const MIN_FRAME_SIZE: usize = HEADER_SIZE + TRAILER_SIZE; // 124 bytes
|
||||||
|
|
||||||
|
// =========================================================
|
||||||
|
// Frame Classes (RFC-0000 Section 4.2)
|
||||||
|
// =========================================================
|
||||||
|
|
||||||
|
pub const FrameClass = enum(u8) {
|
||||||
|
micro = 0x00, // 128 bytes total
|
||||||
|
mini = 0x01, // 512 bytes total
|
||||||
|
standard = 0x02, // 1350 bytes total
|
||||||
|
big = 0x03, // 4096 bytes total (exceeds ION slab!)
|
||||||
|
jumbo = 0x04, // 9000 bytes total (exceeds ION slab!)
|
||||||
|
_,
|
||||||
|
|
||||||
|
pub fn maxTotal(self: FrameClass) u16 {
|
||||||
|
return switch (self) {
|
||||||
|
.micro => 128,
|
||||||
|
.mini => 512,
|
||||||
|
.standard => 1350,
|
||||||
|
.big => 4096,
|
||||||
|
.jumbo => 9000,
|
||||||
|
_ => 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if frame class fits in an ION slab (2048 bytes)
|
||||||
|
pub fn fitsInSlab(self: FrameClass) bool {
|
||||||
|
return switch (self) {
|
||||||
|
.micro, .mini, .standard => true,
|
||||||
|
.big, .jumbo => false,
|
||||||
|
_ => false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// =========================================================
|
||||||
|
// Service Types (RFC-0121)
|
||||||
|
// =========================================================
|
||||||
|
|
||||||
|
pub const ServiceType = struct {
|
||||||
|
pub const DATA_TRANSPORT: u16 = 0x0001;
|
||||||
|
pub const SLASH_PROTOCOL: u16 = 0x0002;
|
||||||
|
pub const IDENTITY_SIGNAL: u16 = 0x0003;
|
||||||
|
pub const ECONOMIC_SETTLEMENT: u16 = 0x0004;
|
||||||
|
pub const RELAY_FORWARD: u16 = 0x0005;
|
||||||
|
pub const STREAM_AUDIO: u16 = 0x0800;
|
||||||
|
pub const STREAM_VIDEO: u16 = 0x0801;
|
||||||
|
pub const STREAM_DATA: u16 = 0x0802;
|
||||||
|
pub const SWARM_MANIFEST: u16 = 0x0B00;
|
||||||
|
pub const SWARM_HAVE: u16 = 0x0B01;
|
||||||
|
pub const SWARM_REQUEST: u16 = 0x0B02;
|
||||||
|
pub const SWARM_BLOCK: u16 = 0x0B03;
|
||||||
|
};
|
||||||
|
|
||||||
|
// =========================================================
|
||||||
|
// LWF Flags (RFC-0000 Section 4.3)
|
||||||
|
// =========================================================
|
||||||
|
|
||||||
|
pub const Flags = struct {
|
||||||
|
pub const ENCRYPTED: u8 = 0x01;
|
||||||
|
pub const SIGNED: u8 = 0x02;
|
||||||
|
pub const RELAYABLE: u8 = 0x04;
|
||||||
|
pub const HAS_ENTROPY: u8 = 0x08;
|
||||||
|
pub const FRAGMENTED: u8 = 0x10;
|
||||||
|
pub const PRIORITY: u8 = 0x20;
|
||||||
|
};
|
||||||
|
|
||||||
|
// =========================================================
|
||||||
|
// LWF Header View (Zero-Copy over ION slab)
|
||||||
|
// =========================================================
|
||||||
|
|
||||||
|
/// Parsed header fields from a raw buffer. No allocation.
|
||||||
|
/// All multi-byte integers are stored big-endian on the wire.
|
||||||
|
pub const HeaderView = struct {
|
||||||
|
/// Pointer to the start of the LWF header in the ION slab
|
||||||
|
raw: [*]const u8,
|
||||||
|
|
||||||
|
// Pre-parsed routing fields (hot path)
|
||||||
|
service_type: u16,
|
||||||
|
payload_len: u16,
|
||||||
|
frame_class: FrameClass,
|
||||||
|
version: u8,
|
||||||
|
flags: u8,
|
||||||
|
sequence: u32,
|
||||||
|
timestamp: u64,
|
||||||
|
|
||||||
|
/// Parse header from raw bytes. Returns null if invalid.
|
||||||
|
/// Does NOT copy — references the original buffer.
|
||||||
|
pub fn parse(data: [*]const u8, data_len: u16) ?HeaderView {
|
||||||
|
if (data_len < HEADER_SIZE) return null;
|
||||||
|
|
||||||
|
// Fast reject: Magic check (4 bytes at offset 0)
|
||||||
|
if (data[0] != 'L' or data[1] != 'W' or data[2] != 'F' or data[3] != 0)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
// Version check (offset 77)
|
||||||
|
const ver = data[77];
|
||||||
|
if (ver != LWF_VERSION) return null;
|
||||||
|
|
||||||
|
// Parse routing-critical fields
|
||||||
|
const service = readU16Big(data[72..74]);
|
||||||
|
const plen = readU16Big(data[74..76]);
|
||||||
|
const fclass = data[76];
|
||||||
|
const flg = data[78];
|
||||||
|
const seq = readU32Big(data[68..72]);
|
||||||
|
const ts = readU64Big(data[80..88]);
|
||||||
|
|
||||||
|
// Sanity: payload_len must fit in remaining buffer
|
||||||
|
const total_needed = HEADER_SIZE + @as(usize, plen) + TRAILER_SIZE;
|
||||||
|
if (total_needed > data_len) return null;
|
||||||
|
|
||||||
|
return HeaderView{
|
||||||
|
.raw = data,
|
||||||
|
.service_type = service,
|
||||||
|
.payload_len = plen,
|
||||||
|
.frame_class = @enumFromInt(fclass),
|
||||||
|
.version = ver,
|
||||||
|
.flags = flg,
|
||||||
|
.sequence = seq,
|
||||||
|
.timestamp = ts,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get destination hint (24 bytes at offset 4)
|
||||||
|
pub fn destHint(self: *const HeaderView) *const [24]u8 {
|
||||||
|
return @ptrCast(self.raw[4..28]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get source hint (24 bytes at offset 28)
|
||||||
|
pub fn sourceHint(self: *const HeaderView) *const [24]u8 {
|
||||||
|
return @ptrCast(self.raw[28..52]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get session ID (16 bytes at offset 52)
|
||||||
|
pub fn sessionId(self: *const HeaderView) *const [16]u8 {
|
||||||
|
return @ptrCast(self.raw[52..68]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get pointer to payload data (starts at offset 88)
|
||||||
|
pub fn payloadPtr(self: *const HeaderView) [*]const u8 {
|
||||||
|
return self.raw + HEADER_SIZE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if frame has PRIORITY flag
|
||||||
|
pub fn isPriority(self: *const HeaderView) bool {
|
||||||
|
return (self.flags & Flags.PRIORITY) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if frame is encrypted
|
||||||
|
pub fn isEncrypted(self: *const HeaderView) bool {
|
||||||
|
return (self.flags & Flags.ENCRYPTED) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Total frame size (header + payload + trailer)
|
||||||
|
pub fn totalSize(self: *const HeaderView) usize {
|
||||||
|
return HEADER_SIZE + @as(usize, self.payload_len) + TRAILER_SIZE;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// =========================================================
|
||||||
|
// Fast Path: Validation for NetSwitch
|
||||||
|
// =========================================================
|
||||||
|
|
||||||
|
/// Quick magic-byte check. Use before full parse for early rejection.
|
||||||
|
/// Expects data to point past the Ethernet header (14 bytes).
|
||||||
|
pub fn isLwfMagic(data: [*]const u8, len: u16) bool {
|
||||||
|
if (len < 4) return false;
|
||||||
|
return data[0] == 'L' and data[1] == 'W' and data[2] == 'F' and data[3] == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validate and parse an LWF frame from an ION slab.
|
||||||
|
/// Returns the parsed header view, or null if invalid.
|
||||||
|
/// The ION slab data should start at the LWF header (after Ethernet strip).
|
||||||
|
pub fn validateFrame(data: [*]const u8, len: u16) ?HeaderView {
|
||||||
|
return HeaderView.parse(data, len);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================
|
||||||
|
// C ABI Exports (for Nim FFI)
|
||||||
|
// =========================================================
|
||||||
|
|
||||||
|
/// Check if a raw buffer contains a valid LWF frame.
|
||||||
|
/// Called from netswitch.nim to decide routing.
|
||||||
|
/// Returns 1 if valid LWF, 0 otherwise.
|
||||||
|
export fn lwf_validate(data: [*]const u8, len: u16) u8 {
|
||||||
|
if (HeaderView.parse(data, len)) |_| {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the service type from a validated LWF frame.
|
||||||
|
/// Returns 0 on invalid input.
|
||||||
|
export fn lwf_get_service_type(data: [*]const u8, len: u16) u16 {
|
||||||
|
if (HeaderView.parse(data, len)) |hdr| {
|
||||||
|
return hdr.service_type;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the payload length from a validated LWF frame.
|
||||||
|
export fn lwf_get_payload_len(data: [*]const u8, len: u16) u16 {
|
||||||
|
if (HeaderView.parse(data, len)) |hdr| {
|
||||||
|
return hdr.payload_len;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if frame has PRIORITY flag set.
|
||||||
|
export fn lwf_is_priority(data: [*]const u8, len: u16) u8 {
|
||||||
|
if (HeaderView.parse(data, len)) |hdr| {
|
||||||
|
return if (hdr.isPriority()) 1 else 0;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================
|
||||||
|
// Freestanding Integer Helpers (no std)
|
||||||
|
// =========================================================
|
||||||
|
|
||||||
|
inline fn readU16Big(bytes: *const [2]u8) u16 {
|
||||||
|
return (@as(u16, bytes[0]) << 8) | @as(u16, bytes[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fn readU32Big(bytes: *const [4]u8) u32 {
|
||||||
|
return (@as(u32, bytes[0]) << 24) |
|
||||||
|
(@as(u32, bytes[1]) << 16) |
|
||||||
|
(@as(u32, bytes[2]) << 8) |
|
||||||
|
@as(u32, bytes[3]);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fn readU64Big(bytes: *const [8]u8) u64 {
|
||||||
|
return (@as(u64, bytes[0]) << 56) |
|
||||||
|
(@as(u64, bytes[1]) << 48) |
|
||||||
|
(@as(u64, bytes[2]) << 40) |
|
||||||
|
(@as(u64, bytes[3]) << 32) |
|
||||||
|
(@as(u64, bytes[4]) << 24) |
|
||||||
|
(@as(u64, bytes[5]) << 16) |
|
||||||
|
(@as(u64, bytes[6]) << 8) |
|
||||||
|
@as(u64, bytes[7]);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fn writeU16Big(buf: *[2]u8, val: u16) void {
|
||||||
|
buf[0] = @truncate(val >> 8);
|
||||||
|
buf[1] = @truncate(val);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fn writeU32Big(buf: *[4]u8, val: u32) void {
|
||||||
|
buf[0] = @truncate(val >> 24);
|
||||||
|
buf[1] = @truncate(val >> 16);
|
||||||
|
buf[2] = @truncate(val >> 8);
|
||||||
|
buf[3] = @truncate(val);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fn writeU64Big(buf: *[8]u8, val: u64) void {
|
||||||
|
buf[0] = @truncate(val >> 56);
|
||||||
|
buf[1] = @truncate(val >> 48);
|
||||||
|
buf[2] = @truncate(val >> 40);
|
||||||
|
buf[3] = @truncate(val >> 32);
|
||||||
|
buf[4] = @truncate(val >> 24);
|
||||||
|
buf[5] = @truncate(val >> 16);
|
||||||
|
buf[6] = @truncate(val >> 8);
|
||||||
|
buf[7] = @truncate(val);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================
|
||||||
|
// Test Frame Builder (for unit tests only)
|
||||||
|
// =========================================================
|
||||||
|
|
||||||
|
/// Build a minimal valid LWF frame in a buffer for testing.
|
||||||
|
/// Returns the total frame size written.
|
||||||
|
fn buildTestFrame(buf: []u8, payload: []const u8, service: u16, flags: u8) usize {
|
||||||
|
const total = HEADER_SIZE + payload.len + TRAILER_SIZE;
|
||||||
|
if (buf.len < total) return 0;
|
||||||
|
|
||||||
|
// Zero the buffer
|
||||||
|
for (buf[0..total]) |*b| b.* = 0;
|
||||||
|
|
||||||
|
// Magic
|
||||||
|
buf[0] = 'L';
|
||||||
|
buf[1] = 'W';
|
||||||
|
buf[2] = 'F';
|
||||||
|
buf[3] = 0;
|
||||||
|
|
||||||
|
// dest_hint (4..28) — leave zeros
|
||||||
|
// source_hint (28..52) — leave zeros
|
||||||
|
// session_id (52..68) — leave zeros
|
||||||
|
|
||||||
|
// Sequence (68..72)
|
||||||
|
writeU32Big(buf[68..72], 1);
|
||||||
|
|
||||||
|
// Service type (72..74)
|
||||||
|
writeU16Big(buf[72..74], service);
|
||||||
|
|
||||||
|
// Payload len (74..76)
|
||||||
|
writeU16Big(buf[74..76], @truncate(payload.len));
|
||||||
|
|
||||||
|
// Frame class (76)
|
||||||
|
buf[76] = @intFromEnum(FrameClass.standard);
|
||||||
|
|
||||||
|
// Version (77)
|
||||||
|
buf[77] = LWF_VERSION;
|
||||||
|
|
||||||
|
// Flags (78)
|
||||||
|
buf[78] = flags;
|
||||||
|
|
||||||
|
// entropy_difficulty (79) — 0
|
||||||
|
|
||||||
|
// Timestamp (80..88)
|
||||||
|
writeU64Big(buf[80..88], 0xDEADBEEF);
|
||||||
|
|
||||||
|
// Payload
|
||||||
|
for (payload, 0..) |byte, i| {
|
||||||
|
buf[HEADER_SIZE + i] = byte;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trailer (zeros = no signature, no checksum)
|
||||||
|
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================
|
||||||
|
// Tests
|
||||||
|
// =========================================================
|
||||||
|
|
||||||
|
const testing = @import("std").testing;
|
||||||
|
|
||||||
|
test "valid LWF frame parses correctly" {
|
||||||
|
var buf: [512]u8 = undefined;
|
||||||
|
const payload = "Hello LWF";
|
||||||
|
const sz = buildTestFrame(&buf, payload, ServiceType.DATA_TRANSPORT, 0);
|
||||||
|
try testing.expect(sz > 0);
|
||||||
|
|
||||||
|
const hdr = HeaderView.parse(&buf, @truncate(sz));
|
||||||
|
try testing.expect(hdr != null);
|
||||||
|
|
||||||
|
const h = hdr.?;
|
||||||
|
try testing.expectEqual(ServiceType.DATA_TRANSPORT, h.service_type);
|
||||||
|
try testing.expectEqual(@as(u16, 9), h.payload_len);
|
||||||
|
try testing.expectEqual(LWF_VERSION, h.version);
|
||||||
|
try testing.expectEqual(@as(u8, 0), h.flags);
|
||||||
|
try testing.expectEqual(@as(u32, 1), h.sequence);
|
||||||
|
try testing.expectEqual(@as(u64, 0xDEADBEEF), h.timestamp);
|
||||||
|
try testing.expectEqual(FrameClass.standard, h.frame_class);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "invalid magic rejected" {
|
||||||
|
var buf: [512]u8 = undefined;
|
||||||
|
_ = buildTestFrame(&buf, "test", ServiceType.DATA_TRANSPORT, 0);
|
||||||
|
|
||||||
|
// Corrupt magic
|
||||||
|
buf[0] = 'X';
|
||||||
|
const hdr = HeaderView.parse(&buf, 160);
|
||||||
|
try testing.expect(hdr == null);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "wrong version rejected" {
|
||||||
|
var buf: [512]u8 = undefined;
|
||||||
|
_ = buildTestFrame(&buf, "test", ServiceType.DATA_TRANSPORT, 0);
|
||||||
|
|
||||||
|
// Set wrong version
|
||||||
|
buf[77] = 0x01;
|
||||||
|
const hdr = HeaderView.parse(&buf, 160);
|
||||||
|
try testing.expect(hdr == null);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "buffer too small rejected" {
|
||||||
|
var buf: [512]u8 = undefined;
|
||||||
|
_ = buildTestFrame(&buf, "test", ServiceType.DATA_TRANSPORT, 0);
|
||||||
|
|
||||||
|
// Pass length smaller than header
|
||||||
|
const hdr = HeaderView.parse(&buf, 80);
|
||||||
|
try testing.expect(hdr == null);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "payload overflow rejected" {
|
||||||
|
var buf: [512]u8 = undefined;
|
||||||
|
_ = buildTestFrame(&buf, "test", ServiceType.DATA_TRANSPORT, 0);
|
||||||
|
|
||||||
|
// Claim huge payload that doesn't fit
|
||||||
|
writeU16Big(buf[74..76], 5000);
|
||||||
|
const hdr = HeaderView.parse(&buf, 160);
|
||||||
|
try testing.expect(hdr == null);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "priority flag detection" {
|
||||||
|
var buf: [512]u8 = undefined;
|
||||||
|
const sz = buildTestFrame(&buf, "urgent", ServiceType.SLASH_PROTOCOL, Flags.PRIORITY);
|
||||||
|
|
||||||
|
const hdr = HeaderView.parse(&buf, @truncate(sz)).?;
|
||||||
|
try testing.expect(hdr.isPriority());
|
||||||
|
try testing.expect(!hdr.isEncrypted());
|
||||||
|
}
|
||||||
|
|
||||||
|
test "encrypted flag detection" {
|
||||||
|
var buf: [512]u8 = undefined;
|
||||||
|
const sz = buildTestFrame(&buf, "secret", ServiceType.IDENTITY_SIGNAL, Flags.ENCRYPTED | Flags.SIGNED);
|
||||||
|
|
||||||
|
const hdr = HeaderView.parse(&buf, @truncate(sz)).?;
|
||||||
|
try testing.expect(hdr.isEncrypted());
|
||||||
|
try testing.expect(!hdr.isPriority());
|
||||||
|
}
|
||||||
|
|
||||||
|
test "isLwfMagic fast path" {
|
||||||
|
var buf: [8]u8 = .{ 'L', 'W', 'F', 0, 0, 0, 0, 0 };
|
||||||
|
try testing.expect(isLwfMagic(&buf, 8));
|
||||||
|
|
||||||
|
buf[2] = 'X';
|
||||||
|
try testing.expect(!isLwfMagic(&buf, 8));
|
||||||
|
|
||||||
|
// Too short
|
||||||
|
try testing.expect(!isLwfMagic(&buf, 3));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "C ABI lwf_validate matches HeaderView.parse" {
|
||||||
|
var buf: [512]u8 = undefined;
|
||||||
|
const sz = buildTestFrame(&buf, "abi_test", ServiceType.DATA_TRANSPORT, 0);
|
||||||
|
|
||||||
|
try testing.expectEqual(@as(u8, 1), lwf_validate(&buf, @truncate(sz)));
|
||||||
|
|
||||||
|
// Corrupt magic
|
||||||
|
buf[0] = 0;
|
||||||
|
try testing.expectEqual(@as(u8, 0), lwf_validate(&buf, @truncate(sz)));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "C ABI lwf_get_service_type" {
|
||||||
|
var buf: [512]u8 = undefined;
|
||||||
|
const sz = buildTestFrame(&buf, "svc", ServiceType.ECONOMIC_SETTLEMENT, 0);
|
||||||
|
|
||||||
|
try testing.expectEqual(ServiceType.ECONOMIC_SETTLEMENT, lwf_get_service_type(&buf, @truncate(sz)));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "frame class slab fit check" {
|
||||||
|
try testing.expect(FrameClass.micro.fitsInSlab());
|
||||||
|
try testing.expect(FrameClass.mini.fitsInSlab());
|
||||||
|
try testing.expect(FrameClass.standard.fitsInSlab());
|
||||||
|
try testing.expect(!FrameClass.big.fitsInSlab());
|
||||||
|
try testing.expect(!FrameClass.jumbo.fitsInSlab());
|
||||||
|
}
|
||||||
|
|
||||||
|
test "totalSize calculation" {
|
||||||
|
var buf: [512]u8 = undefined;
|
||||||
|
const payload = "12345";
|
||||||
|
const sz = buildTestFrame(&buf, payload, ServiceType.DATA_TRANSPORT, 0);
|
||||||
|
|
||||||
|
const hdr = HeaderView.parse(&buf, @truncate(sz)).?;
|
||||||
|
try testing.expectEqual(@as(usize, 88 + 5 + 36), hdr.totalSize());
|
||||||
|
}
|
||||||
|
|
||||||
|
test "dest and source hint accessors" {
|
||||||
|
var buf: [512]u8 = undefined;
|
||||||
|
_ = buildTestFrame(&buf, "hint", ServiceType.DATA_TRANSPORT, 0);
|
||||||
|
|
||||||
|
// Write a known dest hint at offset 4
|
||||||
|
buf[4] = 0xAA;
|
||||||
|
buf[27] = 0xBB;
|
||||||
|
// Write a known source hint at offset 28
|
||||||
|
buf[28] = 0xCC;
|
||||||
|
buf[51] = 0xDD;
|
||||||
|
|
||||||
|
const hdr = HeaderView.parse(&buf, 160).?;
|
||||||
|
try testing.expectEqual(@as(u8, 0xAA), hdr.destHint()[0]);
|
||||||
|
try testing.expectEqual(@as(u8, 0xBB), hdr.destHint()[23]);
|
||||||
|
try testing.expectEqual(@as(u8, 0xCC), hdr.sourceHint()[0]);
|
||||||
|
try testing.expectEqual(@as(u8, 0xDD), hdr.sourceHint()[23]);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "session ID accessor" {
|
||||||
|
var buf: [512]u8 = undefined;
|
||||||
|
_ = buildTestFrame(&buf, "sess", ServiceType.DATA_TRANSPORT, 0);
|
||||||
|
|
||||||
|
// Write session ID at offset 52
|
||||||
|
buf[52] = 0x42;
|
||||||
|
buf[67] = 0x99;
|
||||||
|
|
||||||
|
const hdr = HeaderView.parse(&buf, 160).?;
|
||||||
|
try testing.expectEqual(@as(u8, 0x42), hdr.sessionId()[0]);
|
||||||
|
try testing.expectEqual(@as(u8, 0x99), hdr.sessionId()[15]);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,300 @@
|
||||||
|
// SPDX-License-Identifier: LCL-1.0
|
||||||
|
// Copyright (c) 2026 Markus Maiwald
|
||||||
|
// Stewardship: Self Sovereign Society Foundation
|
||||||
|
//
|
||||||
|
// This file is part of the Nexus Commonwealth.
|
||||||
|
// See legal/LICENSE_COMMONWEALTH.md for license terms.
|
||||||
|
|
||||||
|
//! Project LibWeb: LWF Membrane Client
|
||||||
|
//!
|
||||||
|
//! Userland-side LWF frame handler. Runs in the Membrane where std is
|
||||||
|
//! available. Consumes validated LWF frames from the dedicated ION
|
||||||
|
//! channel (s_lwf_rx) and produces encrypted outbound frames (s_lwf_tx).
|
||||||
|
//!
|
||||||
|
//! This module bridges:
|
||||||
|
//! - ION ring (SysTable s_lwf_rx/s_lwf_tx) for zero-copy kernel IPC
|
||||||
|
//! - Upstream LWF codec (libertaria-stack lwf.zig) for full encode/decode
|
||||||
|
//! - Noise Protocol for transport encryption/decryption
|
||||||
|
//!
|
||||||
|
//! Architecture:
|
||||||
|
//! VirtIO-net → NetSwitch (validates header) → chan_lwf_rx → [this module]
|
||||||
|
//! [this module] → chan_lwf_tx → NetSwitch → VirtIO-net
|
||||||
|
//!
|
||||||
|
//! NOTE: This file is NOT compiled freestanding. It targets the Membrane
|
||||||
|
//! (userland) and has access to std.mem.Allocator.
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
// =========================================================
|
||||||
|
// ION Slab Constants (must match ion/memory.nim)
|
||||||
|
// =========================================================
|
||||||
|
|
||||||
|
const SLAB_SIZE: usize = 2048;
|
||||||
|
const SYSTABLE_ADDR: usize = if (builtin.cpu.arch == .aarch64) 0x50000000 else 0x83000000;
|
||||||
|
const ETH_HEADER_SIZE: usize = 14;
|
||||||
|
|
||||||
|
// =========================================================
|
||||||
|
// LWF Header Constants (duplicated from lwf_adapter.zig
|
||||||
|
// for use with std — adapter is freestanding, this is not)
|
||||||
|
// =========================================================
|
||||||
|
|
||||||
|
pub const HEADER_SIZE: usize = 88;
|
||||||
|
pub const TRAILER_SIZE: usize = 36;
|
||||||
|
pub const MIN_FRAME_SIZE: usize = HEADER_SIZE + TRAILER_SIZE;
|
||||||
|
pub const LWF_MAGIC = [4]u8{ 'L', 'W', 'F', 0 };
|
||||||
|
pub const LWF_VERSION: u8 = 0x02;
|
||||||
|
|
||||||
|
pub const Flags = struct {
|
||||||
|
pub const ENCRYPTED: u8 = 0x01;
|
||||||
|
pub const SIGNED: u8 = 0x02;
|
||||||
|
pub const RELAYABLE: u8 = 0x04;
|
||||||
|
pub const HAS_ENTROPY: u8 = 0x08;
|
||||||
|
pub const FRAGMENTED: u8 = 0x10;
|
||||||
|
pub const PRIORITY: u8 = 0x20;
|
||||||
|
};
|
||||||
|
|
||||||
|
// =========================================================
|
||||||
|
// Frame Processing Result
|
||||||
|
// =========================================================
|
||||||
|
|
||||||
|
pub const FrameError = error{
|
||||||
|
TooSmall,
|
||||||
|
InvalidMagic,
|
||||||
|
InvalidVersion,
|
||||||
|
PayloadOverflow,
|
||||||
|
DecryptionFailed,
|
||||||
|
NoSession,
|
||||||
|
SlabTooSmall,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const ProcessedFrame = struct {
|
||||||
|
service_type: u16,
|
||||||
|
payload: []const u8, // Points into slab — valid until ion_free
|
||||||
|
session_id: [16]u8,
|
||||||
|
dest_hint: [24]u8,
|
||||||
|
source_hint: [24]u8,
|
||||||
|
sequence: u32,
|
||||||
|
flags: u8,
|
||||||
|
encrypted: bool,
|
||||||
|
};
|
||||||
|
|
||||||
|
// =========================================================
|
||||||
|
// LWF Membrane Client
|
||||||
|
// =========================================================
|
||||||
|
|
||||||
|
pub const LwfClient = struct {
|
||||||
|
/// Callback type for incoming LWF frames
|
||||||
|
pub const FrameHandler = *const fn (frame: ProcessedFrame) void;
|
||||||
|
|
||||||
|
on_frame: ?FrameHandler,
|
||||||
|
|
||||||
|
pub fn init() LwfClient {
|
||||||
|
return .{
|
||||||
|
.on_frame = null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Register a callback for incoming LWF frames
|
||||||
|
pub fn setHandler(self: *LwfClient, handler: FrameHandler) void {
|
||||||
|
self.on_frame = handler;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse an LWF frame from a raw ION slab buffer.
|
||||||
|
/// The buffer starts AFTER the Ethernet header (NetSwitch strips it
|
||||||
|
/// to EtherType, but the ION packet still contains the full Ethernet
|
||||||
|
/// frame — so caller must offset by 14 bytes).
|
||||||
|
pub fn parseFrame(data: [*]const u8, len: u16) FrameError!ProcessedFrame {
|
||||||
|
if (len < HEADER_SIZE) return error.TooSmall;
|
||||||
|
|
||||||
|
// Magic check
|
||||||
|
if (data[0] != 'L' or data[1] != 'W' or data[2] != 'F' or data[3] != 0)
|
||||||
|
return error.InvalidMagic;
|
||||||
|
|
||||||
|
// Version check (offset 77)
|
||||||
|
if (data[77] != LWF_VERSION) return error.InvalidVersion;
|
||||||
|
|
||||||
|
// Parse fields
|
||||||
|
const payload_len = readU16Big(data[74..76]);
|
||||||
|
const total_needed = HEADER_SIZE + @as(usize, payload_len) + TRAILER_SIZE;
|
||||||
|
if (total_needed > len) return error.PayloadOverflow;
|
||||||
|
|
||||||
|
var frame: ProcessedFrame = undefined;
|
||||||
|
frame.service_type = readU16Big(data[72..74]);
|
||||||
|
frame.sequence = readU32Big(data[68..72]);
|
||||||
|
frame.flags = data[78];
|
||||||
|
frame.encrypted = (frame.flags & Flags.ENCRYPTED) != 0;
|
||||||
|
frame.payload = data[HEADER_SIZE .. HEADER_SIZE + payload_len];
|
||||||
|
|
||||||
|
@memcpy(&frame.dest_hint, data[4..28]);
|
||||||
|
@memcpy(&frame.source_hint, data[28..52]);
|
||||||
|
@memcpy(&frame.session_id, data[52..68]);
|
||||||
|
|
||||||
|
return frame;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build an outbound LWF frame into a slab buffer.
|
||||||
|
/// Returns the total frame size written.
|
||||||
|
pub fn buildFrame(
|
||||||
|
buf: []u8,
|
||||||
|
service_type: u16,
|
||||||
|
payload: []const u8,
|
||||||
|
session_id: [16]u8,
|
||||||
|
dest_hint: [24]u8,
|
||||||
|
source_hint: [24]u8,
|
||||||
|
sequence: u32,
|
||||||
|
flags: u8,
|
||||||
|
) FrameError!usize {
|
||||||
|
const total = HEADER_SIZE + payload.len + TRAILER_SIZE;
|
||||||
|
if (buf.len < total) return error.SlabTooSmall;
|
||||||
|
|
||||||
|
// Zero header + trailer regions
|
||||||
|
@memset(buf[0..HEADER_SIZE], 0);
|
||||||
|
@memset(buf[HEADER_SIZE + payload.len ..][0..TRAILER_SIZE], 0);
|
||||||
|
|
||||||
|
// Magic
|
||||||
|
buf[0] = 'L';
|
||||||
|
buf[1] = 'W';
|
||||||
|
buf[2] = 'F';
|
||||||
|
buf[3] = 0;
|
||||||
|
|
||||||
|
// Dest/Source hints
|
||||||
|
@memcpy(buf[4..28], &dest_hint);
|
||||||
|
@memcpy(buf[28..52], &source_hint);
|
||||||
|
|
||||||
|
// Session ID
|
||||||
|
@memcpy(buf[52..68], &session_id);
|
||||||
|
|
||||||
|
// Sequence
|
||||||
|
writeU32Big(buf[68..72], sequence);
|
||||||
|
|
||||||
|
// Service type + payload len
|
||||||
|
writeU16Big(buf[72..74], service_type);
|
||||||
|
writeU16Big(buf[74..76], @truncate(payload.len));
|
||||||
|
|
||||||
|
// Frame class (auto-select based on total size)
|
||||||
|
buf[76] = if (total <= 128) 0x00 // micro
|
||||||
|
else if (total <= 512) 0x01 // mini
|
||||||
|
else if (total <= 1350) 0x02 // standard
|
||||||
|
else if (total <= 4096) 0x03 // big
|
||||||
|
else 0x04; // jumbo
|
||||||
|
|
||||||
|
// Version
|
||||||
|
buf[77] = LWF_VERSION;
|
||||||
|
|
||||||
|
// Flags
|
||||||
|
buf[78] = flags;
|
||||||
|
|
||||||
|
// Payload
|
||||||
|
@memcpy(buf[HEADER_SIZE..][0..payload.len], payload);
|
||||||
|
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// =========================================================
|
||||||
|
// Integer Helpers
|
||||||
|
// =========================================================
|
||||||
|
|
||||||
|
fn readU16Big(bytes: *const [2]u8) u16 {
|
||||||
|
return (@as(u16, bytes[0]) << 8) | @as(u16, bytes[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn readU32Big(bytes: *const [4]u8) u32 {
|
||||||
|
return (@as(u32, bytes[0]) << 24) |
|
||||||
|
(@as(u32, bytes[1]) << 16) |
|
||||||
|
(@as(u32, bytes[2]) << 8) |
|
||||||
|
@as(u32, bytes[3]);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn writeU16Big(buf: *[2]u8, val: u16) void {
|
||||||
|
buf[0] = @truncate(val >> 8);
|
||||||
|
buf[1] = @truncate(val);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn writeU32Big(buf: *[4]u8, val: u32) void {
|
||||||
|
buf[0] = @truncate(val >> 24);
|
||||||
|
buf[1] = @truncate(val >> 16);
|
||||||
|
buf[2] = @truncate(val >> 8);
|
||||||
|
buf[3] = @truncate(val);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================
|
||||||
|
// Tests
|
||||||
|
// =========================================================
|
||||||
|
|
||||||
|
test "parseFrame valid" {
|
||||||
|
var buf: [512]u8 = undefined;
|
||||||
|
const payload = "Hello Noise";
|
||||||
|
const sz = try LwfClient.buildFrame(
|
||||||
|
&buf,
|
||||||
|
0x0001, // DATA_TRANSPORT
|
||||||
|
payload,
|
||||||
|
[_]u8{0xAA} ** 16, // session
|
||||||
|
[_]u8{0xBB} ** 24, // dest
|
||||||
|
[_]u8{0xCC} ** 24, // src
|
||||||
|
42,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
|
||||||
|
const frame = try LwfClient.parseFrame(&buf, @truncate(sz));
|
||||||
|
try std.testing.expectEqual(@as(u16, 0x0001), frame.service_type);
|
||||||
|
try std.testing.expectEqual(@as(u32, 42), frame.sequence);
|
||||||
|
try std.testing.expectEqualSlices(u8, payload, frame.payload);
|
||||||
|
try std.testing.expectEqual(@as(u8, 0xAA), frame.session_id[0]);
|
||||||
|
try std.testing.expect(!frame.encrypted);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "parseFrame encrypted flag" {
|
||||||
|
var buf: [512]u8 = undefined;
|
||||||
|
const sz = try LwfClient.buildFrame(
|
||||||
|
&buf,
|
||||||
|
0x0003, // IDENTITY_SIGNAL
|
||||||
|
"encrypted_payload",
|
||||||
|
[_]u8{0} ** 16,
|
||||||
|
[_]u8{0} ** 24,
|
||||||
|
[_]u8{0} ** 24,
|
||||||
|
1,
|
||||||
|
Flags.ENCRYPTED | Flags.SIGNED,
|
||||||
|
);
|
||||||
|
|
||||||
|
const frame = try LwfClient.parseFrame(&buf, @truncate(sz));
|
||||||
|
try std.testing.expect(frame.encrypted);
|
||||||
|
try std.testing.expectEqual(@as(u16, 0x0003), frame.service_type);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "buildFrame auto frame class" {
|
||||||
|
var buf: [2048]u8 = undefined;
|
||||||
|
|
||||||
|
// Micro (total <= 128)
|
||||||
|
_ = try LwfClient.buildFrame(&buf, 0, "", [_]u8{0} ** 16, [_]u8{0} ** 24, [_]u8{0} ** 24, 0, 0);
|
||||||
|
try std.testing.expectEqual(@as(u8, 0x00), buf[76]); // micro
|
||||||
|
|
||||||
|
// Standard (total > 512)
|
||||||
|
var payload: [500]u8 = undefined;
|
||||||
|
@memset(&payload, 0x42);
|
||||||
|
_ = try LwfClient.buildFrame(&buf, 0, &payload, [_]u8{0} ** 16, [_]u8{0} ** 24, [_]u8{0} ** 24, 0, 0);
|
||||||
|
try std.testing.expectEqual(@as(u8, 0x02), buf[76]); // standard
|
||||||
|
}
|
||||||
|
|
||||||
|
test "parseFrame rejects bad magic" {
|
||||||
|
var buf: [512]u8 = undefined;
|
||||||
|
_ = try LwfClient.buildFrame(&buf, 0, "x", [_]u8{0} ** 16, [_]u8{0} ** 24, [_]u8{0} ** 24, 0, 0);
|
||||||
|
buf[0] = 'X';
|
||||||
|
try std.testing.expectError(error.InvalidMagic, LwfClient.parseFrame(&buf, 160));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "buildFrame roundtrip preserves hints" {
|
||||||
|
var buf: [512]u8 = undefined;
|
||||||
|
const dest = [_]u8{0xDE} ** 24;
|
||||||
|
const src = [_]u8{0x5A} ** 24;
|
||||||
|
const sess = [_]u8{0xF0} ** 16;
|
||||||
|
|
||||||
|
_ = try LwfClient.buildFrame(&buf, 0x0800, "audio", sess, dest, src, 99, Flags.PRIORITY);
|
||||||
|
|
||||||
|
const frame = try LwfClient.parseFrame(&buf, 200);
|
||||||
|
try std.testing.expectEqual(@as(u16, 0x0800), frame.service_type);
|
||||||
|
try std.testing.expectEqual(@as(u32, 99), frame.sequence);
|
||||||
|
try std.testing.expectEqualSlices(u8, &dest, &frame.dest_hint);
|
||||||
|
try std.testing.expectEqualSlices(u8, &src, &frame.source_hint);
|
||||||
|
try std.testing.expectEqualSlices(u8, &sess, &frame.session_id);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
/* Minimal math.h stub for freestanding Nim builds */
|
||||||
|
#ifndef _MATH_H_STUB
|
||||||
|
#define _MATH_H_STUB
|
||||||
|
|
||||||
|
static inline double fabs(double x) { return x < 0 ? -x : x; }
|
||||||
|
static inline float fabsf(float x) { return x < 0 ? -x : x; }
|
||||||
|
static inline double fmod(double x, double y) { return x - (long long)(x / y) * y; }
|
||||||
|
static inline double floor(double x) { return (double)(long long)x - (x < (double)(long long)x); }
|
||||||
|
static inline double ceil(double x) { return (double)(long long)x + (x > (double)(long long)x); }
|
||||||
|
static inline double round(double x) { return floor(x + 0.5); }
|
||||||
|
|
||||||
|
#define HUGE_VAL __builtin_huge_val()
|
||||||
|
#define NAN __builtin_nan("")
|
||||||
|
#define INFINITY __builtin_inf()
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
#ifndef STDIO_H
|
||||||
|
#define STDIO_H
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <stdarg.h>
|
||||||
|
typedef void FILE;
|
||||||
|
#define stderr ((FILE*)0)
|
||||||
|
#define stdout ((FILE*)1)
|
||||||
|
int printf(const char* format, ...);
|
||||||
|
int sprintf(char* str, const char* format, ...);
|
||||||
|
int snprintf(char* str, size_t size, const char* format, ...);
|
||||||
|
size_t fwrite(const void* ptr, size_t size, size_t nmemb, FILE* stream);
|
||||||
|
int fflush(FILE* stream);
|
||||||
|
#endif
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
#ifndef STDLIB_H
|
||||||
|
#define STDLIB_H
|
||||||
|
#include <stddef.h>
|
||||||
|
void* malloc(size_t size);
|
||||||
|
void free(void* ptr);
|
||||||
|
void* realloc(void* ptr, size_t size);
|
||||||
|
void abort(void);
|
||||||
|
void exit(int status);
|
||||||
|
int atoi(const char* str);
|
||||||
|
#endif
|
||||||
|
|
@ -0,0 +1,86 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# ============================================================================
|
||||||
|
# Rumpk ARM64 QEMU Runner
|
||||||
|
# ============================================================================
|
||||||
|
# Usage:
|
||||||
|
# ./run_aarch64.sh # Interactive boot
|
||||||
|
# ./run_aarch64.sh test # Automated boot test
|
||||||
|
# ============================================================================
|
||||||
|
set -euo pipefail
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
|
||||||
|
KERNEL="zig-out/bin/rumpk.elf"
|
||||||
|
DISK="build/disk_aarch64.img"
|
||||||
|
|
||||||
|
if [ ! -f "$KERNEL" ]; then
|
||||||
|
echo "ERROR: $KERNEL not found. Run: zig build -Darch=aarch64"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create disk image if missing (16MB for LittleFS)
|
||||||
|
if [ ! -f "$DISK" ]; then
|
||||||
|
mkdir -p build
|
||||||
|
dd if=/dev/zero of="$DISK" bs=1M count=16 2>/dev/null
|
||||||
|
echo "Created $DISK (16MB)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
MODE="${1:-interactive}"
|
||||||
|
|
||||||
|
# VirtIO MMIO devices for aarch64 virt
|
||||||
|
VIRTIO_NET="-device virtio-net-device,netdev=net0 -netdev user,id=net0"
|
||||||
|
VIRTIO_BLK="-device virtio-blk-device,drive=hd0 -drive if=none,id=hd0,format=raw,file=$DISK"
|
||||||
|
|
||||||
|
if [ "$MODE" = "test" ]; then
|
||||||
|
echo "=== Rumpk ARM64 Boot Test ==="
|
||||||
|
TIMEOUT=20
|
||||||
|
|
||||||
|
LOGFILE=$(mktemp /tmp/rumpk-aarch64-XXXXXX.log)
|
||||||
|
timeout "$TIMEOUT" qemu-system-aarch64 \
|
||||||
|
-M virt -cpu cortex-a72 -m 512M \
|
||||||
|
-nographic -serial mon:stdio \
|
||||||
|
-kernel "$KERNEL" \
|
||||||
|
$VIRTIO_NET $VIRTIO_BLK \
|
||||||
|
-no-reboot >"$LOGFILE" 2>&1 || true
|
||||||
|
|
||||||
|
cat "$LOGFILE"
|
||||||
|
echo ""
|
||||||
|
echo "=== Boot Checks ==="
|
||||||
|
|
||||||
|
PASS=0
|
||||||
|
FAIL=0
|
||||||
|
|
||||||
|
check() {
|
||||||
|
local label="$1"
|
||||||
|
local pattern="$2"
|
||||||
|
if grep -qa "$pattern" "$LOGFILE"; then
|
||||||
|
echo " [PASS] $label"
|
||||||
|
PASS=$((PASS + 1))
|
||||||
|
else
|
||||||
|
echo " [FAIL] $label"
|
||||||
|
FAIL=$((FAIL + 1))
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
check "UART loopback" "Loopback Test: PASS"
|
||||||
|
check "aarch64_init reached" "aarch64_init reached"
|
||||||
|
check "GICv2 initialized" "GICv2 initialized"
|
||||||
|
check "Timer initialized" "Timer initialized"
|
||||||
|
check "Vectors installed" "vectors installed"
|
||||||
|
check "Nim handoff" "Handing off to Nim"
|
||||||
|
check "VirtIO-Net found" "VirtIO-Net"
|
||||||
|
check "VirtIO-Blk found" "VirtIO-Block\|VirtIO-Blk\|Storage initialized"
|
||||||
|
check "VirtIO init complete" "Initialization Complete"
|
||||||
|
|
||||||
|
TOTAL=$((PASS + FAIL))
|
||||||
|
echo ""
|
||||||
|
echo "=== Result: $PASS/$TOTAL ==="
|
||||||
|
if [ "$FAIL" -gt 0 ]; then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
exec qemu-system-aarch64 \
|
||||||
|
-M virt -cpu cortex-a72 -m 512M \
|
||||||
|
-nographic -serial mon:stdio \
|
||||||
|
-kernel "$KERNEL" \
|
||||||
|
$VIRTIO_NET $VIRTIO_BLK
|
||||||
|
fi
|
||||||
|
|
@ -0,0 +1,127 @@
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
// LittleFS freestanding configuration for Rumpk unikernel.
|
||||||
|
//
|
||||||
|
// Replaces lfs_util.h entirely via -DLFS_CONFIG=lfs_rumpk.h.
|
||||||
|
// Provides minimal stubs using kernel-provided malloc/free/memcpy/memset.
|
||||||
|
#ifndef LFS_RUMPK_H
|
||||||
|
#define LFS_RUMPK_H
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stddef.h>
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// --- NULL (freestanding stddef.h may not always provide it) ---
|
||||||
|
#ifndef NULL
|
||||||
|
#define NULL ((void *)0)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// --- Logging: all disabled in freestanding kernel ---
|
||||||
|
#define LFS_TRACE(...)
|
||||||
|
#define LFS_DEBUG(...)
|
||||||
|
#define LFS_WARN(...)
|
||||||
|
#define LFS_ERROR(...)
|
||||||
|
|
||||||
|
// --- Assertions: disabled ---
|
||||||
|
#define LFS_ASSERT(test) ((void)(test))
|
||||||
|
|
||||||
|
// --- Memory functions (provided by clib.c / libc_shim.zig) ---
|
||||||
|
extern void *malloc(unsigned long size);
|
||||||
|
extern void free(void *ptr);
|
||||||
|
extern void *memcpy(void *dest, const void *src, unsigned long n);
|
||||||
|
extern void *memset(void *s, int c, unsigned long n);
|
||||||
|
extern int memcmp(const void *s1, const void *s2, unsigned long n);
|
||||||
|
extern unsigned long strlen(const char *s);
|
||||||
|
|
||||||
|
// strchr — needed by lfs.c for path parsing
|
||||||
|
static inline char *strchr(const char *s, int c) {
|
||||||
|
while (*s) {
|
||||||
|
if (*s == (char)c) return (char *)s;
|
||||||
|
s++;
|
||||||
|
}
|
||||||
|
return (c == '\0') ? (char *)s : NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline void *lfs_malloc(unsigned long size) {
|
||||||
|
return malloc(size);
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline void lfs_free(void *p) {
|
||||||
|
free(p);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Builtins ---
|
||||||
|
static inline uint32_t lfs_max(uint32_t a, uint32_t b) {
|
||||||
|
return (a > b) ? a : b;
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline uint32_t lfs_min(uint32_t a, uint32_t b) {
|
||||||
|
return (a < b) ? a : b;
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline uint32_t lfs_aligndown(uint32_t a, uint32_t alignment) {
|
||||||
|
return a - (a % alignment);
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline uint32_t lfs_alignup(uint32_t a, uint32_t alignment) {
|
||||||
|
return lfs_aligndown(a + alignment - 1, alignment);
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline uint32_t lfs_npw2(uint32_t a) {
|
||||||
|
#if defined(__GNUC__) || defined(__clang__)
|
||||||
|
return 32 - __builtin_clz(a - 1);
|
||||||
|
#else
|
||||||
|
uint32_t r = 0, s;
|
||||||
|
a -= 1;
|
||||||
|
s = (a > 0xffff) << 4; a >>= s; r |= s;
|
||||||
|
s = (a > 0xff ) << 3; a >>= s; r |= s;
|
||||||
|
s = (a > 0xf ) << 2; a >>= s; r |= s;
|
||||||
|
s = (a > 0x3 ) << 1; a >>= s; r |= s;
|
||||||
|
return (r | (a >> 1)) + 1;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline uint32_t lfs_ctz(uint32_t a) {
|
||||||
|
#if defined(__GNUC__) || defined(__clang__)
|
||||||
|
return __builtin_ctz(a);
|
||||||
|
#else
|
||||||
|
return lfs_npw2((a & -a) + 1) - 1;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline uint32_t lfs_popc(uint32_t a) {
|
||||||
|
#if defined(__GNUC__) || defined(__clang__)
|
||||||
|
return __builtin_popcount(a);
|
||||||
|
#else
|
||||||
|
a = a - ((a >> 1) & 0x55555555);
|
||||||
|
a = (a & 0x33333333) + ((a >> 2) & 0x33333333);
|
||||||
|
return (((a + (a >> 4)) & 0xf0f0f0f) * 0x1010101) >> 24;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline int lfs_scmp(uint32_t a, uint32_t b) {
|
||||||
|
return (int)(unsigned)(a - b);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Endianness (RISC-V / ARM64 / x86_64 are all little-endian) ---
|
||||||
|
static inline uint32_t lfs_fromle32(uint32_t a) { return a; }
|
||||||
|
static inline uint32_t lfs_tole32(uint32_t a) { return a; }
|
||||||
|
|
||||||
|
static inline uint32_t lfs_frombe32(uint32_t a) {
|
||||||
|
return __builtin_bswap32(a);
|
||||||
|
}
|
||||||
|
static inline uint32_t lfs_tobe32(uint32_t a) {
|
||||||
|
return __builtin_bswap32(a);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- CRC-32 (implementation in lfs.c) ---
|
||||||
|
uint32_t lfs_crc(uint32_t crc, const void *buffer, unsigned long size);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif // LFS_RUMPK_H
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
#!/bin/sh
|
||||||
|
zig cc -target riscv64-freestanding-none -mcpu=sifive_u54 "$@"
|
||||||
Loading…
Reference in New Issue