hachem.wtf
aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--boot/boot.zig38
-rw-r--r--boot/elf.zig1
-rw-r--r--boot/heap.zig100
-rw-r--r--boot/sched.zig41
-rw-r--r--boot/user.zig18
-rw-r--r--bridge/bridge.zig539
-rw-r--r--bridge/convert.zig18
-rw-r--r--bridge/hosts.zig380
-rw-r--r--bridge/serial.zig141
-rw-r--r--build.zig3
-rw-r--r--kernel/font.rv1
-rw-r--r--kernel/fs.rv355
-rw-r--r--kernel/input.rv25
-rw-r--r--kernel/proc.rv23
-rw-r--r--kernel/seed.rv9
-rw-r--r--kernel/shell.rv110
-rw-r--r--kernel/vfs.rv57
-rw-r--r--kernel/vmm.rv1
-rw-r--r--user/calc.c6
-rw-r--r--user/hexview.c32
-rw-r--r--user/save.c45
-rw-r--r--user/user.h17
22 files changed, 1254 insertions, 706 deletions
diff --git a/boot/boot.zig b/boot/boot.zig
index 5a3c429..fff8a6b 100644
--- a/boot/boot.zig
+++ b/boot/boot.zig
@@ -39,7 +39,7 @@ pub const panic = std.debug.FullPanic(struct {
// console and shit
pub const std_options_debug_io: std.Io = bridge.debug_io;
-// check `linker.ld`
+// note: check `linker.ld`
export var base_revision: limine.BaseRevision linksection(".limine_requests") = limine.BaseRevision.init(3);
export var requests_start: limine.RequestsStartMarker linksection(".limine_requests_start") = .{};
export var requests_end: limine.RequestsEndMarker linksection(".limine_requests_end") = .{};
@@ -70,33 +70,42 @@ fn kSerialNext() i64 {
// read only kernel state exposed to revo so it can present it (mem/uptime/ps and
// the memory map dump live in revo now)
var g_memmap: ?*const limine.MemoryMapResponse = null;
+
fn kMemmapCount() u64 {
const mm = g_memmap orelse return 0;
return mm.entry_count;
}
+
fn memmapEntry(i: u64) ?*const limine.MemoryMapEntry {
const mm = g_memmap orelse return null;
if (i >= mm.entry_count) return null;
return mm.entries.?[i];
}
+
fn kMemmapBase(i: u64) u64 {
return if (memmapEntry(i)) |e| e.base else 0;
}
+
fn kMemmapLen(i: u64) u64 {
return if (memmapEntry(i)) |e| e.length else 0;
}
+
fn kMemmapKind(i: u64) u64 {
return if (memmapEntry(i)) |e| @intFromEnum(e.type) else 0;
}
+
fn kMemFree() u64 {
return pmm.freeBytes();
}
+
fn kMemTotal() u64 {
return pmm.usableBytes();
}
+
fn kUptime() u64 {
return timer.now();
}
+
fn kProcCount() u64 {
return sched.procCount();
}
@@ -104,16 +113,18 @@ fn kProcCount() u64 {
fn kDiskSectors() u64 {
return ata.sectorCount();
}
+
fn kDiskRead(lba: u64, phys: u64) u64 {
const buf: *[ata.SECTOR]u8 = @ptrFromInt(pmm.physToVirt(phys));
return if (ata.read(@truncate(lba), buf)) 1 else 0;
}
+
fn kDiskWrite(lba: u64, phys: u64) u64 {
const buf: *const [ata.SECTOR]u8 = @ptrFromInt(pmm.physToVirt(phys));
return if (ata.write(@truncate(lba), buf)) 1 else 0;
}
-// the embedded programs, copied to frames at boot so revo can load them by name
+// the embedded programs
const Prog = struct { name: []const u8, phys: u64, size: u64 };
var g_progs: [elf.programs.len]Prog = undefined;
@@ -129,13 +140,27 @@ fn loadPrograms() void {
fn kProgCount() u64 {
return g_progs.len;
}
+
fn kProgPhys(i: u64) u64 {
return g_progs[i].phys;
}
+
fn kProgName(i: u64) []const u8 {
return g_progs[i].name;
}
+fn kProgSize(i: u64) u64 {
+ return g_progs[i].size;
+}
+
+fn kTaskMax() u64 {
+ return sched.taskMax();
+}
+
+fn kTaskState(i: u64) u64 {
+ return sched.taskState(i);
+}
+
// entry(_start)
export fn _start() callconv(.c) noreturn {
serial.init();
@@ -186,6 +211,9 @@ export fn _start() callconv(.c) noreturn {
.prog_count = &kProgCount,
.prog_phys = &kProgPhys,
.prog_name = &kProgName,
+ .prog_size = &kProgSize,
+ .task_max = &kTaskMax,
+ .task_state = &kTaskState,
.disk_sectors = &kDiskSectors,
.disk_read = &kDiskRead,
.disk_write = &kDiskWrite,
@@ -252,9 +280,9 @@ fn vmmSelfTest() void {
serial.write("vmm test: switched back OK\r\n");
}
-// sleep until an interrupt, then drain the keyboard / tick and repeat. this is
-// scheduler task 0. the cli/pop/sti hlt dance closes the lost wakeup race (a key
-// arriving between "queue empty" and hlt would otherwise sit until the next one)
+// 1- sleep until an interrupt
+// 2- then drain the keyboard / tick
+// 3- repeat
fn eventLoop() noreturn {
var last_ticks: u64 = 0;
while (true) {
diff --git a/boot/elf.zig b/boot/elf.zig
index 51dc107..2070db6 100644
--- a/boot/elf.zig
+++ b/boot/elf.zig
@@ -3,4 +3,5 @@ pub const programs = [_]Program{
.{ .name = "hexview", .bytes = @embedFile("user_hexview") },
.{ .name = "calc", .bytes = @embedFile("user_calc") },
.{ .name = "primes", .bytes = @embedFile("user_primes") },
+ .{ .name = "save", .bytes = @embedFile("user_save") },
};
diff --git a/boot/heap.zig b/boot/heap.zig
index a845f60..badf506 100644
--- a/boot/heap.zig
+++ b/boot/heap.zig
@@ -1,34 +1,98 @@
-// this just grabs a contiguous chunk of frams from the pmm and then
-// hands out a bump allocator, this just gives std.mem.allocator which
-// well pass to the revo vm later. fixedbufferallocator never reclaims
-// freed space unless you free in reverse order which is ehhhhh, fine ig
-// for bootstrapping
-//
-// todo: swap fixed buffer allocator for a free list allocator
+// freelist allocator type shit
const std = @import("std");
const pmm = @import("pmm.zig");
const serial = @import("serial.zig");
-// 32 mib
-// note: bump if the vm needs more
-const HEAP_PAGES: usize = 8192;
+const HEAP_PAGES: usize = 16384; // 64 mib
+const HEADER: usize = 16; // keeps payloads 16 aligned
+const ALIGN: usize = 16;
+const MIN_BLOCK: usize = 32; // header + a usable scrap, dont split below this
-var fba: std.heap.FixedBufferAllocator = undefined;
+var heap_start: usize = 0;
+var heap_end: usize = 0;
+
+inline fn word(b: usize) *usize {
+ return @ptrFromInt(b);
+}
+inline fn blkSize(b: usize) usize {
+ return word(b).* & ~@as(usize, 0xf);
+}
+inline fn blkUsed(b: usize) bool {
+ return (word(b).* & 1) != 0;
+}
+inline fn setBlk(b: usize, size: usize, used: bool) void {
+ word(b).* = size | @as(usize, if (used) 1 else 0);
+}
+inline fn alignUp(x: usize, a: usize) usize {
+ return (x + a - 1) & ~(a - 1);
+}
pub fn init() void {
const phys = pmm.allocContig(HEAP_PAGES) orelse @panic("heap: no contiguous frames for the heap");
- const virt = pmm.physToVirt(phys);
- const buf: [*]u8 = @ptrFromInt(virt);
- fba = std.heap.FixedBufferAllocator.init(buf[0 .. HEAP_PAGES * pmm.PAGE_SIZE]);
+ heap_start = pmm.physToVirt(phys);
+ const total = HEAP_PAGES * pmm.PAGE_SIZE;
+ heap_end = heap_start + total;
+ setBlk(heap_start, total, false); // start as one big free block
serial.write("heap: ");
- serial.writeDec((HEAP_PAGES * pmm.PAGE_SIZE) / (1024 * 1024));
- serial.write(" MiB at ");
- serial.writeHex(virt);
+ serial.writeDec(total / (1024 * 1024));
+ serial.write(" MiB free-list at ");
+ serial.writeHex(heap_start);
serial.write("\r\n");
}
+fn allocImpl(_: *anyopaque, len: usize, alignment: std.mem.Alignment, _: usize) ?[*]u8 {
+ // we only guarantee 16 byte payload alignment
+ // anything else can like actually go fuck itself
+ if (alignment.toByteUnits() > ALIGN) return null;
+ const need = alignUp(HEADER + len, ALIGN);
+
+ var b = heap_start;
+ while (b < heap_end) {
+ if (!blkUsed(b)) {
+ // lazy coalesce
+ var size = blkSize(b);
+ while (b + size < heap_end and !blkUsed(b + size)) size += blkSize(b + size);
+ setBlk(b, size, false);
+
+ if (size >= need) {
+ if (size >= need + MIN_BLOCK) {
+ setBlk(b, need, true);
+ setBlk(b + need, size - need, false); // the leftover tail
+ } else {
+ setBlk(b, size, true);
+ }
+ return @ptrFromInt(b + HEADER);
+ }
+ }
+ b += blkSize(b);
+ }
+ return null;
+}
+
+fn freeImpl(_: *anyopaque, buf: []u8, _: std.mem.Alignment, _: usize) void {
+ const b = @intFromPtr(buf.ptr) - HEADER;
+ setBlk(b, blkSize(b), false);
+}
+
+fn resizeImpl(_: *anyopaque, buf: []u8, _: std.mem.Alignment, new_len: usize, _: usize) bool {
+ const b = @intFromPtr(buf.ptr) - HEADER;
+ return new_len <= blkSize(b) - HEADER;
+}
+
+fn remapImpl(ctx: *anyopaque, buf: []u8, alignment: std.mem.Alignment, new_len: usize, ra: usize) ?[*]u8 {
+ if (resizeImpl(ctx, buf, alignment, new_len, ra)) return buf.ptr;
+ return null;
+}
+
+const vtable = std.mem.Allocator.VTable{
+ .alloc = allocImpl,
+ .resize = resizeImpl,
+ .remap = remapImpl,
+ .free = freeImpl,
+};
+
pub fn allocator() std.mem.Allocator {
- return fba.allocator();
+ return .{ .ptr = undefined, .vtable = &vtable };
}
diff --git a/boot/sched.zig b/boot/sched.zig
index 0857abe..1842309 100644
--- a/boot/sched.zig
+++ b/boot/sched.zig
@@ -1,8 +1,4 @@
-// tiny preemptive round robin scheduler. a task is a saved frame + cr3 + kernel
-// stack, task 0 is the kernel event loop. the pit tick swaps the interrupted
-// frame for the next ready tasks and iretqs in, which covers ring 0 and ring 3
-// alike. each task has its own kernel stack so preempting one mid syscall (e.g. a
-// read() parked on sti/hlt) doesnt clobber anothers rsp0
+// tiny preemptive shitty round robin scheduler
const std = @import("std");
const Frame = @import("regs.zig").Frame;
@@ -25,7 +21,6 @@ const Task = struct {
var tasks: [MAX_TASKS]Task = @splat(.{});
var kstacks: [MAX_TASKS][KSTACK_SIZE]u8 align(16) = undefined;
var current: usize = 0;
-var spawn_count: u64 = 0;
var blocked_reader: ?usize = null;
var foreground: ?usize = null;
@@ -45,13 +40,13 @@ fn stdinPop() ?u8 {
return byte;
}
-// task 0 = the kernel/event loop, running in whatever cr3 boot set up
+// task 0 = the kernel/event loop running in whatever cr3 boot set up
pub fn init(kernel_cr3: u64) void {
tasks[0] = .{ .cr3 = kernel_cr3, .state = .ready, .kstack_top = 0 };
current = 0;
}
-pub fn spawn(cr3: u64, entry: u64, ustack: u64) void {
+pub fn spawn(cr3: u64, entry: u64, ustack: u64, arg: u64, arglen: u64) void {
var i: usize = 1;
while (i < MAX_TASKS) : (i += 1) {
if (tasks[i].state != .free) continue;
@@ -61,8 +56,8 @@ pub fn spawn(cr3: u64, entry: u64, ustack: u64) void {
f.rflags = 0x202; // reserved bit + if=1, so its preemptible
f.rsp = ustack;
f.ss = 0x23; // user data (0x20) | rpl 3
- f.rdi = spawn_count;
- spawn_count += 1;
+ f.rdi = arg; // _start(arg_ptr, arg_len), the sysv first two args
+ f.rsi = arglen;
tasks[i] = .{
.frame = f,
.cr3 = cr3,
@@ -82,14 +77,24 @@ pub fn procCount() u64 {
return count;
}
+// task table size + per slot state
+// state is 0 free, 1 ready, 2 dead, 3 blocked
+pub fn taskMax() u64 {
+ return MAX_TASKS;
+}
+pub fn taskState(i: u64) u64 {
+ if (i >= MAX_TASKS) return 0;
+ return @intFromEnum(tasks[i].state);
+}
+
var proc_exited_flag: bool = false;
-// mark the running process dead. it then sti+hlts
+// mark the running process dead then sti+hlts
pub fn exitCurrent() void {
if (current != 0) {
tasks[current].state = .dead;
- proc_exited_flag = true; // the kernel task reprints the prompt on seeing this
- if (foreground == current) { // hand keyboard back to the shell
+ proc_exited_flag = true;
+ if (foreground == current) {
foreground = null;
stdin_head = 0;
stdin_tail = 0;
@@ -117,7 +122,7 @@ pub fn blockCurrentOnRead(resume_frame: Frame) void {
pub fn deliverInput(byte: u8) bool {
if (blocked_reader) |task| {
- tasks[task].frame.rax = byte; // read()s return value
+ tasks[task].frame.rax = byte;
tasks[task].state = .ready;
blocked_reader = null;
return true;
@@ -133,11 +138,11 @@ pub fn tick(frame: *Frame) void {
if (bridge.vmBusy()) return;
if (tasks[current].state == .dead) {
- tasks[current].state = .free; // reclaim, dont bother saving its context
+ tasks[current].state = .free;
} else if (tasks[current].state == .blocked) {
- // keep the resume frame saved by blockcurrentonread, dont clobber it you bitch
+ // dont clobber it you bitch
} else {
- tasks[current].frame = frame.*; // freeze the current task
+ tasks[current].frame = frame.*;
}
var next = current;
@@ -150,7 +155,7 @@ pub fn tick(frame: *Frame) void {
break;
}
}
-
+
if (tasks[current].state != .ready and next == current) next = 0;
if (next == current) return;
diff --git a/boot/user.zig b/boot/user.zig
index 5ba6604..9bce9e9 100644
--- a/boot/user.zig
+++ b/boot/user.zig
@@ -8,8 +8,7 @@ const regs = @import("regs.zig");
const bridge = @import("bridge");
const Frame = regs.Frame;
-// scratch for a filename copied out of the caller. one static buffer is fine,
-// syscalls run one at a time (if=0)
+// scratch for a filename copied out of the caller legit one static buffer is fine
var name_buf: [64]u8 = undefined;
fn userName(ptr: u64) []const u8 {
const p: [*]const u8 = @ptrFromInt(ptr);
@@ -18,9 +17,6 @@ fn userName(ptr: u64) []const u8 {
return name_buf[0..i];
}
-// what the syscall stub leaves on the stack: the gp regs it pushed, then the
-// frame the cpu pushed on int 0x80. rax holds the syscall number in, and the
-// handler writes the return value back into it (restored by the stubs pop)
pub const SyscallFrame = extern struct {
r15: u64,
r14: u64,
@@ -49,8 +45,10 @@ pub fn installSyscall() void {
idt.setGate(0x80, @intFromPtr(&syscallStub), 3, 1); // ist1: big stack for vm calls
}
-// save gp regs, hand the frame to the zig handler, restore, iretq. the handler
-// returns via syscallframe.rax (restored by the pop below), exit() never returns
+// 1- save gp regs
+// 2- hand the frame to the zig handler
+// 3- restore
+// 4- iretq
export fn syscallStub() callconv(.naked) void {
asm volatile (regs.PUSH_GPRS ++
\\
@@ -120,6 +118,12 @@ export fn syscallHandler(frame: *SyscallFrame) callconv(.c) void {
const dst: [*]u8 = @ptrFromInt(frame.rdx);
frame.rax = bridge.fsReadInto(name, frame.rsi, dst[0..len]);
},
+ 6 => { // fs_write(name_ptr, buf_ptr, len): returns 1 ok / 0 fail
+ const len = @min(frame.rdx, 64 * 1024);
+ const name = userName(frame.rdi);
+ const src: [*]const u8 = @ptrFromInt(frame.rsi);
+ frame.rax = if (bridge.fsStore(name, src[0..len])) 1 else 0;
+ },
else => serial.write("[syscall] unknown\r\n"),
}
}
diff --git a/bridge/bridge.zig b/bridge/bridge.zig
index 7d64528..71f7023 100644
--- a/bridge/bridge.zig
+++ b/bridge/bridge.zig
@@ -4,505 +4,13 @@
const std = @import("std");
const revo = @import("revo");
+const serial = @import("serial.zig");
+const hosts = @import("hosts.zig");
-// all the vms output goes here, boot injects this at runtime so the
-// bridge doesnt need to pull a serial port from its ass
-pub const Sink = *const fn ([]const u8) void;
-fn noopSink(_: []const u8) void {}
-var sink: Sink = noopSink;
-
-// fan a multi slice write payload out to the sink
-// mirrors the shape of std.ios file_write_streaming request so
-// an optional header, then the data whose last slice is repeated
-// splat times
-// idk why zig chose these names, they are funny af
-// note: returns the total byte count written
-fn writeToSink(
- header: []const u8,
- data: []const []const u8,
- splat: usize,
-) std.Io.Operation.FileWriteStreaming.Result {
- if (header.len > 0) emit(header);
- for (data[0..data.len -| 1]) |slice| {
- if (slice.len > 0) emit(slice);
- }
- const last = data[data.len -| 1];
- var i: usize = 0;
- while (i < splat) : (i += 1) {
- if (last.len > 0) emit(last);
- }
-
- var total: usize = header.len;
- for (data[0..data.len -| 1]) |slice| total += slice.len;
- total += last.len * splat;
- return total;
-}
-
-// every file write (stdout, stderr, whatever) routes to the serial port but we
-// dont have a filesystem or multiple sinks yet, so theres nowhere else to go
-fn ioOperate(_: ?*anyopaque, operation: std.Io.Operation) std.Io.Cancelable!std.Io.Operation.Result {
- return switch (operation) {
- .file_write_streaming => |req| .{
- .file_write_streaming = writeToSink(req.header, req.data, req.splat),
- },
- .file_read_streaming => .{ .file_read_streaming = error.InputOutput },
- .device_io_control => .{ .device_io_control = -1 },
- .net_receive => .{ .net_receive = .{ error.NetworkDown, 0 } },
- };
-}
-
-// no timer wired up yet, so time stands still,
-// call me the time bender the way i fuck shit up
-// todo: fine until we have the pit/tsc
-fn ioNow(_: ?*anyopaque, _: std.Io.Clock) std.Io.Timestamp {
- return .{ .nanoseconds = 0 };
-}
-
-fn ioClockResolution(_: ?*anyopaque, _: std.Io.Clock) std.Io.Clock.ResolutionError!std.Io.Duration {
- return .{ .nanoseconds = 1_000_000 };
-}
-
-// the scheduler parks fibers itself, it never needs us to actually block
-fn ioSleep(_: ?*anyopaque, _: std.Io.Timeout) std.Io.Cancelable!void {}
-
-fn ioCrashHandler(_: ?*anyopaque) void {}
-
-// eval captures its own errors into a buffer, so this is never reached lol
-// return canceled rather than recursing into a stderr lock we cant provide
-fn ioLockStderr(_: ?*anyopaque, _: ?std.Io.Terminal.Mode) std.Io.Cancelable!std.Io.LockedStderr {
- return error.Canceled;
-}
-
-fn ioUnlockStderr(_: ?*anyopaque) void {}
-
-// start from stds all failing vtable and override only what the vm touches
-const io_vtable: std.Io.VTable = blk: {
- var vtable = std.Io.failing.vtable.*;
- vtable.operate = ioOperate;
- vtable.now = ioNow;
- vtable.clockResolution = ioClockResolution;
- vtable.sleep = ioSleep;
- vtable.crashHandler = ioCrashHandler;
- vtable.lockStderr = ioLockStderr;
- vtable.unlockStderr = ioUnlockStderr;
- break :blk vtable;
-};
-
-const serial_io: std.Io = .{ .userdata = null, .vtable = &io_vtable };
-
-// a second io used only as stds debug io (for std.debug.print, which revo hits
-// on a few error paths). stds default debug io is a threaded stderr writer that
-// doesnt exist on freestandin, and std.io.failings stderr path is literally
-// unreachable, so we give std a real one that drains to the same sink. unlike
-// serial_io above, its lockstderr must return an actual file.writer (std turns a
-// canceled lock into a panic), and swapcancelprotection must not be unreachable
-var dbg_fw: std.Io.File.Writer = undefined;
-var dbg_fw_buf: [256]u8 = undefined;
-
-fn dbgLockStderr(_: ?*anyopaque, _: ?std.Io.Terminal.Mode) std.Io.Cancelable!std.Io.LockedStderr {
- // file.stderr() reads posix.stderr_fileno, which doesnt exist on
- // freestanding so we build the file by hand instead. iooperate ignores
- // the handle and routes every write to the sink regardless
- const stderr_file: std.Io.File = .{ .handle = undefined, .flags = .{ .nonblocking = false } };
- dbg_fw = stderr_file.writerStreaming(debug_io, &dbg_fw_buf);
- return .{ .file_writer = &dbg_fw, .terminal_mode = .no_color };
-}
-fn dbgUnlockStderr(_: ?*anyopaque) void {
- dbg_fw.interface.flush() catch {};
-}
-fn dbgSwapCancel(_: ?*anyopaque, new: std.Io.CancelProtection) std.Io.CancelProtection {
- return new;
-}
-
-const debug_io_vtable: std.Io.VTable = blk: {
- var vtable = std.Io.failing.vtable.*;
- vtable.operate = ioOperate;
- vtable.now = ioNow;
- vtable.clockResolution = ioClockResolution;
- vtable.sleep = ioSleep;
- vtable.crashHandler = ioCrashHandler;
- vtable.lockStderr = dbgLockStderr;
- vtable.unlockStderr = dbgUnlockStderr;
- vtable.swapCancelProtection = dbgSwapCancel;
- break :blk vtable;
-};
-
-pub const debug_io: std.Io = .{ .userdata = null, .vtable = &debug_io_vtable };
-
-// an unbuffered std.io.writer that drains straight to the sink, so we can render
-// revos own error reports (which write to a std.io.writer) onto the console
-const SinkWriter = struct {
- interface: std.Io.Writer,
-
- fn init() SinkWriter {
- return .{ .interface = .{ .buffer = &.{}, .vtable = &.{ .drain = drain, .flush = flush }, .end = 0 } };
- }
-
- fn drain(w: *std.Io.Writer, data: []const []const u8, splat: usize) std.Io.Writer.Error!usize {
- _ = w;
- var total: usize = 0;
- for (data[0..data.len -| 1]) |slice| {
- if (slice.len > 0) emit(slice);
- total += slice.len;
- }
- const last = data[data.len -| 1];
- var i: usize = 0;
- while (i < splat) : (i += 1) {
- if (last.len > 0) emit(last);
- }
- total += last.len * splat;
- return total;
- }
-
- fn flush(_: *std.Io.Writer) std.Io.Writer.Error!void {}
-};
-
-const HostResult = revo.baselib.host.HostResult;
-const Data = revo.Value;
-
-// revos only number type is f64 which is really fucking annoying
-fn f64ToInt(comptime T: type, n: f64) ?T {
- if (!std.math.isFinite(n)) return null;
- // go through @floatfromint for the bounds: maxint(u64) isnt representable
- // as an f64 literal, so comparing against the comptime_int directly wont
- // even compile bruh
- const min_f: f64 = @floatFromInt(std.math.minInt(T));
- const max_f: f64 = @floatFromInt(std.math.maxInt(T));
- if (n < min_f or n > max_f) return null;
- return @intFromFloat(n);
-}
-
-// pull argument i as an integer of type t. args are already type checked to be
-// numbers by define(&.{.number,...}), so asnumopt cant be null, the result is
-// null only when the value is out of ts range (the host fn maps that to an error)
-fn argInt(comptime T: type, args: []const revo.Value, i: usize) ?T {
- return f64ToInt(T, args[i].asNumOpt().?);
-}
-
-// pass shit to revo
-fn hostOutb(args: []const revo.Value, _: *revo.VM) anyerror!HostResult {
- const port = argInt(u16, args, 0) orelse return HostResult.other("outb: port out of range");
- const val = argInt(u8, args, 1) orelse return HostResult.other("outb: value out of range");
- asm volatile ("outb %[v], %[p]"
- :
- : [v] "{al}" (val),
- [p] "N{dx}" (port),
- );
- return HostResult.data(Data.new.nil());
-}
-
-// pass shit to revo
-fn hostInb(args: []const revo.Value, _: *revo.VM) anyerror!HostResult {
- const port = argInt(u16, args, 0) orelse return HostResult.other("inb: port out of range");
- const val = asm volatile ("inb %[p], %[r]"
- : [r] "={al}" (-> u8),
- : [p] "N{dx}" (port),
- );
- return HostResult.data(Data.new.num(val));
-}
-
-// pass shit to revo
-fn hostPuts(args: []const revo.Value, vm: *revo.VM) anyerror!HostResult {
- const string_id = args[0].asString() orelse return HostResult.other("puts: not a string");
- emit(vm.stringValue(string_id));
- return HostResult.data(Data.new.nil());
-}
-
-// the actual framebuffer, the data layout feels pretty self explanatory
-pub const Fb = struct {
- ptr: [*]u8,
- width: usize,
- height: usize,
- pitch: usize,
-};
-var g_fb: ?Fb = null;
-
-fn hostFbWidth(_: []const revo.Value, _: *revo.VM) anyerror!HostResult {
- return HostResult.data(Data.new.num(if (g_fb) |fb| fb.width else 0));
-}
-
-fn hostFbHeight(_: []const revo.Value, _: *revo.VM) anyerror!HostResult {
- return HostResult.data(Data.new.num(if (g_fb) |fb| fb.height else 0));
-}
-
-// color is 0xrrggbb in the usual limine 32 bpp layout
-fn hostFillRect(args: []const revo.Value, _: *revo.VM) anyerror!HostResult {
- const fb = g_fb orelse return HostResult.data(Data.new.nil()); // no screen, no op
- const x = argInt(usize, args, 0) orelse return HostResult.other("fill_rect: bad x");
- const y = argInt(usize, args, 1) orelse return HostResult.other("fill_rect: bad y");
- const w = argInt(usize, args, 2) orelse return HostResult.other("fill_rect: bad w");
- const h = argInt(usize, args, 3) orelse return HostResult.other("fill_rect: bad h");
- const color = argInt(u32, args, 4) orelse return HostResult.other("fill_rect: bad color");
-
- var yy = y;
- const y_end = @min(y + h, fb.height);
- const x_end = @min(x + w, fb.width);
- while (yy < y_end) : (yy += 1) {
- const row = fb.ptr + yy * fb.pitch;
- var xx = x;
- while (xx < x_end) : (xx += 1) {
- const px: *align(1) u32 = @ptrCast(row + xx * 4);
- px.* = color;
- }
- }
- return HostResult.data(Data.new.nil());
-}
-
-// mirror everything to serial
-fn emit(bytes: []const u8) void {
- sink(bytes);
-}
-
-// shift the whole framebuffer up dy pixels and clear the freed rows
-fn hostFbScroll(args: []const revo.Value, _: *revo.VM) anyerror!HostResult {
- const fb = g_fb orelse return HostResult.data(Data.new.nil());
- const dy = argInt(usize, args, 0) orelse return HostResult.other("fb_scroll: bad dy");
- const color = argInt(u32, args, 1) orelse return HostResult.other("fb_scroll: bad color");
- if (dy == 0 or dy >= fb.height) return HostResult.data(Data.new.nil());
- const move_bytes = dy * fb.pitch;
- const total = fb.height * fb.pitch;
- std.mem.copyForwards(u8, fb.ptr[0 .. total - move_bytes], fb.ptr[move_bytes..total]);
- var y: usize = fb.height - dy;
- while (y < fb.height) : (y += 1) {
- const line = fb.ptr + y * fb.pitch;
- var x: usize = 0;
- while (x < fb.width) : (x += 1) {
- const px: *align(1) u32 = @ptrCast(line + x * 4);
- px.* = color;
- }
- }
- return HostResult.data(Data.new.nil());
-}
-
-pub const KernelOps = struct {
- phys_to_virt: *const fn (u64) u64,
- alloc_frame: *const fn () u64, // 0 on failure
- create_addrspace: *const fn () u64, // pml4 phys, 0 on failure (raw 64 bit entry copy incl. possible nx bit -> not f64 representable)
- spawn: *const fn (u64, u64, u64) void, // pml4, entry, ustack -> queue a ring 3 task
- deliver_input: *const fn (u8) bool, // hand a byte to a read()-blocked task, false if none
- proc_exited: *const fn () bool, // true once after a process exits (consumed)
- serial_next: *const fn () i64, // next mirrored serial byte, or -1 if empty
-
- // read only state for revo to present (mem/uptime/ps/dump_memmap):
- memmap_count: *const fn () u64,
- memmap_base: *const fn (u64) u64,
- memmap_len: *const fn (u64) u64,
- memmap_kind: *const fn (u64) u64,
- mem_free: *const fn () u64,
- mem_total: *const fn () u64,
-
- uptime: *const fn () u64,
- proc_count: *const fn () u64,
- prog_count: *const fn () u64,
- prog_phys: *const fn (u64) u64,
- prog_name: *const fn (u64) []const u8,
-
- disk_sectors: *const fn () u64,
- disk_read: *const fn (u64, u64) u64, // lba, phys buf -> 1 ok / 0 fail
- disk_write: *const fn (u64, u64) u64, // lba, phys buf -> 1 ok / 0 fail
-};
-var g_kops: ?KernelOps = null;
-
-fn kops() KernelOps {
- return g_kops.?;
-}
-
-// mem_read(phys, size) -> value: read 1/2/4/8 bytes of physical memory
-fn hostMemRead(args: []const revo.Value, _: *revo.VM) anyerror!HostResult {
- const phys = argInt(u64, args, 0) orelse return HostResult.other("mem_read: bad addr");
- const size = argInt(u64, args, 1) orelse return HostResult.other("mem_read: bad size");
- const virt = kops().phys_to_virt(phys);
- const val: u64 = switch (size) {
- 1 => @as(*const u8, @ptrFromInt(virt)).*,
- 2 => @as(*align(1) const u16, @ptrFromInt(virt)).*,
- 4 => @as(*align(1) const u32, @ptrFromInt(virt)).*,
- 8 => @as(*align(1) const u64, @ptrFromInt(virt)).*,
- else => return HostResult.other("mem_read: size must be 1/2/4/8"),
- };
- return HostResult.data(Data.new.num(val));
-}
-
-// mem_copy(dst_phys, src_phys, len): raw copy between physical regions
-fn hostMemCopy(args: []const revo.Value, _: *revo.VM) anyerror!HostResult {
- const dst = kops().phys_to_virt(argInt(u64, args, 0) orelse return HostResult.other("mem_copy: bad dst"));
- const src = kops().phys_to_virt(argInt(u64, args, 1) orelse return HostResult.other("mem_copy: bad src"));
- const len = argInt(usize, args, 2) orelse return HostResult.other("mem_copy: bad len");
- @memcpy(@as([*]u8, @ptrFromInt(dst))[0..len], @as([*]const u8, @ptrFromInt(src))[0..len]);
- return HostResult.data(Data.new.nil());
-}
-
-// mem_zero(phys, len)
-fn hostMemZero(args: []const revo.Value, _: *revo.VM) anyerror!HostResult {
- const dst = kops().phys_to_virt(argInt(u64, args, 0) orelse return HostResult.other("mem_zero: bad addr"));
- const len = argInt(usize, args, 1) orelse return HostResult.other("mem_zero: bad len");
- @memset(@as([*]u8, @ptrFromInt(dst))[0..len], 0);
- return HostResult.data(Data.new.nil());
-}
-
-// mem_write(phys, val, size): write 1/2/4/8 bytes of physical memory
-fn hostMemWrite(args: []const revo.Value, _: *revo.VM) anyerror!HostResult {
- const phys = argInt(u64, args, 0) orelse return HostResult.other("mem_write: bad addr");
- const val = argInt(u64, args, 1) orelse return HostResult.other("mem_write: bad val");
- const size = argInt(u64, args, 2) orelse return HostResult.other("mem_write: bad size");
- const virt = kops().phys_to_virt(phys);
- switch (size) {
- 1 => @as(*u8, @ptrFromInt(virt)).* = @truncate(val),
- 2 => @as(*align(1) u16, @ptrFromInt(virt)).* = @truncate(val),
- 4 => @as(*align(1) u32, @ptrFromInt(virt)).* = @truncate(val),
- 8 => @as(*align(1) u64, @ptrFromInt(virt)).* = val,
- else => return HostResult.other("mem_write: size must be 1/2/4/8"),
- }
- return HostResult.data(Data.new.nil());
-}
-
-// invlpg(virt): flush one page from the tlb after remapping it
-fn hostInvlpg(args: []const revo.Value, _: *revo.VM) anyerror!HostResult {
- const virt = argInt(u64, args, 0) orelse return HostResult.other("invlpg: bad addr");
- asm volatile ("invlpg (%[v])"
- :
- : [v] "r" (virt),
- : .{ .memory = true });
- return HostResult.data(Data.new.nil());
-}
-
-fn hostFrameAlloc(_: []const revo.Value, _: *revo.VM) anyerror!HostResult {
- return HostResult.data(Data.new.num(kops().alloc_frame()));
-}
-
-fn hostAsCreate(_: []const revo.Value, _: *revo.VM) anyerror!HostResult {
- return HostResult.data(Data.new.num(kops().create_addrspace()));
-}
-
-fn hostProcRun(args: []const revo.Value, _: *revo.VM) anyerror!HostResult {
- const as = argInt(u64, args, 0) orelse return HostResult.other("proc_run: bad as");
- const entry = argInt(u64, args, 1) orelse return HostResult.other("proc_run: bad entry");
- const ustack = argInt(u64, args, 2) orelse return HostResult.other("proc_run: bad ustack");
- kops().spawn(as, entry, ustack); // queues the task, the scheduler runs it
- return HostResult.data(Data.new.nil());
-}
-
-// stdin_deliver(charcode) -> 1 if a read()-blocked process took it, else 0. the
-// revo key handler calls this before feeding its own line editor
-fn hostStdinDeliver(args: []const revo.Value, _: *revo.VM) anyerror!HostResult {
- const ch = argInt(u8, args, 0) orelse return HostResult.other("stdin_deliver: bad char");
- return HostResult.data(Data.new.num(if (kops().deliver_input(ch)) @as(f64, 1) else 0));
-}
-
-// proc_exited() -> 1 once after a process exits, so the shell can reprint its
-// prompt below the programs output
-fn hostProcExited(_: []const revo.Value, _: *revo.VM) anyerror!HostResult {
- return HostResult.data(Data.new.num(if (kops().proc_exited()) @as(f64, 1) else 0));
-}
-
-// serial_next() -> byte or -1
-fn hostSerialNext(_: []const revo.Value, _: *revo.VM) anyerror!HostResult {
- return HostResult.data(Data.new.num(kops().serial_next()));
-}
-
-fn hostProgCount(_: []const revo.Value, _: *revo.VM) anyerror!HostResult {
- return HostResult.data(Data.new.num(kops().prog_count()));
-}
-
-fn hostProgPhys(args: []const revo.Value, _: *revo.VM) anyerror!HostResult {
- const i = argInt(u64, args, 0) orelse return HostResult.other("prog_phys: bad index");
- return HostResult.data(Data.new.num(kops().prog_phys(i)));
-}
-
-fn hostProgName(args: []const revo.Value, vm: *revo.VM) anyerror!HostResult {
- const i = argInt(u64, args, 0) orelse return HostResult.other("prog_name: bad index");
- return HostResult.data(try vm.ownValueString(kops().prog_name(i)));
-}
-
-// read only state getters revo uses for mem/uptime/ps and dump_memmap
-fn hostMemmapCount(_: []const revo.Value, _: *revo.VM) anyerror!HostResult {
- return HostResult.data(Data.new.num(kops().memmap_count()));
-}
-
-fn hostMemmapBase(args: []const revo.Value, _: *revo.VM) anyerror!HostResult {
- const index = argInt(u64, args, 0) orelse return HostResult.other("memmap_base: bad index");
- return HostResult.data(Data.new.num(kops().memmap_base(index)));
-}
-
-fn hostMemmapLen(args: []const revo.Value, _: *revo.VM) anyerror!HostResult {
- const index = argInt(u64, args, 0) orelse return HostResult.other("memmap_len: bad index");
- return HostResult.data(Data.new.num(kops().memmap_len(index)));
-}
-
-fn hostMemmapKind(args: []const revo.Value, _: *revo.VM) anyerror!HostResult {
- const index = argInt(u64, args, 0) orelse return HostResult.other("memmap_kind: bad index");
- return HostResult.data(Data.new.num(kops().memmap_kind(index)));
-}
-
-fn hostMemFree(_: []const revo.Value, _: *revo.VM) anyerror!HostResult {
- return HostResult.data(Data.new.num(kops().mem_free()));
-}
-
-fn hostMemTotal(_: []const revo.Value, _: *revo.VM) anyerror!HostResult {
- return HostResult.data(Data.new.num(kops().mem_total()));
-}
-
-fn hostUptime(_: []const revo.Value, _: *revo.VM) anyerror!HostResult {
- return HostResult.data(Data.new.num(kops().uptime()));
-}
-
-fn hostProcCount(_: []const revo.Value, _: *revo.VM) anyerror!HostResult {
- return HostResult.data(Data.new.num(kops().proc_count()));
-}
-
-fn hostDiskSectors(_: []const revo.Value, _: *revo.VM) anyerror!HostResult {
- return HostResult.data(Data.new.num(kops().disk_sectors()));
-}
-
-fn hostDiskRead(args: []const revo.Value, _: *revo.VM) anyerror!HostResult {
- const lba = argInt(u64, args, 0) orelse return HostResult.other("disk_read: bad lba");
- const phys = argInt(u64, args, 1) orelse return HostResult.other("disk_read: bad phys");
- return HostResult.data(Data.new.num(kops().disk_read(lba, phys)));
-}
-
-fn hostDiskWrite(args: []const revo.Value, _: *revo.VM) anyerror!HostResult {
- const lba = argInt(u64, args, 0) orelse return HostResult.other("disk_write: bad lba");
- const phys = argInt(u64, args, 1) orelse return HostResult.other("disk_write: bad phys");
- return HostResult.data(Data.new.num(kops().disk_write(lba, phys)));
-}
-
-fn registerPrimitives(vm: *revo.VM) !void {
- const define = revo.baselib.host.define;
- const T = revo.baselib.host.ParamType;
- try vm.registerGlobal("outb", try vm.installHost("outb", define(&[_]T{ .number, .number }, hostOutb)));
- try vm.registerGlobal("inb", try vm.installHost("inb", define(&[_]T{.number}, hostInb)));
- try vm.registerGlobal("serial_puts", try vm.installHost("serial_puts", define(&[_]T{.string}, hostPuts)));
- try vm.registerGlobal("fb_width", try vm.installHost("fb_width", define(&[_]T{}, hostFbWidth)));
- try vm.registerGlobal("fb_height", try vm.installHost("fb_height", define(&[_]T{}, hostFbHeight)));
- try vm.registerGlobal("fill_rect", try vm.installHost("fill_rect", define(&[_]T{ .number, .number, .number, .number, .number }, hostFillRect)));
- try vm.registerGlobal("fb_scroll", try vm.installHost("fb_scroll", define(&[_]T{ .number, .number }, hostFbScroll)));
-
- try vm.registerGlobal("mem_read", try vm.installHost("mem_read", define(&[_]T{ .number, .number }, hostMemRead)));
- try vm.registerGlobal("mem_copy", try vm.installHost("mem_copy", define(&[_]T{ .number, .number, .number }, hostMemCopy)));
- try vm.registerGlobal("mem_zero", try vm.installHost("mem_zero", define(&[_]T{ .number, .number }, hostMemZero)));
- try vm.registerGlobal("mem_write", try vm.installHost("mem_write", define(&[_]T{ .number, .number, .number }, hostMemWrite)));
- try vm.registerGlobal("invlpg", try vm.installHost("invlpg", define(&[_]T{.number}, hostInvlpg)));
- try vm.registerGlobal("frame_alloc", try vm.installHost("frame_alloc", define(&[_]T{}, hostFrameAlloc)));
- try vm.registerGlobal("as_create", try vm.installHost("as_create", define(&[_]T{}, hostAsCreate)));
- try vm.registerGlobal("proc_run", try vm.installHost("proc_run", define(&[_]T{ .number, .number, .number }, hostProcRun)));
- try vm.registerGlobal("stdin_deliver", try vm.installHost("stdin_deliver", define(&[_]T{.number}, hostStdinDeliver)));
- try vm.registerGlobal("proc_exited", try vm.installHost("proc_exited", define(&[_]T{}, hostProcExited)));
- try vm.registerGlobal("serial_next", try vm.installHost("serial_next", define(&[_]T{}, hostSerialNext)));
- try vm.registerGlobal("prog_count", try vm.installHost("prog_count", define(&[_]T{}, hostProgCount)));
- try vm.registerGlobal("prog_phys", try vm.installHost("prog_phys", define(&[_]T{.number}, hostProgPhys)));
- try vm.registerGlobal("prog_name", try vm.installHost("prog_name", define(&[_]T{.number}, hostProgName)));
- try vm.registerGlobal("memmap_count", try vm.installHost("memmap_count", define(&[_]T{}, hostMemmapCount)));
- try vm.registerGlobal("memmap_base", try vm.installHost("memmap_base", define(&[_]T{.number}, hostMemmapBase)));
- try vm.registerGlobal("memmap_len", try vm.installHost("memmap_len", define(&[_]T{.number}, hostMemmapLen)));
- try vm.registerGlobal("memmap_kind", try vm.installHost("memmap_kind", define(&[_]T{.number}, hostMemmapKind)));
- try vm.registerGlobal("mem_free", try vm.installHost("mem_free", define(&[_]T{}, hostMemFree)));
- try vm.registerGlobal("mem_total", try vm.installHost("mem_total", define(&[_]T{}, hostMemTotal)));
- try vm.registerGlobal("uptime", try vm.installHost("uptime", define(&[_]T{}, hostUptime)));
- try vm.registerGlobal("proc_count", try vm.installHost("proc_count", define(&[_]T{}, hostProcCount)));
-
- try vm.registerGlobal("disk_sectors", try vm.installHost("disk_sectors", define(&[_]T{}, hostDiskSectors)));
- try vm.registerGlobal("disk_read", try vm.installHost("disk_read", define(&[_]T{ .number, .number }, hostDiskRead)));
- try vm.registerGlobal("disk_write", try vm.installHost("disk_write", define(&[_]T{ .number, .number }, hostDiskWrite)));
-}
+pub const Sink = serial.Sink;
+pub const debug_io = serial.debug_io;
+pub const Fb = hosts.Fb;
+pub const KernelOps = hosts.KernelOps;
// the vm outlives boot() now, so we juts make this hoe static
var g_vm: ?*revo.VM = null;
@@ -511,6 +19,7 @@ const init_program = @embedFile("k_font") ++ "\n" ++
@embedFile("k_console") ++ "\n" ++
@embedFile("k_vmm") ++ "\n" ++
@embedFile("k_fs") ++ "\n" ++
+ @embedFile("k_vfs") ++ "\n" ++
@embedFile("k_seed") ++ "\n" ++
@embedFile("k_proc") ++ "\n" ++
@embedFile("k_shell") ++ "\n" ++
@@ -518,9 +27,9 @@ const init_program = @embedFile("k_font") ++ "\n" ++
@embedFile("k_main");
pub fn boot(alloc: std.mem.Allocator, out: Sink, fb: ?Fb, ops: ?KernelOps) void {
- sink = out;
- g_fb = fb;
- g_kops = ops;
+ serial.setSink(out);
+ hosts.setFb(fb);
+ hosts.setKops(ops);
// note: we drive the vm directly (compile -> run) instead of the high level
// runtime.eval wrapper, which has a latent type error in revo that only
@@ -532,7 +41,7 @@ pub fn boot(alloc: std.mem.Allocator, out: Sink, fb: ?Fb, ops: ?KernelOps) void
};
vm.* = VM.init(.{
.alloc = alloc,
- .io = serial_io,
+ .io = serial.serial_io,
.argv = &.{},
.diag_alloc = alloc,
}) catch {
@@ -541,7 +50,7 @@ pub fn boot(alloc: std.mem.Allocator, out: Sink, fb: ?Fb, ops: ?KernelOps) void
};
out("bridge: revo VM up\r\n");
- registerPrimitives(vm) catch {
+ hosts.registerPrimitives(vm) catch {
out("bridge: registering primitives failed\r\n");
return;
};
@@ -554,7 +63,7 @@ pub fn boot(alloc: std.mem.Allocator, out: Sink, fb: ?Fb, ops: ?KernelOps) void
.ok => |art| art,
.err => |failure| {
out("bridge: compile error: ");
- var sw = SinkWriter.init();
+ var sw = serial.SinkWriter.init();
revo.lang.renderError(alloc, &sw.interface, .{ .name = "kernel", .text = init_program }, failure) catch {};
out("\r\n");
return;
@@ -576,7 +85,7 @@ pub fn boot(alloc: std.mem.Allocator, out: Sink, fb: ?Fb, ops: ?KernelOps) void
.ok => g_vm = vm, // handler is defined so hand the vm to the event loop
.err => |failure| {
out("\r\nbridge: runtime error: ");
- var sw = SinkWriter.init();
+ var sw = serial.SinkWriter.init();
failure.render(alloc, &sw.interface, init_program) catch {};
out("\r\n");
},
@@ -593,21 +102,20 @@ pub fn onKey(scancode: u8) void {
if (cb.asFunction() == null) return;
g_vm_running = true;
defer g_vm_running = false;
- _ = vm.callFunctionParts(cb, null, &[_]revo.Value{revo.Value.new.num(scancode)}, null) catch {
- sink("bridge: on_key threw\r\n");
+ _ = vm.callFunctionParts(cb, null, &[_]revo.Value{revo.Value.new.num(scancode)}, null) catch |e| {
+ serial.emit("bridge: on_key threw: ");
+ serial.emit(@errorName(e));
+ serial.emit("\r\n");
};
flushConsole(vm);
}
-// render any serial output produced while handling the key
fn flushConsole(vm: *revo.VM) void {
const func = vm.getGlobal("flush_console") orelse return;
if (func.asFunction() == null) return;
_ = vm.callFunctionParts(func, null, &.{}, null) catch {};
}
-// called each pit tick from the event loop, hands it to revos on_tick, which
-// renders any process output and blinks the cursor
pub fn onTick(ticks: u64) void {
const vm = g_vm orelse return;
const cb = vm.getGlobal("on_tick") orelse return;
@@ -649,3 +157,14 @@ pub fn fsReadInto(name: []const u8, offset: u64, dst: []u8) usize {
@memcpy(dst[0..n], bytes[0..n]);
return n;
}
+
+pub fn fsStore(name: []const u8, data: []const u8) bool {
+ const vm = g_vm orelse return false;
+ const func = vm.getGlobal("fs_store") orelse return false;
+ const name_val = vm.ownValueString(name) catch return false;
+ const data_val = vm.ownValueString(data) catch return false;
+ g_vm_running = true;
+ defer g_vm_running = false;
+ const r = vm.callFunctionParts(func, null, &[_]revo.Value{ name_val, data_val }, null) catch return false;
+ return (r.asNumOpt() orelse 0) == 1;
+}
diff --git a/bridge/convert.zig b/bridge/convert.zig
new file mode 100644
index 0000000..5718428
--- /dev/null
+++ b/bridge/convert.zig
@@ -0,0 +1,18 @@
+const std = @import("std");
+const revo = @import("revo");
+
+pub const HostResult = revo.baselib.host.HostResult;
+pub const Data = revo.Value;
+
+// revos only number type is f64 which is really fucking annoying
+pub fn f64ToInt(comptime T: type, n: f64) ?T {
+ if (!std.math.isFinite(n)) return null;
+ const min_f: f64 = @floatFromInt(std.math.minInt(T));
+ const max_f: f64 = @floatFromInt(std.math.maxInt(T));
+ if (n < min_f or n > max_f) return null;
+ return @intFromFloat(n);
+}
+
+pub fn argInt(comptime T: type, args: []const revo.Value, i: usize) ?T {
+ return f64ToInt(T, args[i].asNumOpt().?);
+}
diff --git a/bridge/hosts.zig b/bridge/hosts.zig
new file mode 100644
index 0000000..e329316
--- /dev/null
+++ b/bridge/hosts.zig
@@ -0,0 +1,380 @@
+const std = @import("std");
+const revo = @import("revo");
+const serial = @import("serial.zig");
+const convert = @import("convert.zig");
+
+const HostResult = convert.HostResult;
+const Data = convert.Data;
+const argInt = convert.argInt;
+const emit = serial.emit;
+
+// the actual framebuffer, the data layout feels pretty self explanatory
+pub const Fb = struct {
+ ptr: [*]u8,
+ width: usize,
+ height: usize,
+ pitch: usize,
+};
+var g_fb: ?Fb = null;
+
+pub fn setFb(fb: ?Fb) void {
+ g_fb = fb;
+}
+
+pub const KernelOps = struct {
+ phys_to_virt: *const fn (u64) u64,
+ alloc_frame: *const fn () u64, // 0 on failure
+ create_addrspace: *const fn () u64, // pml4 phys, 0 on failure (raw 64 bit entry copy incl. possible nx bit -> not f64 representable)
+ spawn: *const fn (u64, u64, u64, u64, u64) void, // pml4, entry, ustack, arg_ptr, arg_len -> queue a ring 3 task
+ deliver_input: *const fn (u8) bool, // hand a byte to a read()-blocked task, false if none
+ proc_exited: *const fn () bool, // true once after a process exits (consumed)
+ serial_next: *const fn () i64, // next mirrored serial byte, or -1 if empty
+
+ // read only state for revo to present (mem/uptime/ps/dump_memmap):
+ memmap_count: *const fn () u64,
+ memmap_base: *const fn (u64) u64,
+ memmap_len: *const fn (u64) u64,
+ memmap_kind: *const fn (u64) u64,
+ mem_free: *const fn () u64,
+ mem_total: *const fn () u64,
+
+ uptime: *const fn () u64,
+ proc_count: *const fn () u64,
+ prog_count: *const fn () u64,
+ prog_phys: *const fn (u64) u64,
+ prog_name: *const fn (u64) []const u8,
+ prog_size: *const fn (u64) u64,
+ task_max: *const fn () u64,
+ task_state: *const fn (u64) u64,
+
+ disk_sectors: *const fn () u64,
+ disk_read: *const fn (u64, u64) u64, // lba, phys buf -> 1 ok / 0 fail
+ disk_write: *const fn (u64, u64) u64, // lba, phys buf -> 1 ok / 0 fail
+};
+var g_kops: ?KernelOps = null;
+
+pub fn setKops(ops: ?KernelOps) void {
+ g_kops = ops;
+}
+
+fn kops() KernelOps {
+ return g_kops.?;
+}
+
+// pass shit to revo
+fn hostOutb(args: []const revo.Value, _: *revo.VM) anyerror!HostResult {
+ const port = argInt(u16, args, 0) orelse return HostResult.other("outb: port out of range");
+ const val = argInt(u8, args, 1) orelse return HostResult.other("outb: value out of range");
+ asm volatile ("outb %[v], %[p]"
+ :
+ : [v] "{al}" (val),
+ [p] "N{dx}" (port),
+ );
+ return HostResult.data(Data.new.nil());
+}
+
+// pass shit to revo
+fn hostInb(args: []const revo.Value, _: *revo.VM) anyerror!HostResult {
+ const port = argInt(u16, args, 0) orelse return HostResult.other("inb: port out of range");
+ const val = asm volatile ("inb %[p], %[r]"
+ : [r] "={al}" (-> u8),
+ : [p] "N{dx}" (port),
+ );
+ return HostResult.data(Data.new.num(val));
+}
+
+// pass shit to revo
+fn hostPuts(args: []const revo.Value, vm: *revo.VM) anyerror!HostResult {
+ const string_id = args[0].asString() orelse return HostResult.other("puts: not a string");
+ emit(vm.stringValue(string_id));
+ return HostResult.data(Data.new.nil());
+}
+
+// pass shit to revo
+fn hostFbWidth(_: []const revo.Value, _: *revo.VM) anyerror!HostResult {
+ return HostResult.data(Data.new.num(if (g_fb) |fb| fb.width else 0));
+}
+
+// pass shit to revo
+fn hostFbHeight(_: []const revo.Value, _: *revo.VM) anyerror!HostResult {
+ return HostResult.data(Data.new.num(if (g_fb) |fb| fb.height else 0));
+}
+
+// color is 0xrrggbb in the usual limine 32 bpp layout
+// pass shit to revo
+fn hostFillRect(args: []const revo.Value, _: *revo.VM) anyerror!HostResult {
+ const fb = g_fb orelse return HostResult.data(Data.new.nil()); // no screen, no op
+ const x = argInt(usize, args, 0) orelse return HostResult.other("fill_rect: bad x");
+ const y = argInt(usize, args, 1) orelse return HostResult.other("fill_rect: bad y");
+ const w = argInt(usize, args, 2) orelse return HostResult.other("fill_rect: bad w");
+ const h = argInt(usize, args, 3) orelse return HostResult.other("fill_rect: bad h");
+ const color = argInt(u32, args, 4) orelse return HostResult.other("fill_rect: bad color");
+
+ var yy = y;
+ const y_end = @min(y + h, fb.height);
+ const x_end = @min(x + w, fb.width);
+ while (yy < y_end) : (yy += 1) {
+ const row = fb.ptr + yy * fb.pitch;
+ var xx = x;
+ while (xx < x_end) : (xx += 1) {
+ const px: *align(1) u32 = @ptrCast(row + xx * 4);
+ px.* = color;
+ }
+ }
+ return HostResult.data(Data.new.nil());
+}
+
+// pass shit to revo
+fn hostFbScroll(args: []const revo.Value, _: *revo.VM) anyerror!HostResult {
+ const fb = g_fb orelse return HostResult.data(Data.new.nil());
+ const dy = argInt(usize, args, 0) orelse return HostResult.other("fb_scroll: bad dy");
+ const color = argInt(u32, args, 1) orelse return HostResult.other("fb_scroll: bad color");
+ if (dy == 0 or dy >= fb.height) return HostResult.data(Data.new.nil());
+ const move_bytes = dy * fb.pitch;
+ const total = fb.height * fb.pitch;
+ std.mem.copyForwards(u8, fb.ptr[0 .. total - move_bytes], fb.ptr[move_bytes..total]);
+ var y: usize = fb.height - dy;
+ while (y < fb.height) : (y += 1) {
+ const line = fb.ptr + y * fb.pitch;
+ var x: usize = 0;
+ while (x < fb.width) : (x += 1) {
+ const px: *align(1) u32 = @ptrCast(line + x * 4);
+ px.* = color;
+ }
+ }
+ return HostResult.data(Data.new.nil());
+}
+
+// pass shit to revo
+fn hostMemRead(args: []const revo.Value, _: *revo.VM) anyerror!HostResult {
+ const phys = argInt(u64, args, 0) orelse return HostResult.other("mem_read: bad addr");
+ const size = argInt(u64, args, 1) orelse return HostResult.other("mem_read: bad size");
+ const virt = kops().phys_to_virt(phys);
+ const val: u64 = switch (size) {
+ 1 => @as(*const u8, @ptrFromInt(virt)).*,
+ 2 => @as(*align(1) const u16, @ptrFromInt(virt)).*,
+ 4 => @as(*align(1) const u32, @ptrFromInt(virt)).*,
+ 8 => @as(*align(1) const u64, @ptrFromInt(virt)).*,
+ else => return HostResult.other("mem_read: size must be 1/2/4/8"),
+ };
+ return HostResult.data(Data.new.num(val));
+}
+
+// pass shit to revo
+fn hostMemCopy(args: []const revo.Value, _: *revo.VM) anyerror!HostResult {
+ const dst = kops().phys_to_virt(argInt(u64, args, 0) orelse return HostResult.other("mem_copy: bad dst"));
+ const src = kops().phys_to_virt(argInt(u64, args, 1) orelse return HostResult.other("mem_copy: bad src"));
+ const len = argInt(usize, args, 2) orelse return HostResult.other("mem_copy: bad len");
+ @memcpy(@as([*]u8, @ptrFromInt(dst))[0..len], @as([*]const u8, @ptrFromInt(src))[0..len]);
+ return HostResult.data(Data.new.nil());
+}
+
+// pass shit to revo
+fn hostMemZero(args: []const revo.Value, _: *revo.VM) anyerror!HostResult {
+ const dst = kops().phys_to_virt(argInt(u64, args, 0) orelse return HostResult.other("mem_zero: bad addr"));
+ const len = argInt(usize, args, 1) orelse return HostResult.other("mem_zero: bad len");
+ @memset(@as([*]u8, @ptrFromInt(dst))[0..len], 0);
+ return HostResult.data(Data.new.nil());
+}
+
+// pass shit to revo
+fn hostMemWrite(args: []const revo.Value, _: *revo.VM) anyerror!HostResult {
+ const phys = argInt(u64, args, 0) orelse return HostResult.other("mem_write: bad addr");
+ const val = argInt(u64, args, 1) orelse return HostResult.other("mem_write: bad val");
+ const size = argInt(u64, args, 2) orelse return HostResult.other("mem_write: bad size");
+ const virt = kops().phys_to_virt(phys);
+ switch (size) {
+ 1 => @as(*u8, @ptrFromInt(virt)).* = @truncate(val),
+ 2 => @as(*align(1) u16, @ptrFromInt(virt)).* = @truncate(val),
+ 4 => @as(*align(1) u32, @ptrFromInt(virt)).* = @truncate(val),
+ 8 => @as(*align(1) u64, @ptrFromInt(virt)).* = val,
+ else => return HostResult.other("mem_write: size must be 1/2/4/8"),
+ }
+ return HostResult.data(Data.new.nil());
+}
+
+// pass shit to revo
+fn hostInvlpg(args: []const revo.Value, _: *revo.VM) anyerror!HostResult {
+ const virt = argInt(u64, args, 0) orelse return HostResult.other("invlpg: bad addr");
+ asm volatile ("invlpg (%[v])"
+ :
+ : [v] "r" (virt),
+ : .{ .memory = true });
+ return HostResult.data(Data.new.nil());
+}
+
+// pass shit to revo
+fn hostFrameAlloc(_: []const revo.Value, _: *revo.VM) anyerror!HostResult {
+ return HostResult.data(Data.new.num(kops().alloc_frame()));
+}
+
+// pass shit to revo
+fn hostAsCreate(_: []const revo.Value, _: *revo.VM) anyerror!HostResult {
+ return HostResult.data(Data.new.num(kops().create_addrspace()));
+}
+
+// pass shit to revo
+fn hostProcRun(args: []const revo.Value, _: *revo.VM) anyerror!HostResult {
+ const as = argInt(u64, args, 0) orelse return HostResult.other("proc_run: bad as");
+ const entry = argInt(u64, args, 1) orelse return HostResult.other("proc_run: bad entry");
+ const ustack = argInt(u64, args, 2) orelse return HostResult.other("proc_run: bad ustack");
+ const arg = argInt(u64, args, 3) orelse return HostResult.other("proc_run: bad arg");
+ const arglen = argInt(u64, args, 4) orelse return HostResult.other("proc_run: bad arglen");
+ kops().spawn(as, entry, ustack, arg, arglen);
+ return HostResult.data(Data.new.nil());
+}
+
+// pass shit to revo
+fn hostStdinDeliver(args: []const revo.Value, _: *revo.VM) anyerror!HostResult {
+ const ch = argInt(u8, args, 0) orelse return HostResult.other("stdin_deliver: bad char");
+ return HostResult.data(Data.new.num(if (kops().deliver_input(ch)) @as(f64, 1) else 0));
+}
+
+// pass shit to revo
+fn hostProcExited(_: []const revo.Value, _: *revo.VM) anyerror!HostResult {
+ return HostResult.data(Data.new.num(if (kops().proc_exited()) @as(f64, 1) else 0));
+}
+
+// pass shit to revo
+fn hostSerialNext(_: []const revo.Value, _: *revo.VM) anyerror!HostResult {
+ return HostResult.data(Data.new.num(kops().serial_next()));
+}
+
+// pass shit to revo
+fn hostProgCount(_: []const revo.Value, _: *revo.VM) anyerror!HostResult {
+ return HostResult.data(Data.new.num(kops().prog_count()));
+}
+
+// pass shit to revo
+fn hostProgPhys(args: []const revo.Value, _: *revo.VM) anyerror!HostResult {
+ const i = argInt(u64, args, 0) orelse return HostResult.other("prog_phys: bad index");
+ return HostResult.data(Data.new.num(kops().prog_phys(i)));
+}
+
+// pass shit to revo
+fn hostProgName(args: []const revo.Value, vm: *revo.VM) anyerror!HostResult {
+ const i = argInt(u64, args, 0) orelse return HostResult.other("prog_name: bad index");
+ return HostResult.data(try vm.ownValueString(kops().prog_name(i)));
+}
+
+// pass shit to revo
+fn hostProgSize(args: []const revo.Value, _: *revo.VM) anyerror!HostResult {
+ const i = argInt(u64, args, 0) orelse return HostResult.other("prog_size: bad index");
+ return HostResult.data(Data.new.num(kops().prog_size(i)));
+}
+
+// pass shit to revo
+fn hostTaskMax(_: []const revo.Value, _: *revo.VM) anyerror!HostResult {
+ return HostResult.data(Data.new.num(kops().task_max()));
+}
+
+// pass shit to revo
+fn hostTaskState(args: []const revo.Value, _: *revo.VM) anyerror!HostResult {
+ const i = argInt(u64, args, 0) orelse return HostResult.other("task_state: bad index");
+ return HostResult.data(Data.new.num(kops().task_state(i)));
+}
+
+// pass shit to revo
+fn hostMemmapCount(_: []const revo.Value, _: *revo.VM) anyerror!HostResult {
+ return HostResult.data(Data.new.num(kops().memmap_count()));
+}
+
+// pass shit to revo
+fn hostMemmapBase(args: []const revo.Value, _: *revo.VM) anyerror!HostResult {
+ const index = argInt(u64, args, 0) orelse return HostResult.other("memmap_base: bad index");
+ return HostResult.data(Data.new.num(kops().memmap_base(index)));
+}
+
+// pass shit to revo
+fn hostMemmapLen(args: []const revo.Value, _: *revo.VM) anyerror!HostResult {
+ const index = argInt(u64, args, 0) orelse return HostResult.other("memmap_len: bad index");
+ return HostResult.data(Data.new.num(kops().memmap_len(index)));
+}
+
+// pass shit to revo
+fn hostMemmapKind(args: []const revo.Value, _: *revo.VM) anyerror!HostResult {
+ const index = argInt(u64, args, 0) orelse return HostResult.other("memmap_kind: bad index");
+ return HostResult.data(Data.new.num(kops().memmap_kind(index)));
+}
+
+// pass shit to revo
+fn hostMemFree(_: []const revo.Value, _: *revo.VM) anyerror!HostResult {
+ return HostResult.data(Data.new.num(kops().mem_free()));
+}
+
+// pass shit to revo
+fn hostMemTotal(_: []const revo.Value, _: *revo.VM) anyerror!HostResult {
+ return HostResult.data(Data.new.num(kops().mem_total()));
+}
+
+// pass shit to revo
+fn hostUptime(_: []const revo.Value, _: *revo.VM) anyerror!HostResult {
+ return HostResult.data(Data.new.num(kops().uptime()));
+}
+
+// pass shit to revo
+fn hostProcCount(_: []const revo.Value, _: *revo.VM) anyerror!HostResult {
+ return HostResult.data(Data.new.num(kops().proc_count()));
+}
+
+// pass shit to revo
+fn hostDiskSectors(_: []const revo.Value, _: *revo.VM) anyerror!HostResult {
+ return HostResult.data(Data.new.num(kops().disk_sectors()));
+}
+
+// pass shit to revo
+fn hostDiskRead(args: []const revo.Value, _: *revo.VM) anyerror!HostResult {
+ const lba = argInt(u64, args, 0) orelse return HostResult.other("disk_read: bad lba");
+ const phys = argInt(u64, args, 1) orelse return HostResult.other("disk_read: bad phys");
+ return HostResult.data(Data.new.num(kops().disk_read(lba, phys)));
+}
+
+// pass shit to revo
+fn hostDiskWrite(args: []const revo.Value, _: *revo.VM) anyerror!HostResult {
+ const lba = argInt(u64, args, 0) orelse return HostResult.other("disk_write: bad lba");
+ const phys = argInt(u64, args, 1) orelse return HostResult.other("disk_write: bad phys");
+ return HostResult.data(Data.new.num(kops().disk_write(lba, phys)));
+}
+
+// pass shit to revo
+pub fn registerPrimitives(vm: *revo.VM) !void {
+ const define = revo.baselib.host.define;
+ const T = revo.baselib.host.ParamType;
+ try vm.registerGlobal("outb", try vm.installHost("outb", define(&[_]T{ .number, .number }, hostOutb)));
+ try vm.registerGlobal("inb", try vm.installHost("inb", define(&[_]T{.number}, hostInb)));
+ try vm.registerGlobal("serial_puts", try vm.installHost("serial_puts", define(&[_]T{.string}, hostPuts)));
+ try vm.registerGlobal("fb_width", try vm.installHost("fb_width", define(&[_]T{}, hostFbWidth)));
+ try vm.registerGlobal("fb_height", try vm.installHost("fb_height", define(&[_]T{}, hostFbHeight)));
+ try vm.registerGlobal("fill_rect", try vm.installHost("fill_rect", define(&[_]T{ .number, .number, .number, .number, .number }, hostFillRect)));
+ try vm.registerGlobal("fb_scroll", try vm.installHost("fb_scroll", define(&[_]T{ .number, .number }, hostFbScroll)));
+
+ try vm.registerGlobal("mem_read", try vm.installHost("mem_read", define(&[_]T{ .number, .number }, hostMemRead)));
+ try vm.registerGlobal("mem_copy", try vm.installHost("mem_copy", define(&[_]T{ .number, .number, .number }, hostMemCopy)));
+ try vm.registerGlobal("mem_zero", try vm.installHost("mem_zero", define(&[_]T{ .number, .number }, hostMemZero)));
+ try vm.registerGlobal("mem_write", try vm.installHost("mem_write", define(&[_]T{ .number, .number, .number }, hostMemWrite)));
+ try vm.registerGlobal("invlpg", try vm.installHost("invlpg", define(&[_]T{.number}, hostInvlpg)));
+ try vm.registerGlobal("frame_alloc", try vm.installHost("frame_alloc", define(&[_]T{}, hostFrameAlloc)));
+ try vm.registerGlobal("as_create", try vm.installHost("as_create", define(&[_]T{}, hostAsCreate)));
+ try vm.registerGlobal("proc_run", try vm.installHost("proc_run", define(&[_]T{ .number, .number, .number, .number, .number }, hostProcRun)));
+ try vm.registerGlobal("stdin_deliver", try vm.installHost("stdin_deliver", define(&[_]T{.number}, hostStdinDeliver)));
+ try vm.registerGlobal("proc_exited", try vm.installHost("proc_exited", define(&[_]T{}, hostProcExited)));
+ try vm.registerGlobal("serial_next", try vm.installHost("serial_next", define(&[_]T{}, hostSerialNext)));
+ try vm.registerGlobal("prog_count", try vm.installHost("prog_count", define(&[_]T{}, hostProgCount)));
+ try vm.registerGlobal("prog_phys", try vm.installHost("prog_phys", define(&[_]T{.number}, hostProgPhys)));
+ try vm.registerGlobal("prog_name", try vm.installHost("prog_name", define(&[_]T{.number}, hostProgName)));
+ try vm.registerGlobal("prog_size", try vm.installHost("prog_size", define(&[_]T{.number}, hostProgSize)));
+ try vm.registerGlobal("task_max", try vm.installHost("task_max", define(&[_]T{}, hostTaskMax)));
+ try vm.registerGlobal("task_state", try vm.installHost("task_state", define(&[_]T{.number}, hostTaskState)));
+ try vm.registerGlobal("memmap_count", try vm.installHost("memmap_count", define(&[_]T{}, hostMemmapCount)));
+ try vm.registerGlobal("memmap_base", try vm.installHost("memmap_base", define(&[_]T{.number}, hostMemmapBase)));
+ try vm.registerGlobal("memmap_len", try vm.installHost("memmap_len", define(&[_]T{.number}, hostMemmapLen)));
+ try vm.registerGlobal("memmap_kind", try vm.installHost("memmap_kind", define(&[_]T{.number}, hostMemmapKind)));
+ try vm.registerGlobal("mem_free", try vm.installHost("mem_free", define(&[_]T{}, hostMemFree)));
+ try vm.registerGlobal("mem_total", try vm.installHost("mem_total", define(&[_]T{}, hostMemTotal)));
+ try vm.registerGlobal("uptime", try vm.installHost("uptime", define(&[_]T{}, hostUptime)));
+ try vm.registerGlobal("proc_count", try vm.installHost("proc_count", define(&[_]T{}, hostProcCount)));
+
+ try vm.registerGlobal("disk_sectors", try vm.installHost("disk_sectors", define(&[_]T{}, hostDiskSectors)));
+ try vm.registerGlobal("disk_read", try vm.installHost("disk_read", define(&[_]T{ .number, .number }, hostDiskRead)));
+ try vm.registerGlobal("disk_write", try vm.installHost("disk_write", define(&[_]T{ .number, .number }, hostDiskWrite)));
+}
diff --git a/bridge/serial.zig b/bridge/serial.zig
new file mode 100644
index 0000000..ec0bdb9
--- /dev/null
+++ b/bridge/serial.zig
@@ -0,0 +1,141 @@
+const std = @import("std");
+
+pub const Sink = *const fn ([]const u8) void;
+fn noopSink(_: []const u8) void {}
+var sink: Sink = noopSink;
+
+pub fn setSink(s: Sink) void {
+ sink = s;
+}
+
+// mirror everything to serial
+pub fn emit(bytes: []const u8) void {
+ sink(bytes);
+}
+
+// note: returns the total byte count written
+fn writeToSink(
+ header: []const u8,
+ data: []const []const u8,
+ splat: usize,
+) std.Io.Operation.FileWriteStreaming.Result {
+ if (header.len > 0) emit(header);
+ for (data[0..data.len -| 1]) |slice| {
+ if (slice.len > 0) emit(slice);
+ }
+ const last = data[data.len -| 1];
+ var i: usize = 0;
+ while (i < splat) : (i += 1) {
+ if (last.len > 0) emit(last);
+ }
+
+ var total: usize = header.len;
+ for (data[0..data.len -| 1]) |slice| total += slice.len;
+ total += last.len * splat;
+ return total;
+}
+
+fn ioOperate(_: ?*anyopaque, operation: std.Io.Operation) std.Io.Cancelable!std.Io.Operation.Result {
+ return switch (operation) {
+ .file_write_streaming => |req| .{
+ .file_write_streaming = writeToSink(req.header, req.data, req.splat),
+ },
+ .file_read_streaming => .{ .file_read_streaming = error.InputOutput },
+ .device_io_control => .{ .device_io_control = -1 },
+ .net_receive => .{ .net_receive = .{ error.NetworkDown, 0 } },
+ };
+}
+
+// no timer wired up yet
+// #call me the time bender the way i fuck shit up
+// todo: fine until we have the pit/tsc
+fn ioNow(_: ?*anyopaque, _: std.Io.Clock) std.Io.Timestamp {
+ return .{ .nanoseconds = 0 };
+}
+
+fn ioClockResolution(_: ?*anyopaque, _: std.Io.Clock) std.Io.Clock.ResolutionError!std.Io.Duration {
+ return .{ .nanoseconds = 1_000_000 };
+}
+
+fn ioSleep(_: ?*anyopaque, _: std.Io.Timeout) std.Io.Cancelable!void {}
+
+fn ioCrashHandler(_: ?*anyopaque) void {}
+
+// eval captures its own errors into a buffer, so this is never reached lol
+fn ioLockStderr(_: ?*anyopaque, _: ?std.Io.Terminal.Mode) std.Io.Cancelable!std.Io.LockedStderr {
+ return error.Canceled;
+}
+
+fn ioUnlockStderr(_: ?*anyopaque) void {}
+
+const io_vtable: std.Io.VTable = blk: {
+ var vtable = std.Io.failing.vtable.*;
+ vtable.operate = ioOperate;
+ vtable.now = ioNow;
+ vtable.clockResolution = ioClockResolution;
+ vtable.sleep = ioSleep;
+ vtable.crashHandler = ioCrashHandler;
+ vtable.lockStderr = ioLockStderr;
+ vtable.unlockStderr = ioUnlockStderr;
+ break :blk vtable;
+};
+
+pub const serial_io: std.Io = .{ .userdata = null, .vtable = &io_vtable };
+
+var dbg_fw: std.Io.File.Writer = undefined;
+var dbg_fw_buf: [256]u8 = undefined;
+
+fn dbgLockStderr(_: ?*anyopaque, _: ?std.Io.Terminal.Mode) std.Io.Cancelable!std.Io.LockedStderr {
+ const stderr_file: std.Io.File = .{ .handle = undefined, .flags = .{ .nonblocking = false } };
+ dbg_fw = stderr_file.writerStreaming(debug_io, &dbg_fw_buf);
+ return .{ .file_writer = &dbg_fw, .terminal_mode = .no_color };
+}
+fn dbgUnlockStderr(_: ?*anyopaque) void {
+ dbg_fw.interface.flush() catch {};
+}
+fn dbgSwapCancel(_: ?*anyopaque, new: std.Io.CancelProtection) std.Io.CancelProtection {
+ return new;
+}
+
+const debug_io_vtable: std.Io.VTable = blk: {
+ var vtable = std.Io.failing.vtable.*;
+ vtable.operate = ioOperate;
+ vtable.now = ioNow;
+ vtable.clockResolution = ioClockResolution;
+ vtable.sleep = ioSleep;
+ vtable.crashHandler = ioCrashHandler;
+ vtable.lockStderr = dbgLockStderr;
+ vtable.unlockStderr = dbgUnlockStderr;
+ vtable.swapCancelProtection = dbgSwapCancel;
+ break :blk vtable;
+};
+
+pub const debug_io: std.Io = .{ .userdata = null, .vtable = &debug_io_vtable };
+
+// an unbuffered std.io.writer that drains straight to the sink, so we can render
+// revos own error reports (which write to a std.io.writer) onto the console
+pub const SinkWriter = struct {
+ interface: std.Io.Writer,
+
+ pub fn init() SinkWriter {
+ return .{ .interface = .{ .buffer = &.{}, .vtable = &.{ .drain = drain, .flush = flush }, .end = 0 } };
+ }
+
+ fn drain(w: *std.Io.Writer, data: []const []const u8, splat: usize) std.Io.Writer.Error!usize {
+ _ = w;
+ var total: usize = 0;
+ for (data[0..data.len -| 1]) |slice| {
+ if (slice.len > 0) emit(slice);
+ total += slice.len;
+ }
+ const last = data[data.len -| 1];
+ var i: usize = 0;
+ while (i < splat) : (i += 1) {
+ if (last.len > 0) emit(last);
+ }
+ total += last.len * splat;
+ return total;
+ }
+
+ fn flush(_: *std.Io.Writer) std.Io.Writer.Error!void {}
+};
diff --git a/build.zig b/build.zig
index 0c38e9f..3575cf0 100644
--- a/build.zig
+++ b/build.zig
@@ -68,6 +68,7 @@ pub fn build(b: *std.Build) void {
.{ "k_console", "kernel/console.rv" },
.{ "k_vmm", "kernel/vmm.rv" },
.{ "k_fs", "kernel/fs.rv" },
+ .{ "k_vfs", "kernel/vfs.rv" },
.{ "k_seed", "kernel/seed.rv" },
.{ "k_proc", "kernel/proc.rv" },
.{ "k_shell", "kernel/shell.rv" },
@@ -81,7 +82,7 @@ pub fn build(b: *std.Build) void {
// each ring 3 program compiles to its own freestanding elf, embedded under
// user_<name>. boot copies them to frames and the shells `run <name>` loads one
- inline for (.{ "hexview", "calc", "primes" }) |prog| {
+ inline for (.{ "hexview", "calc", "primes", "save" }) |prog| {
const cc = b.addSystemCommand(&.{
"zig", "cc",
"-target", "x86_64-freestanding",
diff --git a/kernel/font.rv b/kernel/font.rv
index 0720dd4..7735b38 100644
--- a/kernel/font.rv
+++ b/kernel/font.rv
@@ -2,6 +2,7 @@
# ascii 0x20..0x7f, indexed by (code - 32)
# one byte per row, 16 rows
# bit 0 (lsb) of a byte is the leftmost pixel (vga data is msb first, reversed here)
+
global FONT = {
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, # space
{ 0x00, 0x00, 0x18, 0x3C, 0x3C, 0x3C, 0x18, 0x18, 0x18, 0x00, 0x18, 0x18, 0x00, 0x00, 0x00, 0x00 }, # !
diff --git a/kernel/fs.rv b/kernel/fs.rv
index 0c98c36..c045950 100644
--- a/kernel/fs.rv
+++ b/kernel/fs.rv
@@ -1,32 +1,37 @@
-# revelfs, a tiny fat style filesystem, now an object you talk to as fs:method()
-#
# on disk layout for the 16 mib test disk (32768 sectors):
-# sector 0 superblock (magic + total sectors)
-# sectors 1..8 directory, 128 entries of 32 bytes (16 per sector)
-# sectors 9..136 fat, one 2 byte entry per sector on the whole disk
-# sectors 137.. file data, one block == one sector
-# a fat entry points at the next block in a files chain, 0 is free, END is the last
+# sector 0 superblock (magic + total sectors + root dir block)
+# sectors 1..128 fat, one 2 byte entry per block on the whole disk
+# sectors 129.. data, used for both file bytes and directory entry blocks
+#
+# a directory is just a file whose data blocks hold 32 byte entries. the
+# superblock points at the root dir, paths are walked from there. a fat entry
+# points at the next block in a chain, 0 is free, END is the last
const SB_LBA = 0
-const DIR_LBA = 1
-const DIR_SECTORS = 8
-const DIR_ENTRIES = 128
-const FAT_LBA = 9
+const FAT_LBA = 1
const FAT_SECTORS = 128
-const DATA_LBA = 137
+const DATA_LBA = 129
const BLOCK = 512
+const ENTS = 16 # 32 byte entries per 512 byte block
const NAME_MAX = 24
-const MAGIC = 0x53465652 # "RVFS" little endian
-const END = 0xffff # end of chain marker in the fat
+const MAGIC = 0x32465652 # "RVF2" little endian
+const END = 0xffff # end of chain marker in the fat
+const T_FILE = 1
+const T_DIR = 2
-# pure index math, no state, so theyre plain helpers not methods
-fn dir_lba(i) DIR_LBA + (i shr 4)
-fn dir_off(i) (i band 15) * 32
+fn split_path(path) do
+ const raw = path:split("/")
+ let parts = {}
+ for i in 0..raw:len() do
+ if raw[i]:len() > 0 parts:push(raw[i])
+ end
+ return parts
+end
-# buf is a scratch sector for the superblock/directory/data, fat is a separate
-# scratch sector so fat lookups dont trample a directory sector were mid edit
+# buf is a scratch sector for superblocks/dirs/data, fat is a separate scratch so
+# fat lookups dont trample a dir block were mid edit.
global fs = {
- buf = 0, fat = 0, ready = :false, total = 0,
+ buf = 0, fat = 0, ready = :false, total = 0, root = 0,
fat_get = fn(self, block) do
disk_read(FAT_LBA + (block shr 8), self.fat)
@@ -40,8 +45,6 @@ global fs = {
disk_write(lba, self.fat)
end,
- # first free data block, or 0 if full. scans a whole fat sector per read so this
- # isnt snails pace
fat_alloc = fn(self) do
let block = DATA_LBA
while block < self.total do
@@ -65,39 +68,145 @@ global fs = {
end
end,
- # dir entry is 32 bytes:
- # +0 used
+ # entry is 32 bytes:
+ # +0 type (0 free/1 file/2 dir)
# +1 name len
# +2 first block
# +4 size
- # +8 name
- name_at = fn(self, base, len) do
+ # +8 name bytes
+ ent_name = fn(self, eoff) do
+ const len = mem_read(self.buf + eoff + 1, 1)
let out = ""
- for k in 0..len do out = out ~ string.of_ascii(mem_read(base + 8 + k, 1)) end
+ for k in 0..len do out = out ~ string.of_ascii(mem_read(self.buf + eoff + 8 + k, 1)) end
out
end,
- name_matches = fn(self, base, target) do
- const len = mem_read(base + 1, 1)
- if len != target:len() return :false
- return self:name_at(base, len) == target
+ ent_write = fn(self, eoff, type, name, first, size) do
+ mem_write(self.buf + eoff, type, 1)
+ mem_write(self.buf + eoff + 1, name:len(), 1)
+ mem_write(self.buf + eoff + 2, first, 2)
+ mem_write(self.buf + eoff + 4, size, 4)
+ for k in 0..name:len() do mem_write(self.buf + eoff + 8 + k, name[k]:ascii(), 1) end
+ end,
+
+ dir_lookup = fn(self, dir_first, name) do
+ let block = dir_first
+ while (block != 0 and block != END) do
+ const nxt = self:fat_get(block)
+ disk_read(block, self.buf)
+ for slot in 0..ENTS do
+ const eoff = slot * 32
+ if mem_read(self.buf + eoff, 1) != 0 and self:ent_name(eoff) == name do
+ return { type = mem_read(self.buf + eoff, 1), first = mem_read(self.buf + eoff + 2, 2), size = mem_read(self.buf + eoff + 4, 4) }
+ end
+ end
+ block = nxt
+ end
+ return :nil
+ end,
+
+ # add or overwrite `name` in the dir
+ # grows a block if every slot is taken
+ dir_set = fn(self, dir_first, name, type, first, size) do
+ let block = dir_first
+ let found_block = 0
+ let found_off = 0
+ let free_block = 0
+ let free_off = -1
+ let last_block = dir_first
+ while (block != 0 and block != END) do
+ last_block = block
+ const nxt = self:fat_get(block)
+ disk_read(block, self.buf)
+ for slot in 0..ENTS do
+ const eoff = slot * 32
+ if mem_read(self.buf + eoff, 1) == 0 do
+ if free_off < 0 do free_block = block free_off = eoff end
+ end
+ else do
+ if self:ent_name(eoff) == name do found_block = block found_off = eoff end
+ end
+ end
+ block = nxt
+ end
+
+ let target_block = 0
+ let target_off = 0
+ if found_block != 0 do target_block = found_block target_off = found_off end
+ else if free_off >= 0 do target_block = free_block target_off = free_off end
+ else do
+ const nb = self:fat_alloc()
+ if nb == 0 return :false
+ self:fat_set(nb, END)
+ self:fat_set(last_block, nb)
+ mem_zero(self.buf, BLOCK)
+ disk_write(nb, self.buf)
+ target_block = nb
+ target_off = 0
+ end
+
+ disk_read(target_block, self.buf)
+ self:ent_write(target_off, type, name, first, size)
+ disk_write(target_block, self.buf)
+ return :true
+ end,
+
+ dir_clear = fn(self, dir_first, name) do
+ let block = dir_first
+ while (block != 0 and block != END) do
+ const nxt = self:fat_get(block)
+ disk_read(block, self.buf)
+ for slot in 0..ENTS do
+ const eoff = slot * 32
+ if mem_read(self.buf + eoff, 1) != 0 and self:ent_name(eoff) == name do
+ const info = { type = mem_read(self.buf + eoff, 1), first = mem_read(self.buf + eoff + 2, 2) }
+ mem_write(self.buf + eoff, 0, 1)
+ disk_write(block, self.buf)
+ return info
+ end
+ end
+ block = nxt
+ end
+ return :nil
+ end,
+
+ dir_empty = fn(self, dir_first) do
+ let block = dir_first
+ while (block != 0 and block != END) do
+ const nxt = self:fat_get(block)
+ disk_read(block, self.buf)
+ for slot in 0..ENTS do
+ if mem_read(self.buf + slot * 32, 1) != 0 return :false
+ end
+ block = nxt
+ end
+ return :true
end,
- find = fn(self, name) do
- for i in 0..DIR_ENTRIES do
- disk_read(dir_lba(i), self.buf)
- const base = self.buf + dir_off(i)
- if mem_read(base, 1) == 1 and self:name_matches(base, name) return i
+ resolve = fn(self, path) do
+ const parts = split_path(path)
+ let cur = self.root
+ let ent = { type = T_DIR, first = self.root, size = 0 }
+ for i in 0..parts:len() do
+ if ent.type != T_DIR return :nil
+ const found = self:dir_lookup(cur, parts[i])
+ if found == :nil return :nil
+ ent = found
+ cur = found.first
end
- return -1
+ return ent
end,
- dir_alloc = fn(self) do
- for i in 0..DIR_ENTRIES do
- disk_read(dir_lba(i), self.buf)
- if mem_read(self.buf + dir_off(i), 1) == 0 return i
+ resolve_parent = fn(self, path) do
+ const parts = split_path(path)
+ if parts:len() == 0 return :nil
+ let pdir = self.root
+ for i in 0..(parts:len() - 1) do
+ const found = self:dir_lookup(pdir, parts[i])
+ if found == :nil or found.type != T_DIR return :nil
+ pdir = found.first
end
- return -1
+ return { dir = pdir, name = parts[parts:len() - 1] }
end,
mount = fn(self) do
@@ -106,10 +215,13 @@ global fs = {
self.total = disk_sectors()
if self.total == 0 do self.ready = :false return :nil end
disk_read(SB_LBA, self.buf)
- self.ready = if mem_read(self.buf, 4) == MAGIC :true else :false
+ if mem_read(self.buf, 4) == MAGIC do
+ self.root = mem_read(self.buf + 8, 2)
+ self.ready = :true
+ end
+ else do self.ready = :false end
end,
- # ready is a field so the gate cant be called `ready`, this complains if theres no fs
ensure = fn(self) do
if self.ready return :true
puts("no filesystem, run mkfs first\n")
@@ -119,28 +231,44 @@ global fs = {
format = fn(self) do
if disk_sectors() == 0 do puts("mkfs: no disk\n") return :nil end
mem_zero(self.buf, BLOCK)
+ for s in 0..FAT_SECTORS do disk_write(FAT_LBA + s, self.buf) end
+ const root = self:fat_alloc()
+ self:fat_set(root, END)
+ mem_zero(self.buf, BLOCK)
+ disk_write(root, self.buf)
+ self.root = root
+ mem_zero(self.buf, BLOCK)
mem_write(self.buf, MAGIC, 4)
mem_write(self.buf + 4, self.total, 4)
+ mem_write(self.buf + 8, root, 2)
disk_write(SB_LBA, self.buf)
- mem_zero(self.buf, BLOCK)
- for s in 0..DIR_SECTORS do disk_write(DIR_LBA + s, self.buf) end
- for s in 0..FAT_SECTORS do disk_write(FAT_LBA + s, self.buf) end
self.ready = :true
puts("formatted #{self.total} sectors, #{(self.total shr 1)} KiB\n")
end,
- # silent core of write, returns 1 ok / 0 fail
- store = fn(self, name, text) do
+ mkdir = fn(self, path) do
+ if self:ensure() == :false return :nil
+ const pp = self:resolve_parent(path)
+ if pp == :nil do puts("mkdir: #{path}: bad path\n") return :nil end
+ if pp.name:len() > NAME_MAX do puts("name too long (max #{NAME_MAX})\n") return :nil end
+ if self:dir_lookup(pp.dir, pp.name) != :nil do puts("mkdir: #{pp.name}: already exists\n") return :nil end
+ const nb = self:fat_alloc()
+ if nb == 0 do puts("disk full\n") return :nil end
+ self:fat_set(nb, END)
+ mem_zero(self.buf, BLOCK)
+ disk_write(nb, self.buf)
+ self:dir_set(pp.dir, pp.name, T_DIR, nb, 0)
+ end,
+
+ store = fn(self, path, text) do
if self:ensure() == :false return 0
- if name:len() > NAME_MAX do puts("name too long (max #{NAME_MAX})\n") return 0 end
- let idx = self:find(name)
- if idx >= 0 do
- disk_read(dir_lba(idx), self.buf)
- self:free_chain(mem_read(self.buf + dir_off(idx) + 2, 2))
- end
- else do
- idx = self:dir_alloc()
- if idx < 0 do puts("directory full\n") return 0 end
+ const pp = self:resolve_parent(path)
+ if pp == :nil do puts("#{path}: bad path\n") return 0 end
+ if pp.name:len() > NAME_MAX do puts("name too long (max #{NAME_MAX})\n") return 0 end
+ const existing = self:dir_lookup(pp.dir, pp.name)
+ if existing != :nil do
+ if existing.type == T_DIR do puts("#{pp.name}: is a directory\n") return 0 end
+ self:free_chain(existing.first)
end
const size = text:len()
@@ -150,7 +278,7 @@ global fs = {
while written < size do
const block = self:fat_alloc()
if block == 0 do puts("disk full\n") return 0 end
- self:fat_set(block, END) # reserve
+ self:fat_set(block, END)
if first == 0 do first = block end
else do self:fat_set(prev, block) end
mem_zero(self.buf, BLOCK)
@@ -163,40 +291,26 @@ global fs = {
disk_write(block, self.buf)
prev = block
end
-
- disk_read(dir_lba(idx), self.buf)
- const base = self.buf + dir_off(idx)
- mem_write(base, 1, 1)
- mem_write(base + 1, name:len(), 1)
- mem_write(base + 2, first, 2)
- mem_write(base + 4, size, 4)
- for k in 0..name:len() do mem_write(base + 8 + k, name[k]:ascii(), 1) end
- disk_write(dir_lba(idx), self.buf)
+ self:dir_set(pp.dir, pp.name, T_FILE, first, size)
return 1
end,
- # write a file, gee i wonder what a function called `write` does
- write = fn(self, name, text) do
- if self:store(name, text) == 1 puts("wrote #{name} (#{text:len()} bytes)\n")
+ write = fn(self, path, text) do
+ if self:store(path, text) == 1 puts("wrote #{path} (#{text:len()} bytes)\n")
end,
- size = fn(self, name) do
- const idx = self:find(name)
- if idx < 0 return -1
- disk_read(dir_lba(idx), self.buf)
- return mem_read(self.buf + dir_off(idx) + 4, 4)
+ size = fn(self, path) do
+ const ent = self:resolve(path)
+ if ent == :nil or ent.type == T_DIR return -1
+ return ent.size
end,
- # read up to len bytes from offset, returned as a string (shorter at eof)
- read_bytes = fn(self, name, offset, len) do
- const idx = self:find(name)
- if idx < 0 return ""
- disk_read(dir_lba(idx), self.buf)
- const base = self.buf + dir_off(idx)
- let block = mem_read(base + 2, 2)
- const size = mem_read(base + 4, 4)
+ read_bytes = fn(self, path, offset, len) do
+ const ent = self:resolve(path)
+ if ent == :nil or ent.type == T_DIR return ""
+ let block = ent.first
let stop = offset + len
- if stop > size do stop = size end
+ if stop > ent.size do stop = ent.size end
let pos = 0
let out = ""
while (block != 0 and block != END and pos < stop) do
@@ -216,17 +330,16 @@ global fs = {
return out
end,
- cat = fn(self, name) do
+ cat = fn(self, path) do
if self:ensure() == :false return :nil
- const idx = self:find(name)
- if idx < 0 do puts("cat: #{name}: no such file\n") return :nil end
- disk_read(dir_lba(idx), self.buf)
- const base = self.buf + dir_off(idx)
- let block = mem_read(base + 2, 2)
- let remaining = mem_read(base + 4, 4)
+ const ent = self:resolve(path)
+ if ent == :nil do puts("cat: #{path}: no such file\n") return :nil end
+ if ent.type == T_DIR do puts("cat: #{path}: is a directory\n") return :nil end
+ let block = ent.first
+ let remaining = ent.size
let out = ""
while (block != 0 and block != END and remaining > 0) do
- const nxt = self:fat_get(block) # grab next before disk_read reuses self.buf
+ const nxt = self:fat_get(block)
disk_read(block, self.buf)
const chunk = if remaining > BLOCK BLOCK else remaining
for k in 0..chunk do out = out ~ string.of_ascii(mem_read(self.buf + k, 1)) end
@@ -237,31 +350,49 @@ global fs = {
puts("\n")
end,
- ls = fn(self) do
- if self:ensure() == :false return :nil
+ ls = fn(self, path) do
+ if self:ensure() == :false return -1
+ const ent = self:resolve(path)
+ if ent == :nil do puts("ls: #{path}: no such file\n") return -1 end
+ if ent.type != T_DIR do puts(" #{path} #{ent.size} bytes\n") return -1 end
let count = 0
- for i in 0..DIR_ENTRIES do
- disk_read(dir_lba(i), self.buf)
- const base = self.buf + dir_off(i)
- if mem_read(base, 1) == 1 do
- puts(" #{self:name_at(base, mem_read(base + 1, 1))} #{mem_read(base + 4, 4)} bytes\n")
- count += 1
+ let block = ent.first
+ while (block != 0 and block != END) do
+ const nxt = self:fat_get(block)
+ disk_read(block, self.buf)
+ for slot in 0..ENTS do
+ const eoff = slot * 32
+ const t = mem_read(self.buf + eoff, 1)
+ if t != 0 do
+ const nm = self:ent_name(eoff)
+ if t == T_DIR do puts(" #{nm}/\n") end
+ else do puts(" #{nm} #{mem_read(self.buf + eoff + 4, 4)} bytes\n") end
+ count += 1
+ end
end
+ block = nxt
end
- if count == 0 puts(" (empty)\n")
+ return count
end,
- rm = fn(self, name) do
+ rm = fn(self, path) do
if self:ensure() == :false return :nil
- const idx = self:find(name)
- if idx < 0 do puts("rm: #{name}: no such file\n") return :nil end
- disk_read(dir_lba(idx), self.buf)
- const base = self.buf + dir_off(idx)
- self:free_chain(mem_read(base + 2, 2))
- disk_read(dir_lba(idx), self.buf)
- mem_write(self.buf + dir_off(idx), 0, 1)
- disk_write(dir_lba(idx), self.buf)
- puts("removed #{name}\n")
+ const pp = self:resolve_parent(path)
+ if pp == :nil do puts("rm: #{path}: no such file\n") return :nil end
+ const ent = self:dir_lookup(pp.dir, pp.name)
+ if ent == :nil do puts("rm: #{path}: no such file\n") return :nil end
+ if ent.type == T_DIR and self:dir_empty(ent.first) == :false do
+ puts("rm: #{pp.name}: directory not empty\n")
+ return :nil
+ end
+ self:free_chain(ent.first)
+ self:dir_clear(pp.dir, pp.name)
+ puts("removed #{pp.name}\n")
+ end,
+
+ is_dir = fn(self, path) do
+ const ent = self:resolve(path)
+ return ent != :nil and ent.type == T_DIR
end,
}
diff --git a/kernel/input.rv b/kernel/input.rv
index fe25c81..7e0d156 100644
--- a/kernel/input.rv
+++ b/kernel/input.rv
@@ -1,7 +1,7 @@
# keyboard scancode table
# ts is not a good way to write a driver lmao
+
const KBD = " 1234567890-= qwertyuiop[] asdfghjkl;'` \\zxcvbnm,./ "
-# same layout, shifted. index 42 (left shift) / 54 (right shift) stay blank
const KBD_SHIFT = " !@#$%^&*()_+ QWERTYUIOP{} ASDFGHJKL:\"~ |ZXCVBNM<>? "
# 8259 pic
@@ -25,11 +25,8 @@ end
# she stroke my keys till i handle
# no fr this is just a key stroke handler
-global shell = { line = "", shift = :false }
+global shell = { line = "", shift = :false, ext = :false }
-# decode one scancode to a character string, or :nil for releases / modifiers /
-# unmapped keys. this is the only scancode decoder now, used by both the shell
-# and the read() syscall
fn decode_key(code) do
if code == 0x2a or code == 0x36 do shell.shift = :true return :nil end # shift down
if code == 0xaa or code == 0xb6 do shell.shift = :false return :nil end # shift up
@@ -44,12 +41,24 @@ fn decode_key(code) do
end
global on_key = fn(code) do
+ if code == 0xe0 do shell.ext = :true return :nil end
+ if shell.ext do
+ shell.ext = :false
+ let final = 0
+ if code == 0x48 do final = 65 end # up
+ else if code == 0x50 do final = 66 end # down
+ else if code == 0x4d do final = 67 end # right
+ else if code == 0x4b do final = 68 end # left
+ if final != 0 do
+ stdin_deliver(27)
+ stdin_deliver(91)
+ stdin_deliver(final)
+ end
+ return :nil
+ end
const ch = decode_key(code)
if ch == :nil return :nil
- # a process blocked in read() gets first dibs, if it takes the byte, the shell
- # doesnt see it (the process echoes its own input via write())
if stdin_deliver(ch:ascii()) return :nil
- # otherwise the shells line editor
if ch == "\n" do puts("\n") run(shell.line) shell.line = "" return :nil end
if ch == "\x08" do
if shell.line:len() > 0 do shell.line = shell.line:sub(0, shell.line:len() - 1) puts("\x08 \x08") end
diff --git a/kernel/proc.rv b/kernel/proc.rv
index f76c511..2f8bb2f 100644
--- a/kernel/proc.rv
+++ b/kernel/proc.rv
@@ -2,8 +2,9 @@
const PT_LOAD = 1
const PAGE_FLAGS = 6 # write | user
+const ARG_VADDR = 0x50000000 # where we map the argument string in the new space
-fn load_elf(base) do
+fn load_elf(base, arg) do
if mem_read(base, 4) != 0x464c457f do
puts("not an ELF\n")
return :nil
@@ -34,14 +35,24 @@ fn load_elf(base) do
end
end
const stack_top = 0x7fff0000
- as_map(space, stack_top - 4096, frame_alloc(), PAGE_FLAGS)
- proc_run(space, entry, stack_top)
+ for p in 0..4 do as_map(space, stack_top - (p + 1) * 4096, frame_alloc(), PAGE_FLAGS) end
+ let arg_ptr = 0
+ const arg_len = arg:len()
+ if arg_len > 0 do
+ const argframe = frame_alloc()
+ mem_zero(argframe, 4096)
+ for k in 0..arg_len do mem_write(argframe + k, arg[k]:ascii(), 1) end
+ as_map(space, ARG_VADDR, argframe, PAGE_FLAGS)
+ arg_ptr = ARG_VADDR
+ end
+ proc_run(space, entry, stack_top, arg_ptr, arg_len)
end
-# launch a program by name, :true if one matched
-fn run_prog(name) do
+fn run_prog(name, arg) do
+ let n = name
+ if n:starts_with?("/bin/") n = n[5..]
for i in 0..prog_count() do
- if prog_name(i) == name do load_elf(prog_phys(i)) return :true end
+ if prog_name(i) == n do load_elf(prog_phys(i), arg) return :true end
end
puts("no such program: #{name}\n")
return :false
diff --git a/kernel/seed.rv b/kernel/seed.rv
index 72f0efd..8c0d9b7 100644
--- a/kernel/seed.rv
+++ b/kernel/seed.rv
@@ -1,5 +1,10 @@
# js dw about this ok?
fn seed_fs() do
- fs:write("i have big balls.txt", "dude my balls are actually massive")
- fs:write("balls.txt", "its lowk insane how big my balls are")
+ fs:mkdir("/usr")
+ fs:mkdir("/etc")
+ fs:mkdir("/home")
+
+ fs:store("/etc/motd", "welcome to revel")
+ fs:store("/home/balls.txt", "its lowk insane how big my balls are")
+ fs:store("/home/i have big balls.txt", "dude my balls are actually massive")
end
diff --git a/kernel/shell.rv b/kernel/shell.rv
index f72387a..40930c5 100644
--- a/kernel/shell.rv
+++ b/kernel/shell.rv
@@ -19,10 +19,14 @@ fn cmd_help() do
help_row("zoom in|out", "scale the font")
help_row("echo <text>", "print it back")
puts("\n files\n")
- help_row("ls", "list files")
- help_row("cat <name>", "print a file")
- help_row("write <name> <text>", "create or overwrite")
- help_row("rm <name>", "delete a file")
+ help_row("ls [path]", "list a directory")
+ help_row("cd <path>", "change directory")
+ help_row("pwd", "print working dir")
+ help_row("mkdir <path>", "make a directory")
+ help_row("cat <path>", "print a file")
+ help_row("edit <path>", "line editor, . to save")
+ help_row("write <path> <text>", "create or overwrite")
+ help_row("rm <path>", "delete a file or empty dir")
help_row("mkfs", "format the disk")
puts("\n programs\n")
help_row("run", "list programs")
@@ -34,7 +38,7 @@ fn welcome() do
let fsline = "no filesystem"
if fs.ready do fsline = "revelfs mounted" end
puts("\n")
- puts(" a tiny os whose brain lives in revo, zig only touches the metal\n")
+ puts(" kernel written in revo - by an alcoholic\n")
puts(" #{(mem_total() shr 20)} MiB ram | #{fsline} | #{prog_count()} programs\n")
puts(" type help to see what it does\n\n% ")
end
@@ -43,7 +47,6 @@ fn arg_name(line, start) do
let quoted = :false
let begin = start
if (begin < line:len() and line[begin] == "\"") do begin += 1 quoted = :true end
- # scan to the end of the name, a closing quote or a space depending on how it opened
let i = begin
if quoted do
while (i < line:len() and line[i] != "\"") do i += 1 end
@@ -52,29 +55,103 @@ fn arg_name(line, start) do
while (i < line:len() and line[i] != " ") do i += 1 end
end
const name = line[begin..i]
- # step past the closing quote and the separating space so rest points at the text
let rest = i
if (quoted and rest < line:len()) do rest += 1 end
if (rest < line:len() and line[rest] == " ") do rest += 1 end
{ name = name, rest = rest }
end
+global wd = { path = "/" }
+
+global editor = { active = :false, path = "", buf = "" }
+
+fn editor_start(path) do
+ editor.active = :true
+ editor.path = path
+ editor.buf = ""
+ const sz = fs:size(path)
+ if sz > 0 do
+ editor.buf = fs:read_bytes(path, 0, sz)
+ puts(editor.buf)
+ end
+ puts("-- editing #{path}, a single . on a line saves and exits --\n")
+end
+
+fn editor_line(line) do
+ if line == "." do
+ fs:store(editor.path, editor.buf)
+ editor.active = :false
+ puts("saved #{editor.path} (#{editor.buf:len()} bytes)\n% ")
+ end
+ else do editor.buf = editor.buf ~ line ~ "\n" end
+end
+
+fn cmd_edit(line) do
+ const a = arg_name(line, 5)
+ if a.name:len() == 0 do puts("usage: edit <path>\n% ") return :nil end
+ editor_start(abspath(a.name))
+end
+
+fn abspath(arg) do
+ if arg:len() == 0 return wd.path
+ let p = arg
+ if arg[0] != "/" do
+ if wd.path == "/" do p = "/#{arg}" end
+ else do p = "#{wd.path}/#{arg}" end
+ end
+ while (p:len() > 1 and p[p:len() - 1] == "/") do p = p[0..(p:len() - 1)] end
+ return p
+end
+
+fn parent_of(path) do
+ if path == "/" return "/"
+ let i = path:len() - 1
+ while (i > 0 and path[i] != "/") do i -= 1 end
+ if i == 0 return "/"
+ return path[0..i]
+end
+
+fn cmd_cd(words) do
+ const arg = if words:len() > 1 words[1] else "/"
+ const target = if arg == ".." parent_of(wd.path) else abspath(arg)
+ if target == "/proc" or target == "/bin" or fs:is_dir(target) do wd.path = target end
+ else do puts("cd: #{arg}: not a directory\n") end
+end
+
+fn cmd_pwd() puts("#{wd.path}\n")
+
+fn cmd_ls(words) do
+ const target = if words:len() > 1 abspath(words[1]) else wd.path
+ if vfs_owns(target) do vfs_ls(target) return :nil end
+ const n = fs:ls(target)
+ # the virtual mounts arent real dir entries, overlay them on the root listing
+ if target == "/" do puts(" bin/\n proc/\n") end
+ else if n == 0 do puts(" (empty)\n") end
+end
+
+fn cmd_mkdir(words) do
+ if words:len() < 2 do puts("usage: mkdir <name>\n") return :nil end
+ fs:mkdir(abspath(words[1]))
+end
+
fn cmd_write(line) do
const a = arg_name(line, 6)
if a.name:len() == 0 do puts("usage: write <name> <text>\n") return :nil end
- fs:write(a.name, line[a.rest..])
+ fs:write(abspath(a.name), line[a.rest..])
end
fn cmd_cat(line) do
const a = arg_name(line, 4)
if a.name:len() == 0 do puts("usage: cat <name>\n") return :nil end
- fs:cat(a.name)
+ const p = abspath(a.name)
+ if vfs_owns(p) do vfs_cat(p) return :nil end
+ fs:cat(p)
end
fn cmd_rm(line) do
const a = arg_name(line, 3)
if a.name:len() == 0 do puts("usage: rm <name>\n") return :nil end
- fs:rm(a.name)
+ fs:rm(abspath(a.name))
end
fn zoom_by(delta) do
@@ -136,12 +213,18 @@ fn cmd_uptime() puts("#{math.floor(uptime() / 100)}s (#{uptime()} ticks)\n")
fn cmd_ps() puts("#{proc_count()} process(es) running\n")
fn run(line) do
+ if editor.active do editor_line(line) return :nil end
if line:trim():len() == 0 do puts("% ") return :nil end
const words = line:split(" ")
const cmd = words[0]
+ if cmd == "edit" do cmd_edit(line) return :nil end
if cmd == "run" do
if words:len() <= 1 do list_progs() puts("% ") return :nil end
- if run_prog(words[1]) == :true return :nil
+ const prog = words[1]
+ # everything after "run <prog> " is the programs argument string
+ const start = 4 + prog:len() + 1
+ const arg = if line:len() > start line[start..] else ""
+ if run_prog(prog, arg) == :true return :nil
puts("% ")
return :nil
end
@@ -154,10 +237,13 @@ fn run(line) do
| "ps" => cmd_ps()
| "zoom" => cmd_zoom(words)
| "mkfs" => fs:format()
- | "ls" => fs:ls()
+ | "ls" => cmd_ls(words)
| "cat" => cmd_cat(line)
| "write" => cmd_write(line)
| "rm" => cmd_rm(line)
+ | "mkdir" => cmd_mkdir(words)
+ | "cd" => cmd_cd(words)
+ | "pwd" => cmd_pwd()
| "echo" => if line:len() > 5 puts("#{line[5..]}\n") else puts("\n")
| _ => puts("unknown command: #{cmd}\n")
puts("% ")
diff --git a/kernel/vfs.rv b/kernel/vfs.rv
new file mode 100644
index 0000000..5d2d1de
--- /dev/null
+++ b/kernel/vfs.rv
@@ -0,0 +1,57 @@
+# shitty vfs implementation
+# proc and /bin arent on disk, theyre completely virtual
+
+const TASK_STATES = { "free", "ready", "dead", "blocked" }
+
+fn vfs_owns(path) do
+ if path == "/proc" or path:starts_with?("/proc/") return :true
+ if path == "/bin" or path:starts_with?("/bin/") return :true
+ return :false
+end
+
+fn parse_pid(s) do
+ if s:len() == 0 return -1
+ let n = 0
+ for i in 0..s:len() do
+ const c = s[i]:ascii()
+ if c < 48 or c > 57 return -1
+ n = n * 10 + (c - 48)
+ end
+ return n
+end
+
+fn vfs_ls(path) do
+ if path == "/proc" do
+ puts(" uptime\n meminfo\n")
+ for i in 0..task_max() do
+ if task_state(i) != 0 puts(" #{i}\n")
+ end
+ end
+ else if path == "/bin" do
+ for i in 0..prog_count() do puts(" #{prog_name(i)}\n") end
+ end
+ else do puts("ls: #{path}: no such directory\n") end
+end
+
+fn vfs_cat(path) do
+ if path == "/proc/uptime" do puts("#{math.floor(uptime() / 100)}s\n") return :nil end
+ if path == "/proc/meminfo" do puts("#{(mem_free() shr 20)} MiB free / #{(mem_total() shr 20)} MiB total\n") return :nil end
+ if path:starts_with?("/proc/") do
+ const pid = parse_pid(path[6..])
+ if pid >= 0 and pid < task_max() and task_state(pid) != 0 do
+ puts("pid #{pid}: #{TASK_STATES[task_state(pid)]}\n")
+ return :nil
+ end
+ puts("cat: #{path}: no such file\n")
+ return :nil
+ end
+ if path:starts_with?("/bin/") do
+ const nm = path[5..]
+ for i in 0..prog_count() do
+ if prog_name(i) == nm do puts("#{nm}: elf program, #{prog_size(i)} bytes\n") return :nil end
+ end
+ puts("cat: #{path}: no such file\n")
+ return :nil
+ end
+ puts("cat: #{path}: no such file\n")
+end
diff --git a/kernel/vmm.rv b/kernel/vmm.rv
index ece5682..a4c0eea 100644
--- a/kernel/vmm.rv
+++ b/kernel/vmm.rv
@@ -4,7 +4,6 @@ const PTE_ADDR = 0x000FFFFFFFFFF000
fn pte_idx(virt, level) (virt shr (12 + 9 * level)) band 0x1ff
-# next level table phys under entry `index` of table `table`, creating it if absent
fn next_table(table, index) do
const slot = table + index * 8
const entry = mem_read(slot, 8)
diff --git a/user/calc.c b/user/calc.c
index 2d0e3f8..cf5b1d2 100644
--- a/user/calc.c
+++ b/user/calc.c
@@ -23,6 +23,7 @@ void _start(void)
int valid = 1;
int saw_number = 0;
int position = 0;
+
while (position < length)
{
char character = line[position];
@@ -31,11 +32,13 @@ void _start(void)
position++;
continue;
}
+
if (is_digit(character))
{
long value = 0;
while (position < length && is_digit(line[position]))
value = value * 10 + (line[position++] - '0');
+
if (operator == '+')
accumulator += value;
else if (operator == '-')
@@ -52,6 +55,7 @@ void _start(void)
}
accumulator = (operator == '/') ? accumulator / value : accumulator % value;
}
+
saw_number = 1;
}
else if (character == '+' || character == '-' || character == '*'
@@ -67,6 +71,7 @@ void _start(void)
break;
}
}
+
if (valid && saw_number)
{
put("= ");
@@ -74,5 +79,6 @@ void _start(void)
put("\n");
}
}
+
sys_exit(0);
}
diff --git a/user/hexview.c b/user/hexview.c
index 6354338..c211b44 100644
--- a/user/hexview.c
+++ b/user/hexview.c
@@ -19,12 +19,22 @@ static void put_hex32(unsigned long value)
#define PAGE 256
#define COLS 16
-void _start(void)
+void _start(const char* arg, unsigned long arglen)
{
- put("hexview, type a filename\nfile: ");
char name[64];
- read_line(name, 64);
- put("\n");
+
+ if (arglen > 0 && arglen < 64)
+ {
+ for (unsigned long i = 0; i < arglen; i++)
+ name[i] = arg[i];
+ name[arglen] = 0;
+ }
+ else
+ {
+ put("hexview, type a filename\nfile: ");
+ read_line(name, 64);
+ put("\n");
+ }
long size = sys_fs_size(name);
if (size < 0)
@@ -35,6 +45,7 @@ void _start(void)
unsigned long offset = 0;
char buffer[PAGE];
+
for (;;)
{
long count = sys_fs_read(name, offset, buffer, PAGE);
@@ -61,6 +72,7 @@ void _start(void)
else
put(" ");
}
+
put(" ");
for (int column = 0; column < COLS && row + column < count; column++)
{
@@ -73,8 +85,18 @@ void _start(void)
put("\n");
}
- put("[n]ext [p]rev [q]uit > ");
+ put("[n]ext [p]rev [q]uit (or up/down arrows) > ");
char key = (char)sys_read();
+ if (key == 27) // ESC
+ {
+ sys_read();
+ char final = (char)sys_read();
+ if (final == 'A')
+ key = 'p';
+ else if (final == 'B')
+ key = 'n';
+ }
+
put("\n");
if (key == 'q')
break;
diff --git a/user/save.c b/user/save.c
new file mode 100644
index 0000000..b3664f4
--- /dev/null
+++ b/user/save.c
@@ -0,0 +1,45 @@
+#include "user.h"
+
+void _start(const char* arg, unsigned long arglen)
+{
+ if (arglen == 0 || arglen >= 64)
+ {
+ put("usage: run save <path>\n");
+ sys_exit(1);
+ }
+
+ char name[64];
+ for (unsigned long i = 0; i < arglen; i++)
+ name[i] = arg[i];
+ name[arglen] = 0;
+
+ put("saving to ");
+ put(name);
+ put(" -- type lines, a single . saves\n");
+
+ char buffer[4096];
+ int length = 0;
+ for (;;)
+ {
+ char line[256];
+ int n = read_line(line, 256);
+ put("\n");
+ if (n == 1 && line[0] == '.')
+ break;
+ for (int i = 0; i < n && length < 4000; i++)
+ buffer[length++] = line[i];
+ if (length < 4000)
+ buffer[length++] = '\n';
+ }
+
+ if (sys_fs_write(name, buffer, (unsigned long)length))
+ {
+ put("saved ");
+ put(name);
+ put("\n");
+ }
+ else
+ put("write failed\n");
+
+ sys_exit(0);
+}
diff --git a/user/user.h b/user/user.h
index 676326b..635dc86 100644
--- a/user/user.h
+++ b/user/user.h
@@ -1,4 +1,4 @@
-// shared syscall stubs + tiny helpers for revel ring 3 programs
+// shared syscall stubs
static long sys_write(const char* buffer, unsigned long length)
{
@@ -37,6 +37,16 @@ static long sys_fs_read(const char* name, unsigned long offset, char* buffer, un
return result;
}
+static long sys_fs_write(const char* name, const char* buffer, unsigned long length)
+{
+ long result;
+ __asm__ volatile("int $0x80"
+ : "=a"(result)
+ : "a"(6L), "D"(name), "S"(buffer), "d"(length)
+ : "memory");
+ return result;
+}
+
static void put(const char* string)
{
unsigned long length = 0;
@@ -78,20 +88,25 @@ static void put_int(long value)
int length = 0;
int negative = value < 0;
unsigned long magnitude = negative ? (unsigned long)(-value) : (unsigned long)value;
+
if (magnitude == 0)
buffer[length++] = '0';
+
while (magnitude > 0)
{
buffer[length++] = (char)('0' + magnitude % 10);
magnitude /= 10;
}
+
if (negative)
buffer[length++] = '-';
+
for (int left = 0, right = length - 1; left < right; left++, right--)
{
char swap = buffer[left];
buffer[left] = buffer[right];
buffer[right] = swap;
}
+
sys_write(buffer, length);
}