Files
LithosAnanake/src/starkernel/vm/host/shim.c
T
2026-09-01 12:07:32 -04:00

1311 lines
47 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 20232025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 20232025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* shim.c - Kernel shims for VM integration
*
* Provides minimal implementations of logging, mutex, time, and malloc/free
* to satisfy the hosted VM when linked into the kernel. All logging is muted
* to avoid dependencies on libc formatting.
*/
#ifdef __STARKERNEL__
/* shim.c provides concrete sf_time wrappers below; suppress the inline versions.
* PLATFORM_TIME_NO_INLINE is also set globally via COMMON_CFLAGS in Makefile.starkernel,
* but kept here as documentation and for non-Makefile build contexts. */
#ifndef PLATFORM_TIME_NO_INLINE
#define PLATFORM_TIME_NO_INLINE
#endif
#include "platform_time.h"
#include "platform_lock.h"
/* No LOG_LINE_MAX include-order constraint anymore: vm.h's own
* LOG_LINE_MAX (persistent block-log line size, 64) and log.h's in-memory
* message line length (renamed LOG_MSG_LINE_MAX, 256) no longer share a
* name, so include order is irrelevant here. */
#include "starkernel/repl.h"
#include "starkernel/vm/bootstrap/sk_vm_bootstrap.h"
#include "log.h"
#include "vm_host.h"
#include "console.h"
#include "kmalloc.h"
#include "platform_io.h"
#include <string.h>
#include <stdarg.h>
#include <stdint.h>
#include <stddef.h>
/* -----------------------------------------------------------------------------
* abort() — freestanding: print to console and spin
* ---------------------------------------------------------------------------*/
/**
* @brief Freestanding @c abort() replacement for the kernel build.
*
* Prints @c "[ABORT] abort() called — halting" to the kernel console and
* enters an infinite spin loop with a memory-clobber barrier to prevent the
* compiler from optimizing it away. Never returns. Replaces the libc
* @c abort() so that code paths that call it on fatal errors (e.g.,
* @c assert(), debug panics) are safe in the freestanding kernel environment.
*/
void abort(void) {
console_println("[ABORT] abort() called — halting");
for (;;) { __asm__ volatile ("" ::: "memory"); }
}
/* -----------------------------------------------------------------------------
* Minimal malloc/free backed by kmalloc/kfree
* ---------------------------------------------------------------------------*/
/**
* @brief Kernel @c malloc() stub backed by @c kmalloc().
*
* Satisfies the libc @c malloc() contract expected by shared VM code. Returns
* @c NULL for zero-size requests; otherwise delegates to @c kmalloc(). The
* kernel heap is finite — callers must not assume the same retry or OOM
* semantics as a POSIX @c malloc().
*
* @param size Number of bytes to allocate.
* @return Pointer to allocated memory, or @c NULL if @p size is zero or
* the kernel heap is exhausted.
*/
void *malloc(size_t size) {
if (size == 0) return NULL;
return kmalloc(size);
}
/**
* @brief Kernel @c free() stub backed by @c kfree().
*
* Releases memory previously allocated by @c malloc(). Silently ignores
* @c NULL pointers (matching standard @c free() behaviour). Delegates to
* @c kfree() for the actual reclaim.
*
* @param ptr Pointer to free; no-op if @c NULL.
*/
void free(void *ptr) {
if (ptr) kfree(ptr);
}
/**
* @brief Kernel @c calloc() stub: allocate and zero @p n × @p size bytes.
*
* Computes the total size, delegates allocation to @c malloc(), then zeroes
* the block with @c memset(). Does not guard against integer overflow in
* @c n × @p size — callers must ensure the product fits in @c size_t.
*
* @param n Number of elements to allocate.
* @param size Size of each element in bytes.
* @return Pointer to zeroed memory, or @c NULL on failure.
*/
void *calloc(size_t n, size_t size) {
size_t total = n * size;
void *p = malloc(total);
if (p) memset(p, 0, total);
return p;
}
/**
* @brief Kernel @c realloc() stub: resize a heap allocation.
*
* Implements standard @c realloc() semantics using @c malloc() + @c memcpy() +
* @c free(). Because the old allocation size is not tracked by @c kmalloc(),
* the copy uses @p size as the byte count — meaning this truncates the content
* if @p size < old size, and does not guarantee the tail bytes are zero if
* @p size > old size. Suitable for rare realloc calls in shared VM code.
*
* Special cases:
* - @p ptr == @c NULL → equivalent to @c malloc(@p size).
* - @p size == 0 → frees @p ptr and returns @c NULL.
*
* @param ptr Pointer to existing allocation; may be @c NULL.
* @param size New desired size in bytes.
* @return Pointer to resized block, or @c NULL on failure or when @p size is 0.
*/
void *realloc(void *ptr, size_t size) {
if (!ptr) return malloc(size);
if (size == 0) {
free(ptr);
return NULL;
}
/* Simple realloc: allocate new, copy min(old,new), free old */
void *newp = malloc(size);
if (newp) {
/* Unknown old size; copy conservatively */
memcpy(newp, ptr, size);
free(ptr);
}
return newp;
}
/* -----------------------------------------------------------------------------
* Minimal logging stubs (no formatting)
* ---------------------------------------------------------------------------*/
static LogLevel current_level = LOG_INFO;
/**
* @brief Set the active kernel log level filter.
*
* Messages at levels numerically greater than @p level are suppressed.
* In release builds set to @c LOG_TEST; in @c SK_PARITY_DEBUG builds set to
* @c LOG_DEBUG to capture traversal traces.
*
* @param level New log level threshold (e.g., @c LOG_INFO, @c LOG_DEBUG).
*/
void log_set_level(LogLevel level) { current_level = level; }
/**
* @brief Return the currently active kernel log level filter.
*
* @return The @c LogLevel value last set by @c log_set_level(), or the
* default @c LOG_INFO if @c log_set_level() has not been called.
*/
LogLevel log_get_level(void) { return current_level; }
static int kvsnprintf(char *buf, size_t n, const char *fmt, va_list args);
/**
* @brief Read an architecture-specific cycle or time counter for log timestamps.
*
* Returns a monotonically increasing 64-bit counter suitable for computing
* relative elapsed ticks between log calls. The counter is:
* - x86-64/i386: RDTSC (cycle counter)
* - AArch64: @c cntpct_el0 (physical timer count, preceded by ISB)
* - RISC-V: @c rdcycle pseudoinstruction
* - Other: always 0
*
* The absolute frequency is not calibrated; the value is used only as a
* relative delta from @c log_tsc_base captured on the first log call.
*
* @return Raw counter value for the current ISA.
*/
static inline uint64_t shim_rdtsc(void) {
#if defined(__x86_64__) || defined(_M_X64) || defined(__i386__)
uint32_t lo, hi;
__asm__ volatile ("rdtsc" : "=a"(lo), "=d"(hi));
return ((uint64_t)hi << 32) | lo;
#elif defined(__aarch64__) || defined(_M_ARM64)
uint64_t val;
__asm__ volatile ("isb\n\tmrs %0, cntpct_el0" : "=r"(val));
return val;
#elif defined(__riscv)
uint64_t val;
__asm__ volatile ("rdcycle %0" : "=r"(val));
return val;
#else
return 0;
#endif
}
static uint64_t log_tsc_base = 0; /* Captured on first log call */
/**
* @brief Write a @c uint64_t value as a decimal ASCII string into @p buf.
*
* Writes decimal digits for @p val into @p buf starting at the current
* position and returns a pointer one past the last digit written. Used by
* @c log_message() to format the @c "[KRELTSC: N]" relative-tick prefix
* without depending on @c sprintf() or libc digit-conversion routines.
* No null terminator is written — the caller is responsible.
*
* @param buf Destination buffer; must have at least 20 bytes of space.
* @param val Value to convert.
* @return Pointer to the byte immediately after the last digit written.
*/
static char *u64_to_dec(char *buf, uint64_t val) {
char tmp[21];
int i = 0;
if (val == 0) {
*buf++ = '0';
return buf;
}
while (val > 0) {
tmp[i++] = '0' + (val % 10);
val /= 10;
}
while (i > 0) {
*buf++ = tmp[--i];
}
return buf;
}
/**
* @brief Emit a formatted log message to the kernel console with ANSI colour.
*
* Formats @p fmt + variadic arguments using the internal @c kvsnprintf()
* formatter (no libc dependency) and emits a coloured log line to the kernel
* console in the form:
* @code
* COLOR[HADES][LEVEL] RESET[KRELTSC: N] message
* @endcode
*
* The @c KRELTSC value is the TSC/timer delta (in raw ticks) from the first
* call to this function, captured in @c log_tsc_base. This provides relative
* timestamps in QEMU serial output without requiring a calibrated clock.
*
* Log level colours: ERROR=red, WARN=yellow, INFO=green, TEST=magenta,
* DEBUG=blue. Messages whose @p level exceeds @c current_level are dropped.
*
* @param level Severity of the message; suppressed if > @c current_level.
* @param fmt @c printf-style format string (must not be @c NULL).
* @param ... Format arguments.
*/
void log_message(LogLevel level, const char *fmt, ...) {
if (level > current_level || !fmt) return;
/* Relative TSC timestamp */
uint64_t now = shim_rdtsc();
if (log_tsc_base == 0) log_tsc_base = now;
uint64_t rel_tsc = now - log_tsc_base;
/* ANSI colors and level names indexed by LogLevel (04) */
static const char *colors[] = {
"\x1b[31m", /* ERROR — red */
"\x1b[33m", /* WARN — yellow */
"\x1b[32m", /* INFO — green */
"\x1b[35m", /* TEST — magenta */
"\x1b[34m", /* DEBUG — blue */
};
static const char *names[] = {
"ERROR", "WARN ", "INFO ", "TEST ", "DEBUG"
};
static const char *reset = "\x1b[0m";
int idx = (level < LOG_ERROR || level > LOG_DEBUG) ? LOG_ERROR : level;
/* Format message */
char buf[LOG_LINE_MAX];
va_list args;
va_start(args, fmt);
kvsnprintf(buf, sizeof(buf), fmt, args);
va_end(args);
/* Build KRELTSC prefix */
char prefix[32];
char *p = prefix;
const char *tag = "[KRELTSC: ";
while (*tag) *p++ = *tag++;
p = u64_to_dec(p, rel_tsc);
*p++ = ']';
*p++ = ' ';
*p = '\0';
/* Emit: COLOR[HADES][LEVEL] RESET[KRELTSC: xxx] message\n */
console_puts(colors[idx]);
console_puts("[HADES][");
console_puts(names[idx]);
console_puts("] ");
console_puts(reset);
console_puts(prefix);
console_println(buf);
}
/**
* @brief Emit a POST test result line to the kernel console.
*
* Prints a coloured @c "Testing <word_name> ... PASS/FAIL/SKIP\n" line to
* the kernel console, visible in QEMU serial output. Suppressed if
* @c current_level < @c LOG_TEST or if @p word_name is @c NULL.
*
* @param word_name Name of the FORTH word under test.
* @param result Test outcome: @c TEST_PASS, @c TEST_FAIL, or @c TEST_SKIP.
*/
void log_test_result(const char *word_name, TestResult result) {
if (current_level == LOG_NONE || current_level < LOG_TEST) return;
if (!word_name) return;
const char *color;
const char *status;
switch (result) {
case TEST_PASS: color = "\x1b[32m"; status = "PASS"; break;
case TEST_FAIL: color = "\x1b[31m"; status = "FAIL"; break;
case TEST_SKIP: color = "\x1b[33m"; status = "SKIP"; break;
default: color = "\x1b[0m"; status = "????"; break;
}
console_puts("\x1b[35m[HADES][TEST ] \x1b[0m");
console_puts("Testing ");
console_puts(word_name);
console_puts(" ... ");
console_puts(color);
console_puts(status);
console_println("\x1b[0m");
}
/**
* @brief No-op VM pointer registration for the kernel log shim.
*
* The hosted log subsystem stores a VM pointer for context in formatted
* log lines. The kernel shim has no such context (log goes directly to the
* console) so this is a deliberate no-op.
*
* @param vm Ignored.
*/
void log_set_vm(struct VM *vm) { (void)vm; }
/* -----------------------------------------------------------------------------
* Mutex stubs (single-threaded kernel)
* ---------------------------------------------------------------------------*/
/**
* @brief Initialise a kernel mutex (sets state to 0 — unlocked).
*
* The kernel VM runs on a single thread with no preemption at the VM level,
* so the full spinlock/pthread path is unnecessary. This stub zeros the
* @c state field in @p mutex to leave it in a consistent unlocked state.
*
* @param mutex Mutex to initialise; returns -1 immediately if @c NULL.
* @return 0 on success, -1 if @p mutex is @c NULL.
*/
int sf_mutex_init(sf_mutex_t *mutex) {
if (!mutex) return -1;
mutex->state = 0;
return 0;
}
/**
* @brief Destroy a kernel mutex (no-op).
*
* Nothing to release in the kernel stub — the mutex holds only a single
* @c int field and has no OS resources.
*
* @param mutex Ignored.
*/
void sf_mutex_destroy(sf_mutex_t *mutex) {
(void)mutex;
}
/**
* @brief Acquire a kernel mutex (no-op).
*
* Single-threaded kernel: no contention is possible at the VM level, so
* acquiring the mutex is always immediate and guaranteed to succeed.
*
* This no-op is correct only as long as nothing running in interrupt
* context mutates the structure a given lock protects (FABRIC.md §21.2,
* §25.1 item 0.9). If that ever stops being true, making this a real
* spinlock is not the fix: a single hart taking an interrupt while
* already holding the lock would spin against itself and deadlock. The
* correct fix in that case is disabling interrupts around the critical
* section, not adding a spinlock here.
*
* @param mutex Ignored.
*/
void sf_mutex_lock(sf_mutex_t *mutex) {
(void)mutex;
}
/**
* @brief Release a kernel mutex (no-op).
*
* Single-threaded kernel: matching @c sf_mutex_lock() no-op.
*
* @param mutex Ignored.
*/
void sf_mutex_unlock(sf_mutex_t *mutex) {
(void)mutex;
}
/* -----------------------------------------------------------------------------
* Time backend using host services (deterministic in PARITY_MODE)
* ---------------------------------------------------------------------------*/
#include "starkernel/hal/hal.h"
#undef SK_TIME_NS
#define SK_TIME_NS() sk_hal_time_ns()
/**
* @brief Return the current monotonic time in nanoseconds via HAL.
*
* Delegates to @c SK_TIME_NS() which expands to @c sk_hal_time_ns().
* In @c PARITY_MODE builds the HAL returns a deterministic synthetic clock
* rather than the real TSC, ensuring tick-identical results across runs.
*
* @return Nanoseconds since boot (monotonic, never decreasing).
*/
static uint64_t shim_monotonic_ns(void) {
return SK_TIME_NS();
}
/**
* @brief Return the current wall-clock time in nanoseconds (aliased to monotonic).
*
* The kernel shim has no independent RTC path at the shim level; real-time
* is served by the same HAL monotonic clock. Code that needs wall time should
* use the HAL RTC capability directly.
*
* @return Same value as @c shim_monotonic_ns().
*/
static uint64_t shim_realtime_ns(void) {
return shim_monotonic_ns();
}
/**
* @brief Attempt to set the real-time clock (always fails in shim).
*
* The shim does not implement RTC write-back; the HAL owns that responsibility.
* Returns -1 unconditionally so callers degrade gracefully.
*
* @param ns Desired epoch time in nanoseconds (ignored).
* @return -1 always.
*/
static int shim_set_realtime_ns(uint64_t ns) {
(void)ns;
return -1;
}
/**
* @brief Format a nanosecond timestamp as a human-readable string (stub).
*
* The shim does not implement date/time formatting — the kernel has no libc
* @c strftime() or @c localtime(). Writes a single @c "0" string to @p buf
* so that shared VM code expecting a non-NULL timestamp string does not crash.
*
* @param ns Timestamp to format (ignored).
* @param buf Output buffer; must be at least 2 bytes.
* @param format_24h Ignored.
* @return 0 on success, -1 if @p buf is @c NULL.
*/
static int shim_format_timestamp(uint64_t ns, char *buf, int format_24h) {
(void)ns;
(void)format_24h;
if (!buf) return -1;
buf[0] = '0';
buf[1] = '\0';
return 0;
}
/**
* @brief Report RTC availability (always unavailable in shim).
*
* The shim time backend has no RTC access. Returns 0 so callers fall back
* to monotonic time rather than attempting RTC reads.
*
* @return 0 always (no RTC).
*/
static int shim_has_rtc(void) { return 0; }
static const sf_time_backend_t shim_backend = {
.get_monotonic_ns = shim_monotonic_ns,
.get_realtime_ns = shim_realtime_ns,
.set_realtime_ns = shim_set_realtime_ns,
.format_timestamp = shim_format_timestamp,
.has_rtc = shim_has_rtc
};
const sf_time_backend_t *sf_time_backend = &shim_backend;
/**
* @brief Initialise the shim time backend pointer.
*
* Explicitly sets @c sf_time_backend to @c &shim_backend rather than relying
* on the static-const initialiser. This avoids GOT-relative relocation issues
* in the PE32+ kernel image where @c -fPIC generates GOT accesses that cannot
* be resolved at kernel link time. Must be called early in kernel bootstrap,
* before any @c sf_monotonic_ns() or @c sf_realtime_ns() calls.
*/
void sf_time_init(void) {
sf_time_backend = &shim_backend;
}
/* Direct wrappers for sf_time functions - avoids GOT indirection issues with -fPIC.
* The inline functions in platform_time.h generate GOT-relative accesses for
* sf_time_backend, which causes extra dereferences in PE files without a GOT.
* These wrappers call the local shim functions directly. */
/**
* @brief Return monotonic nanoseconds — kernel direct wrapper.
*
* Bypasses the @c sf_time_backend vtable to avoid GOT-relative dereferences
* in the PE32+ image. Calls @c shim_monotonic_ns() directly.
*
* @return Nanoseconds since boot.
*/
sf_time_ns_t sf_monotonic_ns(void) {
return shim_monotonic_ns();
}
/**
* @brief Return real-time nanoseconds — kernel direct wrapper.
*
* Bypasses the @c sf_time_backend vtable. Returns the same monotonic value
* as @c sf_monotonic_ns() (the shim aliases realtime to monotonic).
*
* @return Nanoseconds since boot (no RTC offset applied).
*/
sf_time_ns_t sf_realtime_ns(void) {
return shim_realtime_ns();
}
/**
* @brief Set the real-time clock — kernel direct wrapper (always fails).
*
* Bypasses the @c sf_time_backend vtable. Returns -1 unconditionally; the
* kernel shim does not support RTC write-back.
*
* @param ns Desired epoch time in nanoseconds (ignored).
* @return -1 always.
*/
int sf_set_realtime_ns(sf_time_ns_t ns) {
return shim_set_realtime_ns(ns);
}
/**
* @brief Format a timestamp — kernel direct wrapper (stub).
*
* Bypasses the @c sf_time_backend vtable. Writes @c "0" to @p buf; see
* @c shim_format_timestamp() for full behaviour.
*
* @param ns Timestamp to format (ignored).
* @param buf Output buffer (at least 2 bytes).
* @param format_24h Ignored.
* @return 0 on success, -1 if @p buf is @c NULL.
*/
int sf_format_timestamp(sf_time_ns_t ns, char *buf, int format_24h) {
return shim_format_timestamp(ns, buf, format_24h);
}
/**
* @brief Query RTC availability — kernel direct wrapper (always 0).
*
* Bypasses the @c sf_time_backend vtable.
*
* @return 0 always (no RTC in shim).
*/
int sf_has_rtc(void) {
return shim_has_rtc();
}
/* -----------------------------------------------------------------------------
* Minimal string/memory functions
* ---------------------------------------------------------------------------*/
/**
* @brief Fill @p n bytes of @p s with @p c (freestanding @c memset).
*
* Replaces libc @c memset() for the kernel build. Simple byte loop with no
* SIMD acceleration; acceptable for the low-frequency use in VM init paths.
*
* @param s Destination buffer.
* @param c Fill byte value (cast to @c unsigned char).
* @param n Number of bytes to fill.
* @return @p s.
*/
void *memset(void *s, int c, size_t n) {
unsigned char *p = s;
while (n--) *p++ = (unsigned char)c;
return s;
}
/**
* @brief Copy @p n bytes from @p src to @p dst (freestanding @c memcpy).
*
* Undefined behaviour if the regions overlap — use @c memmove() for that case.
* Simple forward byte loop; no SIMD acceleration.
*
* @param dst Destination buffer.
* @param src Source buffer.
* @param n Number of bytes to copy.
* @return @p dst.
*/
void *memcpy(void *dst, const void *src, size_t n) {
unsigned char *d = dst;
const unsigned char *s = src;
while (n--) *d++ = *s++;
return dst;
}
/**
* @brief Copy @p n bytes from @p src to @p dst, handling overlap (freestanding @c memmove).
*
* Copies forward if @p dst < @p src, backward otherwise, so overlapping
* regions are handled correctly. Returns immediately if @p dst == @p src or
* @p n is zero.
*
* @param dst Destination buffer.
* @param src Source buffer.
* @param n Number of bytes to copy.
* @return @p dst.
*/
void *memmove(void *dst, const void *src, size_t n) {
unsigned char *d = dst;
const unsigned char *s = src;
if (d == s || n == 0) return dst;
if (d < s) {
while (n--) *d++ = *s++;
} else {
d += n; s += n;
while (n--) *--d = *--s;
}
return dst;
}
/**
* @brief Compare @p n bytes of @p a and @p b (freestanding @c memcmp).
*
* @param a First buffer.
* @param b Second buffer.
* @param n Number of bytes to compare.
* @return Negative if @p a < @p b, zero if equal, positive if @p a > @p b
* (as signed difference of the differing bytes).
*/
int memcmp(const void *a, const void *b, size_t n) {
const unsigned char *p = a, *q = b;
while (n--) {
if (*p != *q) return (int)*p - (int)*q;
p++; q++;
}
return 0;
}
/**
* @brief Search @p n bytes of @p s for byte value @p c (freestanding @c memchr).
*
* @param s Pointer to memory to search.
* @param c Byte value to find (compared as @c unsigned char).
* @param n Maximum bytes to scan.
* @return Pointer to first occurrence of @p c in @p s, or @c NULL if not found.
*/
void *memchr(const void *s, int c, size_t n) {
const unsigned char *p = s;
while (n--) {
if (*p == (unsigned char)c) return (void *)p;
p++;
}
return NULL;
}
/**
* @brief Return the length of a null-terminated string (freestanding @c strlen).
*
* Returns 0 if @p s is @c NULL (defensive; standard @c strlen has undefined
* behaviour on NULL).
*
* @param s Null-terminated string.
* @return Number of bytes before the null terminator.
*/
size_t strlen(const char *s) {
size_t n = 0;
while (s && *s++) n++;
return n;
}
/**
* @brief Compare two null-terminated strings (freestanding @c strcmp).
*
* @param a First string.
* @param b Second string.
* @return Negative, zero, or positive as @p a is less than, equal to, or
* greater than @p b (unsigned char comparison).
*/
int strcmp(const char *a, const char *b) {
while (*a && (*a == *b)) { a++; b++; }
return (unsigned char)*a - (unsigned char)*b;
}
/**
* @brief Compare up to @p n bytes of two strings (freestanding @c strncmp).
*
* @param a First string.
* @param b Second string.
* @param n Maximum number of characters to compare.
* @return Negative, zero, or positive as the bounded prefixes compare
* less than, equal to, or greater than each other.
*/
int strncmp(const char *a, const char *b, size_t n) {
while (n-- && *a && (*a == *b)) { a++; b++; }
return n == (size_t)-1 ? 0 : (unsigned char)*a - (unsigned char)*b;
}
/**
* @brief Copy up to @p n bytes from @p src to @p dst, padding with NUL (freestanding @c strncpy).
*
* If @p src is shorter than @p n bytes, the remainder of @p dst is filled
* with @c '\\0'. If @p src is @p n or more bytes long, @p dst is not
* null-terminated — the same classic caveat as standard @c strncpy().
*
* @param dst Destination buffer of at least @p n bytes.
* @param src Source null-terminated string.
* @param n Maximum bytes to write.
* @return @p dst.
*/
char *strncpy(char *dst, const char *src, size_t n) {
size_t i = 0;
for (; i < n && src[i]; i++) dst[i] = src[i];
for (; i < n; i++) dst[i] = '\0';
return dst;
}
/**
* @brief Find the first occurrence of byte @p c in string @p s (freestanding @c strchr).
*
* Searches forward including the null terminator, so @c strchr(s, 0) returns
* a pointer to the null terminator.
*
* @param s Null-terminated string to search.
* @param c Character to find.
* @return Pointer to first occurrence, or @c NULL if @p c is not in @p s
* (and @p c != 0).
*/
char *strchr(const char *s, int c) {
while (*s) { if (*s == (char)c) return (char*)s; s++; }
return (c == 0) ? (char*)s : NULL;
}
/**
* @brief Find the first occurrence of needle @p n in haystack @p h (freestanding @c strstr).
*
* Returns @p h immediately if @p n is an empty string. Uses a naive O(|h||n|)
* scan — acceptable for short strings in kernel diagnostics.
*
* @param h Null-terminated haystack string.
* @param n Null-terminated needle string.
* @return Pointer to first occurrence of @p n in @p h, or @c NULL.
*/
char *strstr(const char *h, const char *n) {
if (!*n) return (char*)h;
for (; *h; h++) {
if (*h == *n) {
const char *a = h, *b = n;
while (*a && *b && *a == *b) { a++; b++; }
if (!*b) return (char*)h;
}
}
return NULL;
}
/**
* @brief Convert a string to a @c long integer (freestanding @c strtol).
*
* Supports base 10 and hex digit characters (af, AF). Handles a leading
* '-' for negative values. Sets @c *endptr to the first unconverted character
* if @p endptr is non-NULL. Does not set @c errno; does not detect overflow.
*
* @param nptr Null-terminated string to convert.
* @param endptr If non-NULL, receives pointer past the last converted digit.
* @param base Numeric base (only 0 and 10 are treated identically; base 16
* digit letters are always recognised).
* @return Converted @c long value, or 0 if @p nptr is @c NULL.
*/
long strtol(const char *nptr, char **endptr, int base) {
const char *s = nptr;
long sign = 1, acc = 0;
if (!s) return 0;
if (*s == '-') { sign = -1; s++; }
if (base == 0) base = 10;
while (*s) {
int d = (*s >= '0' && *s <= '9') ? *s - '0' :
(*s >= 'a' && *s <= 'f') ? *s - 'a' + 10 :
(*s >= 'A' && *s <= 'F') ? *s - 'A' + 10 : -1;
if (d < 0 || d >= base) break;
acc = acc * base + d;
s++;
}
if (endptr) *endptr = (char*)s;
return sign * acc;
}
/** @brief Convert string to @c unsigned long; wraps @c strtol() with cast. */
unsigned long strtoul(const char *s, char **endptr, int base)
{ return (unsigned long)strtol(s, endptr, base); }
/** @brief Convert string to @c long long; wraps @c strtol() with cast. */
long long strtoll(const char *s, char **endptr, int base)
{ return (long long)strtol(s, endptr, base); }
/** @brief Convert decimal string to @c int; wraps @c strtol(s, NULL, 10). */
int atoi(const char *s) { return (int)strtol(s, (char**)0, 10); }
/** @brief Convert decimal string to @c long; wraps @c strtol(s, NULL, 10). */
long atol(const char *s) { return strtol(s, (char**)0, 10); }
/**
* @brief Convert a string to @c double (freestanding stub — integer only).
*
* Parses an integer with @c strtol() and casts to @c double. Fractional
* parts after a decimal point are silently discarded. The kernel has no
* floating-point parse infrastructure; this stub satisfies link-time
* references from shared VM code that rarely reaches the @c strtod() call
* path in a kernel context.
*
* @param nptr Null-terminated string to convert.
* @param endptr If non-NULL, receives pointer past the last converted digit
* (set by @c strtol()).
* @return Integral @c double representation of the leading integer in @p nptr.
*/
double strtod(const char *nptr, char **endptr) {
long v = strtol(nptr, endptr, 10);
return (double)v;
}
/* -----------------------------------------------------------------------------
* Minimal errno
* ---------------------------------------------------------------------------*/
/**
* @brief Return a pointer to the kernel's single-thread @c errno variable.
*
* The kernel has no thread-local storage and runs VM code on a single thread,
* so a static @c int is sufficient. Returns the same address on every call.
* Satisfies the glibc @c __errno_location() symbol expected by code that
* accesses @c errno through the macro.
*
* @return Pointer to the static @c errno storage.
*/
int *__errno_location(void) {
static int e = 0;
return &e;
}
/**
* @brief Minimal @c strerror(): no filesystem/locale in the kernel, so this
* just returns a fixed string regardless of @p errnum.
*
* Diagnostic callers only need a non-NULL string to format; the numeric
* errno value itself is the actionable part of the message.
*
* @param errnum Ignored.
* @return Pointer to a static, constant string.
*/
char *strerror(int errnum) {
(void)errnum;
return "error";
}
/* -----------------------------------------------------------------------------
* Minimal stdio stubs
* ---------------------------------------------------------------------------*/
typedef struct { int dummy; } FILE;
FILE *stdin = (FILE*)0;
FILE *stdout = (FILE*)0;
FILE *stderr = (FILE*)0;
/**
* @brief Minimal @c vsnprintf implementation for the freestanding kernel.
*
* Formats @p fmt + @p args into @p buf, writing at most @p n - 1 characters
* and always null-terminating (unless @p n is zero). Supports the following
* format specifiers:
*
* - @c %%s — null-terminated string (@c "(null)" if pointer is @c NULL)
* - @c %%d / @c %%u — signed/unsigned @c int
* - @c %%x — unsigned @c int as lowercase hex
* - @c %%p — @c void* as "0x..." lowercase hex
* - @c %%ld — @c long
* - @c %%lu — @c unsigned long
* - @c %%llu — @c unsigned long long
* - @c %%zu — @c size_t
* - @c %%.*s — length-bounded string (precision from @c int argument)
* - @c %%%% — literal @c %
*
* All other specifiers are silently consumed without output. No width/padding
* flags are supported. Used by @c log_message(), @c printf(), and @c snprintf()
* to avoid any libc dependency.
*
* @param buf Output buffer.
* @param n Buffer capacity including null terminator.
* @param fmt Format string.
* @param args Variadic argument list opened by the caller.
* @return Number of characters written (not counting the null terminator).
*/
static int kvsnprintf(char *buf, size_t n, const char *fmt, va_list args) {
size_t used = 0;
const char *p = fmt;
if (n == 0) return 0;
while (*p && used + 1 < n) {
if (*p == '%' && *(p + 1)) {
p++;
/* Handle %.*s (precision string) */
if (*p == '.' && *(p + 1) == '*' && *(p + 2) == 's') {
int precision = va_arg(args, int);
const char *s = va_arg(args, const char*);
if (!s) s = "(null)";
int count = 0;
while (*s && count < precision && used + 1 < n) {
buf[used++] = *s++;
count++;
}
p += 3; /* skip ".*s" */
continue;
}
/* Handle %zu (size_t) */
if (*p == 'z' && *(p + 1) == 'u') {
size_t v = va_arg(args, size_t);
char tmp[32]; int i = 0;
do { tmp[i++] = (char)('0' + (v % 10)); v /= 10; } while (v && i < (int)sizeof(tmp));
while (i-- && used + 1 < n) buf[used++] = tmp[i];
p += 2; /* skip "zu" */
continue;
}
/* Handle %ld (long) */
if (*p == 'l' && *(p + 1) == 'd') {
long v = va_arg(args, long);
char tmp[32]; int neg = 0, i = 0;
if (v < 0) { neg = 1; v = -v; }
do { tmp[i++] = (char)('0' + (v % 10)); v /= 10; } while (v && i < (int)sizeof(tmp));
if (neg && i < (int)sizeof(tmp)) tmp[i++] = '-';
while (i-- && used + 1 < n) buf[used++] = tmp[i];
p += 2; /* skip "ld" */
continue;
}
/* Handle %llu (unsigned long long — unambiguous 64-bit on all arches) */
if (*p == 'l' && *(p + 1) == 'l' && *(p + 2) == 'u') {
unsigned long long v = va_arg(args, unsigned long long);
char tmp[32]; int i = 0;
do { tmp[i++] = (char)('0' + (v % 10)); v /= 10; } while (v && i < (int)sizeof(tmp));
while (i-- && used + 1 < n) buf[used++] = tmp[i];
p += 3; /* skip "llu" */
continue;
}
/* Handle %lu (unsigned long) */
if (*p == 'l' && *(p + 1) == 'u') {
unsigned long v = va_arg(args, unsigned long);
char tmp[32]; int i = 0;
do { tmp[i++] = (char)('0' + (v % 10)); v /= 10; } while (v && i < (int)sizeof(tmp));
while (i-- && used + 1 < n) buf[used++] = tmp[i];
p += 2; /* skip "lu" */
continue;
}
if (*p == 's') {
const char *s = va_arg(args, const char*);
if (!s) s = "(null)";
while (*s && used + 1 < n) buf[used++] = *s++;
} else if (*p == 'd' || *p == 'u') {
long v = (*p == 'd') ? va_arg(args, int) : (long)va_arg(args, unsigned int);
char tmp[32]; int neg = 0, i = 0;
if (*p == 'd' && v < 0) { neg = 1; v = -v; }
do { tmp[i++] = (char)('0' + (v % 10)); v /= 10; } while (v && i < (int)sizeof(tmp));
if (neg && i < (int)sizeof(tmp)) tmp[i++] = '-';
while (i-- && used + 1 < n) buf[used++] = tmp[i];
} else if (*p == 'x' || *p == 'p') {
uint64_t v = (*p == 'p') ? (uint64_t)(uintptr_t)va_arg(args, void*) : (uint64_t)va_arg(args, unsigned int);
const char *hex = "0123456789abcdef";
char tmp[32]; int i = 0;
do { tmp[i++] = hex[v & 0xF]; v >>= 4; } while (v && i < (int)sizeof(tmp));
if (*p == 'p' && used + 2 < n) { buf[used++] = '0'; buf[used++] = 'x'; }
while (i-- && used + 1 < n) buf[used++] = tmp[i];
} else if (*p == '%') {
buf[used++] = '%';
}
p++;
continue;
}
buf[used++] = *p++;
}
buf[used] = '\0';
return (int)used;
}
/**
* @brief Kernel @c snprintf() stub backed by @c kvsnprintf().
*
* @param buf Output buffer.
* @param n Buffer capacity including null terminator.
* @param fmt Format string.
* @param ... Format arguments.
* @return Number of characters written (not counting null terminator).
*/
int snprintf(char *buf, size_t n, const char *fmt, ...) {
va_list args; va_start(args, fmt);
int r = kvsnprintf(buf, n, fmt, args);
va_end(args);
return r;
}
/**
* @brief Kernel @c printf() stub: format and emit to the console.
*
* Formats into a 256-byte stack buffer via @c kvsnprintf() and prints with
* @c console_println(). Output exceeding 255 characters is silently truncated.
*
* @param fmt Format string.
* @param ... Format arguments.
* @return Number of characters in the formatted output.
*/
int printf(const char *fmt, ...) {
char buf[256];
va_list args; va_start(args, fmt);
kvsnprintf(buf, sizeof(buf), fmt, args);
va_end(args);
console_println(buf);
return (int)strlen(buf);
}
/**
* @brief Kernel @c fprintf() stub: ignores stream, formats and emits to console.
*
* The kernel has no real file streams; @p stream is discarded. Output goes to
* the kernel console via @c console_println(). Truncated at 255 characters.
*
* @param stream Ignored.
* @param fmt Format string.
* @param ... Format arguments.
* @return Number of characters in the formatted output.
*/
int fprintf(FILE *stream, const char *fmt, ...) {
(void)stream;
char buf[256];
va_list args; va_start(args, fmt);
kvsnprintf(buf, sizeof(buf), fmt, args);
va_end(args);
console_println(buf);
return (int)strlen(buf);
}
/**
* @brief Kernel @c vfprintf() stub: ignores stream, formats and emits to console.
*
* Same as @c fprintf() but accepts a pre-started @c va_list. Used by code
* that wraps printf-family functions with @c va_list propagation.
*
* @param stream Ignored.
* @param fmt Format string.
* @param args Variadic argument list (caller must have called @c va_start()).
* @return Number of characters in the formatted output.
*/
int vfprintf(FILE *stream, const char *fmt, va_list args) {
(void)stream;
char buf[256];
kvsnprintf(buf, sizeof(buf), fmt, args);
console_println(buf);
return (int)strlen(buf);
}
/** @brief Kernel @c puts(): print @p s + newline to the console; returns 0. */
int puts(const char *s) { console_println(s ? s : ""); return 0; }
/** @brief Kernel @c putchar(): emit one character to the console; returns @p c. */
int putchar(int c) { console_putc((char)c); return c; }
/** @brief Kernel @c putc(): ignores stream, same as @c putchar(). GCC's -O2
* folds @c putchar(c) / @c fputc(c, stdout) call sites into @c putc(c, stdout)
* (FABRIC.md item 4.5d) -- this symbol was never needed at -O0 because that
* fold pass is inactive there. */
int putc(int c, FILE *stream) { (void)stream; return putchar(c); }
/** @brief Kernel @c fflush(): no-op (console writes are synchronous); returns 0. */
int fflush(FILE *stream) { (void)stream; return 0; }
/** @brief Kernel @c fputs(): ignores stream, prints @p s + newline to console. */
int fputs(const char *s, FILE *stream) { (void)stream; console_println(s ? s : ""); return 0; }
/** @brief Kernel @c fopen(): always returns @c NULL — no filesystem in kernel. */
FILE *fopen(const char *path, const char *mode) { (void)path; (void)mode; return NULL; }
/** @brief Kernel @c fclose(): no-op; returns 0. */
int fclose(FILE *f) { (void)f; return 0; }
/** @brief Kernel @c fread(): always returns 0 — no filesystem in kernel. */
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream) { (void)ptr; (void)size; (void)nmemb; (void)stream; return 0; }
/** @brief Kernel @c fwrite(): always returns 0 — no filesystem in kernel. */
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream) { (void)ptr; (void)size; (void)nmemb; (void)stream; return 0; }
/** @brief Kernel @c fseek(): always returns -1 — no filesystem in kernel. */
int fseek(FILE *stream, long offset, int whence) { (void)stream; (void)offset; (void)whence; return -1; }
/** @brief Kernel @c ftell(): always returns 0 — no filesystem in kernel. */
long ftell(FILE *stream) { (void)stream; return 0; }
/** @brief Kernel @c rewind(): no-op — no filesystem in kernel. */
void rewind(FILE *stream) { (void)stream; }
/** @brief Kernel @c fscanf(): always returns -1 — no filesystem in kernel. */
int fscanf(FILE *stream, const char *fmt, ...) { (void)stream; (void)fmt; return -1; }
/** @brief Kernel @c sscanf(): always returns -1 — not implemented in shim. */
int sscanf(const char *str, const char *fmt, ...) { (void)str; (void)fmt; return -1; }
/* fgets()/getchar()'s real target: whichever VM the console is currently
* addressing (Tripod's USE redirect), or Mama when nothing is redirected --
* the same fallback sk_repl_run()/sk_repl_step() themselves use, since
* neither fgets() nor getchar() has a VM* of its own to work with (unlike
* a FORTH primitive, which always does). */
static VM *shim_console_vm(void) {
VM *active = sk_repl_get_active_vm();
return active ? active : (VM *)sk_get_mama_vm();
}
/** @brief Kernel @c fgets(): real body -- QUERY/EXPECT's underlying line
* read, routed through sk_console_readline() (the same echo/backspace
* line editor the REPL's own prompt uses). @p stream is ignored: the
* kernel has exactly one input source, the attached console, regardless
* of which stdio handle a caller passes. Returns NULL only if @p s is
* NULL or @p size is non-positive, matching glibc's own fgets() contract;
* an empty line (bare Enter) still returns @p s with @p s[0] == '\0',
* same as glibc. */
char *fgets(char *s, int size, FILE *stream) {
(void)stream;
if (!s || size <= 0) return NULL;
sk_console_readline(s, size, shim_console_vm(), 0);
return s;
}
/** @brief Kernel @c fputc(): ignores stream; emits @p c to kernel console. */
int fputc(int c, FILE *stream) { (void)stream; console_putc((char)c); return c; }
/** @brief Kernel @c fgetc(): real body -- same one input source as
* @c getchar(), @p stream ignored (see @c fgets() above). Forward-declared
* here since @c getchar() itself isn't defined until further down this
* file and shim.c has no shared stdio.h to declare it earlier (its own
* @c FILE typedef, above, would collide with freestanding/stdio.h's). */
int getchar(void);
int fgetc(FILE *stream) { (void)stream; return getchar(); }
/**
* @brief Kernel @c __isoc99_sscanf() stub (ISO C99 internal sscanf symbol).
*
* Some libc builds emit @c __isoc99_sscanf rather than @c sscanf for C99
* format strings. This stub satisfies the link-time reference; always
* returns 0 (no items matched).
*
* @param str Ignored.
* @param fmt Ignored.
* @return 0 always.
*/
int __isoc99_sscanf(const char *str, const char *fmt, ...) {
(void)str; (void)fmt; return 0;
}
/**
* @brief Kernel @c __isoc99_fscanf() stub (ISO C99 internal fscanf symbol).
*
* Satisfies link-time references from shared code compiled with glibc headers.
* Always returns 0.
*
* @param stream Ignored.
* @param fmt Ignored.
* @return 0 always.
*/
int __isoc99_fscanf(void *stream, const char *fmt, ...) {
(void)stream; (void)fmt; return 0;
}
/**
* @brief Kernel @c signal() stub (SysV ABI; always returns @c NULL).
*
* Signal handling is not meaningful in the freestanding kernel environment.
* Returns @c NULL (SIG_DFL equivalent) to satisfy link-time references.
*
* @param sig Signal number (ignored).
* @param handler Handler pointer (ignored).
* @return @c NULL always.
*/
void * __sysv_signal(int sig, void *handler) {
(void)sig; (void)handler; return NULL;
}
/**
* @brief Kernel @c raise() stub: silently discards signal.
*
* No signal mechanism exists in the freestanding kernel. Returns 0 to
* indicate success so callers do not enter error paths.
*
* @param sig Signal number (ignored).
* @return 0 always.
*/
int raise(int sig) {
(void)sig; return 0;
}
static const unsigned short ctype_table[257] = {0};
/**
* @brief Return a pointer to the kernel's stub @c ctype locale table.
*
* The glibc @c __ctype_b_loc() function returns a pointer to the thread-local
* ctype classification table used by @c isalpha(), @c isdigit(), etc.
* The kernel shim provides a zeroed 257-entry table (all characters classify
* as unknown/false) to satisfy link-time references from libc-aware code.
* Callers that rely on correct ctype classification must not be used in the
* kernel build.
*
* @return Pointer to a static pointer to the zeroed ctype table.
*/
const unsigned short ** __ctype_b_loc(void) {
static const unsigned short *p = &ctype_table[1];
return &p;
}
/** @brief Kernel @c getchar(): real body -- KEY's underlying single-key
* read, routed through sk_console_getkey() (blocks with heartbeat/idle
* servicing, no echo). See fgets()'s own doc comment on shim_console_vm(). */
int getchar(void) { return sk_console_getkey(shim_console_vm()); }
/** @brief Kernel @c getc(): ignores stream, same as @c getchar(). GCC's -O2
* folds @c getchar() call sites into @c getc(stdin) (FABRIC.md item 4.5d) --
* this symbol was never needed at -O0 because that fold pass is inactive
* there. */
int getc(FILE *stream) { (void)stream; return getchar(); }
/** @brief Kernel @c sf_terminal_ready(): real body of the standard
* dictionary's ?TERMINAL word (platform_io.h) -- non-blocking peek via
* sk_console_key_available(), which buffers any found byte so a
* following KEY/getchar() still returns it. */
int sf_terminal_ready(void) { return sk_console_key_available(); }
/* -----------------------------------------------------------------------------
* Misc platform stubs
* ---------------------------------------------------------------------------*/
typedef int pid_t;
/** @brief Kernel @c sched_getscheduler() stub; always returns 0 (SCHED_OTHER). */
int sched_getscheduler(pid_t pid) { (void)pid; return 0; }
/** @brief Kernel @c sched_getparam() stub; always returns 0 (success, no data). */
int sched_getparam(pid_t pid, void *param) { (void)pid; (void)param; return 0; }
/** @brief Kernel @c sched_rr_get_interval() stub; always returns 0. */
int sched_rr_get_interval(pid_t pid, void *ts) { (void)pid; (void)ts; return 0; }
/** @brief Kernel @c sysconf() stub; always returns 0 (value not available). */
long sysconf(int name) { (void)name; return 0; }
/* -----------------------------------------------------------------------------
* qsort (simple bubble sort)
* ---------------------------------------------------------------------------*/
/**
* @brief Sort an array in place (freestanding @c qsort — O(n²) bubble sort).
*
* Implements the standard @c qsort() interface with a simple bubble sort.
* Performance is O(n²) and unsuitable for large arrays, but the kernel build
* only calls @c qsort() in the diagnostics path (@c ALL-HEATS) on ≤1024
* entries, where the cost is acceptable.
*
* No-op if @p base is @c NULL, @p nmemb < 2, @p size is 0, or @p compar
* is @c NULL.
*
* @param base Pointer to the first element of the array.
* @param nmemb Number of elements in the array.
* @param size Size in bytes of each element.
* @param compar Comparison function: returns < 0, 0, or > 0.
*/
void qsort(void *base, size_t nmemb, size_t size, int (*compar)(const void *, const void *)) {
if (!base || nmemb < 2 || size == 0 || !compar) return;
unsigned char *b = base;
for (size_t i = 0; i < nmemb - 1; i++) {
for (size_t j = 0; j < nmemb - i - 1; j++) {
unsigned char *x = b + j * size;
unsigned char *y = x + size;
if (compar(x, y) > 0) {
for (size_t k = 0; k < size; k++) {
unsigned char tmp = x[k];
x[k] = y[k];
y[k] = tmp;
}
}
}
}
}
#endif /* __STARKERNEL__ */