#!/usr/bin/env python3 # genGnoKernel.py — generate wrappers for the GNO kernel toolset. # # The GNO kernel is implemented as Apple IIgs user toolset $03. Each # kernel function is identified by a 16-bit number $XX03 where XX is # the function index and 03 is the toolset. The dispatcher is the # standard IIgs tool dispatcher at $E10000. # # Function list is hard-coded from include/gno/kerntool.h in the GNO # Consortium source (https://github.com/GnoConsortium/gno). The # canonical ORCA-style signature is preserved in a comment. # # Output: runtime/src/gnoKernel.s — asm wrappers callable from our C # ABI (arg0 in A, arg0 i32 in A:X, rest pushed RTL on stack). Each # wrapper re-pushes args in toolbox/Pascal order (high-word first for # 32-bit values), allocates result space, JSLs $E10000, pops the # result back into A:X if non-void. # # Run after editing the function table: # python3 scripts/genGnoKernel.py # # The libc layer in runtime/src/libcGno.c wraps these K* primitives # into POSIX names (fork, exec, wait, etc.). from pathlib import Path OUT_ASM = Path("/home/scott/claude/llvm816/runtime/src/gnoKernel.s") OUT_HEADER = Path("/home/scott/claude/llvm816/runtime/include/gno/kernel.h") # Each entry: (funcId, name, retSize, [argSize, ...]) # Sizes in bytes (2 = i16, 4 = i32 / ptr32). Names follow ORCA's K* prefix. # # Pulled from include/gno/kerntool.h in the GNO Consortium repo. SYSCALLS = [ # ---- Process control ---- (0x0903, "Kgetpid", 2, []), # () -> int (0x0A03, "Kkill", 2, [2, 2, 4]), # (pid, sig, *errno) -> int (0x0B03, "Kfork", 2, [4, 4]), # (*subr, *errno) -> int (0x0903, "Kgetpid_dup", 2, []), # alias kept for sanity check (0x4003, "Kgetppid", 2, [4]), # (*errno) -> int (0x1703, "Kwait", 2, [4, 4]), # (*status, *errno) -> int (0x1D03, "K_execve", 2, [4, 4, 4]), # (*file, *cmdline, *err) -> int (0x1603, "Ksignal", 4, [2, 4, 4]), # (sig, func_ptr, *err) -> sig_t (0x1E03, "Kalarm", 4, [4, 4]), # (seconds, *errno) -> longword (0x4203, "Kalarm10", 4, [4, 4]), # (seconds-tenths, *errno) -> longword (0x2103, "Ksigpause", 2, [4, 4]), # (mask, *errno) -> int (0x1B03, "Ksigsetmask", 4, [4, 4]), # (mask, *errno) -> longword (0x1C03, "Ksigblock", 4, [4, 4]), # (mask, *errno) -> longword # ---- File descriptors ---- (0x2203, "Kdup", 2, [2, 4]), # (oldfd, *errno) -> int (0x2303, "Kdup2", 2, [2, 2, 4]), # (oldfd, newfd, *errno) -> int (0x2403, "Kpipe", 2, [4, 4]), # (*fildes[2], *errno) -> int (0x2603, "Kioctl", 2, [2, 4, 4, 4]), # (fd, req:ulong, *ptr, *errno) -> int # ---- stat ---- (0x2703, "Kstat", 2, [4, 4, 4]), # (*path, *sbuf, *errno) -> int (0x2803, "Kfstat", 2, [2, 4, 4]), # (fd, *sbuf, *errno) -> int (0x2903, "Klstat", 2, [4, 4, 4]), # (*path, *sbuf, *errno) -> int # ---- IDs ---- (0x2A03, "Kgetuid", 2, [4]), # (*errno) -> int (0x2B03, "Kgetgid", 2, [4]), # (*errno) -> int (0x2C03, "Kgeteuid", 2, [4]), # (*errno) -> int (0x2D03, "Kgetegid", 2, [4]), # (*errno) -> int (0x2E03, "Ksetuid", 2, [2, 4]), # (uid, *errno) -> int (0x2F03, "Ksetgid", 2, [2, 4]), # (gid, *errno) -> int # ---- Process groups / TTY ---- (0x1803, "Ktcnewpgrp", 2, [2, 4]), # (fdtty, *errno) -> int (0x1903, "Ksettpgrp", 2, [2, 4]), # (fdtty, *errno) -> int (0x1A03, "Ktctpgrp", 2, [2, 2, 4]), # (fdtty, pid, *err) -> int (0x2503, "K_getpgrp", 2, [2, 4]), # (pid, *errno) -> pid_t (0x3403, "Ksetpgrp", 2, [2, 2, 4]), # (pid, pgrp, *errno) -> int # ---- Kernel VM (process inspection) ---- (0x1103, "Kkvm_open", 2, [4]), # (*errno) -> int (0x1203, "Kkvm_close", 2, [4, 4]), # (kvmt, *errno) -> int # ---- Misc ---- (0x3503, "Ktimes", 4, [4, 4]), # (*tms, *errno) -> clock_t # ---- Quit setup ---- (0x4103, "KSetGNOQuitRec", 0, [2, 4, 2, 4]), # (pCount, GSStringPtr, flags, *err) -> void ] DISPATCHER = 0xE10000 def emitWrapper(funcId, name, retSize, argSizes): """Emit one .s function body for the given GNO kernel call.""" lines = [] argTypes = [f"i{size*8}" for size in argSizes] lines.append(f"; {name}({', '.join(argTypes) or 'void'}) -> " f"{'void' if retSize == 0 else f'i{retSize*8}'}") lines.append(f"; toolset 3, func 0x{funcId:04X}") lines.append(f"\t.section .text.{name},\"ax\"") lines.append(f"\t.globl {name}") lines.append(f"{name}:") scratchDP = 0xE0 firstArgIs32 = bool(argSizes) and argSizes[0] == 4 # Stash arg0 if any args. arg0 in A (i16) or A:X (i32). if argSizes: lines.append(f"\t; --- stash arg0 (in A{'/X' if firstArgIs32 else ''}) ---") lines.append(f"\tsta 0x{scratchDP:02X}") if firstArgIs32: lines.append(f"\tstx 0x{scratchDP + 2:02X}") # Result space (rounded up to whole words). resultWords = (retSize + 1) // 2 if resultWords > 0: lines.append(f"\t; --- result space ({retSize} bytes) ---") for _ in range(resultWords): lines.append("\tpea 0") # Push args in Pascal order (high-word first for 32-bit). pushedBytes = resultWords * 2 # arg0. if argSizes: lines.append("\t; --- arg0 ---") if firstArgIs32: lines.append(f"\tlda 0x{scratchDP + 2:02X}") lines.append("\tpha") pushedBytes += 2 lines.append(f"\tlda 0x{scratchDP:02X}") lines.append("\tpha") pushedBytes += 2 # arg1+. stackArgOffset = 4 # caller's first stack arg (after 3-byte JSL ret addr) for i, size in enumerate(argSizes[1:], start=1): lines.append(f"\t; --- arg{i} ({size}B) ---") if size <= 2: lines.append(f"\tlda {stackArgOffset + pushedBytes}, s") lines.append("\tpha") pushedBytes += 2 stackArgOffset += 2 elif size == 4: # High word first. Caller pushed low-word-first, so HI is at # caller offset +2. After PHA, the LO becomes accessible at # the SAME stack-rel offset (PHA shifted SP by 2). lines.append(f"\tlda {stackArgOffset + pushedBytes + 2}, s") lines.append("\tpha") pushedBytes += 2 lines.append(f"\tlda {stackArgOffset + pushedBytes}, s") lines.append("\tpha") pushedBytes += 2 stackArgOffset += 4 else: raise RuntimeError(f"unhandled arg size {size} in {name}") # Dispatch. lines.append(f"\tldx #0x{funcId:04X}") lines.append(f"\tjsl 0x{DISPATCHER:x}") # Pop result. if resultWords > 0: lines.append("\tpla ; result lo -> A") if resultWords == 2: lines.append("\tplx ; result hi -> X") lines.append("\trtl") lines.append("") return lines def emitHeader(): """Generate a C header with extern decls for use by libc.""" hLines = [ "// AUTOGENERATED by scripts/genGnoKernel.py — DO NOT EDIT.", "// GNO kernel toolset $03 wrappers, callable from C.", "// Convention: each K* returns the kernel result (or -1 on error);", "// the last argument is `int *errno` and gets the kernel's errno.", "//", "// These are LOW-LEVEL primitives — libc routines in libcGno.c", "// wrap them into POSIX-named fork/exec/wait/etc.", "#ifndef GNO_KERNEL_H", "#define GNO_KERNEL_H", "", "#include ", "", ] for funcId, name, retSize, argSizes in SYSCALLS: # Skip the sanity-check duplicate. if name.endswith("_dup"): continue retC = "void" if retSize == 0 else ( "int" if retSize == 2 else "unsigned long") argC = [] for i, size in enumerate(argSizes): t = "int" if size == 2 else "void *" argC.append(f"{t} a{i}") hLines.append(f"extern {retC} {name}({', '.join(argC) or 'void'});" f" // 0x{funcId:04X}") hLines.append("") hLines.append("#endif") hLines.append("") return hLines def main(): asmLines = [ "; AUTOGENERATED by scripts/genGnoKernel.py — DO NOT EDIT by hand.", ";", "; GNO/ME kernel toolset $03 wrappers.", "; Dispatcher: JSL $E10000 with LDX #funcId.", ";", "; C ABI: arg0 (i16) in A, arg0 (i32) in A:X, arg1+ on stack", "; (caller-pushed, lo-word first for 32-bit values).", "; Each wrapper re-pushes args in Pascal order (high-word first", "; for 32-bit), allocates result space, dispatches, pops result.", "", "\t.text", "", ] seen = set() for funcId, name, retSize, argSizes in SYSCALLS: if name in seen: continue seen.add(name) asmLines.extend(emitWrapper(funcId, name, retSize, argSizes)) OUT_ASM.parent.mkdir(parents=True, exist_ok=True) OUT_ASM.write_text("\n".join(asmLines)) print(f"wrote {OUT_ASM} ({len(asmLines)} lines, " f"{len(seen) - 1} wrappers)") OUT_HEADER.parent.mkdir(parents=True, exist_ok=True) OUT_HEADER.write_text("\n".join(emitHeader())) print(f"wrote {OUT_HEADER}") if __name__ == "__main__": main()