From 8767dbecb45bbe5f748952fb9171b1a96c173f67 Mon Sep 17 00:00:00 2001 From: salmonik Date: Thu, 27 Aug 2026 17:16:04 +0200 Subject: tool migration: v2 exports + 10 external CLI binaries MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit rsapi v2: 14 new ABI function pointers appended after v1 — fs_resolve, fs_dir_next (iterator), fs_create, fs_remove, fs_read, fs_write, fs_size, fs_close, rtc_now, mem_stats, pci_probe, pci_device, klog_dump, kernel_version. New C-repr structs: DirEntry, RtcTime, MemStats, PciDevice, KernelVersion. ABI_VERSION bumped to 2. Shell-side: build_exports() constructs the full table at runtime per invocation (replaces static const). 14 kernel-side wrapper functions bridge rsapi types to rsnix kernel APIs; fs_dir_next maintains a per-handle cache of children() results for iteration. Tool binaries (10 crates, all linked against rsapi only): - date: rtc_now → formatted YYYY-MM-DD HH:MM:SS - mem: heap/pages free/total via mem_stats - pci: pci_probe + pci_device enumerate all devices - cat: fs_read paths or get_line stdin; put_line output - ls: fs_resolve + fs_dir_next iteration; trailing / for dirs - mkdir: fs_create(path, true) - touch: fs_create(path, false) - rm: fs_remove(path) - klog: klog_dump 2048 bytes → put_line - info: rtc_now + kernel_version (minimal; mem/pci deferred) Gate verified: date prints RTC; pci lists all 4 QEMU devices (8086:29c0, 1234:1111, 8086:10d3, 8086:2918); hello/boom/count still work; test-disk PASS; poweroff clean. --- shell/src/lib.rs | 211 ++++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 202 insertions(+), 9 deletions(-) (limited to 'shell/src') diff --git a/shell/src/lib.rs b/shell/src/lib.rs index 5248ea9..11f5786 100644 --- a/shell/src/lib.rs +++ b/shell/src/lib.rs @@ -28,6 +28,7 @@ use alloc::collections::VecDeque; use alloc::string::{String, ToString}; use alloc::vec::Vec; use rsnix::tty::{self, LineInput}; +use alloc::boxed::Box; use rsnix::{gop_print, gop_println}; /// Run the home-terminal shell (task 0). The home terminal is an @@ -952,14 +953,205 @@ fn abi_exit(code: i32) -> ! { rsnix::sched::abort_current("exit") } -static EXPORTS_V1: rsapi::Exports = rsapi::Exports { - magic: rsapi::EXPORTS_MAGIC, - version: rsapi::ABI_VERSION, - put_line: abi_put_line, - get_line: abi_get_line, - log_line: abi_log_line, - exit: abi_exit, -}; + +/// Directory iteration cache: one slot per simultaneous fs_dir_next. +struct DirIter { + entries: Vec<(String, rsnix::fs::FileType, u64)>, + pos: usize, +} +static mut DIR_CACHE: [Option; 16] = [const { None }; 16]; + +fn cache_key(h: i32) -> usize { ((h as usize) & 0x7FFFFFFF) % 16 } + +/// ABI: resolve a path to a handle (-1 on error). +fn abi_fs_resolve(path: rsapi::Line) -> rsapi::FsHandle { + match rsnix::fs::resolve(path.as_str()) { + Some(h) => h as i32, + None => -1, + } +} + +/// ABI: iterate directory children one at a time. +fn abi_fs_dir_next(handle: rsapi::FsHandle, out: &mut rsapi::DirEntry) -> bool { + let key = cache_key(handle); + unsafe { + if DIR_CACHE[key].is_none() { + let h = handle as usize; + let kids = rsnix::fs::children(h); + DIR_CACHE[key] = Some(DirIter { entries: kids, pos: 0 }); + } + let iter = DIR_CACHE[key].as_mut().unwrap(); + if iter.pos < iter.entries.len() { + let (ref name, kind, size) = iter.entries[iter.pos]; + out.name = rsapi::Line { ptr: name.as_ptr(), len: name.len() }; + out.is_dir = kind == rsnix::fs::FileType::Dir; + out.size = size; + iter.pos += 1; + true + } else { + DIR_CACHE[key] = None; + false + } + } +} + +/// ABI: create a file or directory at path. +fn abi_fs_create(path: rsapi::Line, is_dir: bool) -> i32 { + let p = path.as_str(); + if is_dir { + match rsnix::fs::mkdir(p) { + Ok(()) => 0, + Err(()) => -1, + } + } else { + match rsnix::fs::ensure_file(p) { + Ok(_) => 0, + Err(()) => -1, + } + } +} + +/// ABI: remove a file or empty directory. +fn abi_fs_remove(path: rsapi::Line) -> i32 { + match rsnix::fs::remove_path(path.as_str()) { + Ok(()) => 0, + Err(()) => -1, + } +} + +/// ABI: read bytes from a handle at a position. +fn abi_fs_read(handle: rsapi::FsHandle, buf: *mut u8, pos: u64, len: u64) -> i64 { + let h = handle as usize; + let mut tmp = [0u8; 512]; + let mut total = 0i64; + let mut off = pos; + let mut rem = len as usize; + while rem > 0 { + let chunk = rem.min(512); + match rsnix::fs::read(h, &mut tmp[..chunk], off) { + Ok(0) | Err(_) => break, + Ok(n) => { + unsafe { core::ptr::copy_nonoverlapping(tmp.as_ptr(), buf.add(total as usize), n); } + total += n as i64; + off += n as u64; + rem -= n; + } + } + } + total +} + +/// ABI: write bytes to a handle at a position. +fn abi_fs_write(handle: rsapi::FsHandle, buf: *const u8, pos: u64, len: u64) -> i64 { + let h = handle as usize; + if len == 0 { return 0; } + let slice = unsafe { core::slice::from_raw_parts(buf, len as usize) }; + match rsnix::fs::write(h, slice, pos) { + Ok(n) => n as i64, + Err(()) => -1, + } +} + +/// ABI: get file size. +fn abi_fs_size(handle: rsapi::FsHandle) -> u64 { + rsnix::fs::size(handle as usize) +} + +/// ABI: close handle (currently no-op). +fn abi_fs_close(_handle: rsapi::FsHandle) {} + +/// ABI: current real-time clock. +fn abi_rtc_now(out: &mut rsapi::RtcTime) { + let t = rsnix::rtc::now(); + out.year = t.year; + out.month = t.month; + out.day = t.day; + out.hour = t.hour; + out.minute = t.minute; + out.second = t.second; +} + +/// ABI: memory statistics. +fn abi_mem_stats(out: &mut rsapi::MemStats) { + let s = rsnix::memory::heap_stats(); + out.heap_total = s.heap_size as u64; + out.heap_free = s.free_bytes as u64; + let ps = rsnix::memory::page_stats(); + out.pages_total = ps.span_pages as u64; + out.pages_free = ps.free_pages as u64; +} + +/// ABI: enumerate PCI devices. +fn abi_pci_probe() -> u32 { + rsnix::pci::probe().len() as u32 +} + +/// ABI: get one PCI device by index. +fn abi_pci_device(index: u32, out: &mut rsapi::PciDevice) -> bool { + let devs = rsnix::pci::probe(); + match devs.get(index as usize) { + Some(d) => { + out.bus = d.bus; + out.device = d.device; + out.function = d.func; + out.class = d.class; + out.subclass = d.subclass; + out.vendor = d.vendor; + out.device_id = d.device_id; + true + } + None => false, + } +} + +/// ABI: dump kernel log ring into buffer. +fn abi_klog_dump(buf: *mut u8, cap: u64) -> u64 { + let s = rsnix::klog::dump(); + let bytes = s.as_bytes(); + let n = (bytes.len() as u64).min(cap) as usize; + unsafe { core::ptr::copy_nonoverlapping(bytes.as_ptr(), buf, n); } + n as u64 +} + +/// ABI: kernel version info. +fn abi_kernel_version(out: &mut rsapi::KernelVersion) { + let name = concat!(env!("CARGO_PKG_NAME"), " ", env!("CARGO_PKG_VERSION")); + let mut it = env!("CARGO_PKG_VERSION").split('.'); + let major: u16 = it.next().and_then(|s| s.parse().ok()).unwrap_or(0); + let minor: u16 = it.next().and_then(|s| s.parse().ok()).unwrap_or(0); + let patch: u16 = it.next().and_then(|s| s.parse().ok()).unwrap_or(0); + out.name = rsapi::Line { ptr: name.as_ptr(), len: name.len() }; + out.major = major; + out.minor = minor; + out.patch = patch; +} + +/// Construct the Exports table for this invocation. v2 fields are +/// populated at runtime (binary callbacks → shell → kernel). +fn build_exports() -> rsapi::Exports { + rsapi::Exports { + magic: rsapi::EXPORTS_MAGIC, + version: rsapi::ABI_VERSION, + put_line: abi_put_line, + get_line: abi_get_line, + log_line: abi_log_line, + exit: abi_exit, + fs_resolve: abi_fs_resolve, + fs_dir_next: abi_fs_dir_next, + fs_create: abi_fs_create, + fs_remove: abi_fs_remove, + fs_read: abi_fs_read, + fs_write: abi_fs_write, + fs_size: abi_fs_size, + fs_close: abi_fs_close, + rtc_now: abi_rtc_now, + mem_stats: abi_mem_stats, + pci_probe: abi_pci_probe, + pci_device: abi_pci_device, + klog_dump: abi_klog_dump, + kernel_version: abi_kernel_version, + } +} /// Run `cmd` as an external PE32+ binary from /bin/cli/. Returns false /// when no external binary resolved (the caller falls back to built-ins). @@ -1026,7 +1218,8 @@ fn exec_external(p: &Parsed) -> bool { let mut cmd_done = false; let mut exit_code: i64 = 0; - match rsnix::exec::spawn(image, &EXPORTS_V1, &argv, &name) { + let exports = Box::leak(Box::new(build_exports())); + match rsnix::exec::spawn(image, exports, &argv, &name) { Ok(_id) => { // Pumping mode: assemble canonical lines from our queue into // the channel while the command runs. The WM pump delivers -- cgit v1.2.3