From 6f14bec123a27d8e921708b53df989b26f910591 Mon Sep 17 00:00:00 2001 From: cozis Date: Sun, 17 May 2026 14:36:46 +0200 Subject: [PATCH] First commit --- .gitignore | 7 + README.md | 66 +++ examples/example.zig | 53 +++ src/file_system.zig | 416 ++++++++++++++++++ src/io_interface.zig | 993 +++++++++++++++++++++++++++++++++++++++++++ src/main.zig | 53 +++ src/network.zig | 247 +++++++++++ src/node.zig | 383 +++++++++++++++++ src/scheduler.zig | 192 +++++++++ src/simulator.zig | 98 +++++ 10 files changed, 2508 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 examples/example.zig create mode 100644 src/file_system.zig create mode 100644 src/io_interface.zig create mode 100644 src/main.zig create mode 100644 src/network.zig create mode 100644 src/node.zig create mode 100644 src/scheduler.zig create mode 100644 src/simulator.zig diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..29c6bd2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +.zig-cache +zig-out +.codex +.agents +ignored +main +mock_fs_dump diff --git a/README.md b/README.md new file mode 100644 index 0000000..a82331c --- /dev/null +++ b/README.md @@ -0,0 +1,66 @@ +# Zigmulator + +Zigmulator is a Deterministic Simulation Testing framework for Zig. + +It allows you to run one or more zig programs in a controlled environment with the ability to inject arbitrary I/O faults. The zigmulation is fully deterministic, which allows you to replay any scenario. It's ideal for testing distributed systems where bugs lay in the interleavings of different nodes. + +It's still under construction, but the basic idea is that you write your programs in the form: + +```zig +pub fn main(init: std.process.Init) anyerror!void { + // ... +} +``` + +Then import them in the simulation program: + +```zig +const std = @import("std"); +const Io = std.Io; + +const Simulator = @import("zigmulator").Simulator; + +const programA = @import("projectA/main.zig").main; +const programB = @import("projectB/main.zig").main; +const programC = @import("projectC/main.zig").main; + +pub fn main(init: std.process.Init) !void { + + var sim: Simulator = undefined; + sim.init(std.heap.page_allocator, init.io); + defer sim.deinit(); + + // Associate executable names to zig entry functions + try sim.addExecutable("program_a", programA); + try sim.addExecutable("program_b", programB); + try sim.addExecutable("program_c", programC); + + // Now run commands in the form: + // program_name arg1 arg2 arg3 ... + // where program_name is one of the registered functions. + try sim.spawn("program_a", .{}); + try sim.spawn("program_b", .{}); + try sim.spawn("program_c", .{}); + + // Advance the cluster's state by advancing the program's + // states one by one. Exits when all programs have returned + // or failed. + while (sim.scheduleOne()) {} + + std.debug.print("Simulation ended\n", .{}); +} +``` + +## How it works + +Zigmulator implements an in-memory kernel, disk, network and plugs it into your application via the new IO interface (since Zig 0.16). Multiple programs can run in the same simulation, in which case network traffic is routed between their mocks. This is a bruteforce approach but the only one that allows a complete control of the environment the applications are running in. + +To avoid non-determinism of kernel-level scheduling, all simulated programs run in a single kernel thread and are scheduled in userspace. Any time a program performs an I/O operation, it yields to a different program. + +## Limitations + +Since Zigmulator mocks the entire world, the main limitation is in how much of it is mocked and how it differs from the real implementation. This project takes a best-effort approach: the model of the external world is simplified, and applications tested with Zigmulator are expected to only rely on functionality that is mocked. For instance, in the Zigmulator model, each process owns its own machine. If you spawn multiple nodes, each will have its own NIC and disk. + +## Current Status + +Currently Zigmulator is a proof of concept. A basic version of network, disk, scheduling have been implemented and only the write operations have been mocked. diff --git a/examples/example.zig b/examples/example.zig new file mode 100644 index 0000000..369f322 --- /dev/null +++ b/examples/example.zig @@ -0,0 +1,53 @@ +const std = @import("std"); +const Io = std.Io; + +const Simulator = @import("zigmulator").Simulator; + +fn programA(init: std.process.Init) anyerror!void { + var stdout = Io.File.stdout().writerStreaming(init.io, &.{}); + for (0..3) |_| { + try stdout.interface.print("Hello from program A!\n", .{}); + try stdout.interface.flush(); + } +} + +fn programB(init: std.process.Init) anyerror!void { + var stdout = Io.File.stdout().writerStreaming(init.io, &.{}); + for (0..3) |_| { + try stdout.interface.print("Hello from program B!\n", .{}); + try stdout.interface.flush(); + } +} + +fn programC(init: std.process.Init) anyerror!void { + var stdout = Io.File.stdout().writerStreaming(init.io, &.{}); + for (0..3) |_| { + try stdout.interface.print("Hello from program C!\n", .{}); + try stdout.interface.flush(); + } +} + +pub fn main(init: std.process.Init) !void { + var sim: Simulator = undefined; + sim.init(std.heap.page_allocator, init.io); + defer sim.deinit(); + + // Associate executable names to zig entry functions + try sim.addExecutable("program_a", programA); + try sim.addExecutable("program_b", programB); + try sim.addExecutable("program_c", programC); + + // Now run commands in the form: + // program_name arg1 arg2 arg3 ... + // where program_name is one of the registered functions. + try sim.spawn("program_a", .{}); + try sim.spawn("program_b", .{}); + try sim.spawn("program_c", .{}); + + // Advance the cluster's state by advancing the program's + // states one by one. Exits when all programs have returned + // or failed. + while (sim.scheduleOne()) {} + + std.debug.print("Simulation ended\n", .{}); +} diff --git a/src/file_system.zig b/src/file_system.zig new file mode 100644 index 0000000..dff4b29 --- /dev/null +++ b/src/file_system.zig @@ -0,0 +1,416 @@ +const std = @import("std"); + +const FileSystem = @This(); +const Allocator = std.mem.Allocator; + +const ENTITY_NAME_LIMIT = 127; +const MAX_PATH_COMPONENTS = 64; + +const EntityChild = struct { + name: [ENTITY_NAME_LIMIT]u8 = undefined, + name_len: u16 = 0, + addr: *Entity, + + fn init(name: []const u8, addr: *Entity) !EntityChild { + var self: EntityChild = .{ .addr = addr }; + try self.setName(name); + return self; + } + + fn setName(self: *EntityChild, name: []const u8) !void { + if (name.len > self.name.len) + return error.OutOfMemory; + @memcpy(self.name[0..name.len], name); + self.name_len = @intCast(name.len); + } + + fn getName(self: *const EntityChild) []const u8 { + return self.name[0..self.name_len]; + } +}; + +const Entity = struct { + is_dir : bool, + ref_count: u32 = 0, + children : std.ArrayList(EntityChild) = .empty, + bytes : std.ArrayList(u8) = .empty, + + fn initDir(gpa: Allocator) Allocator.Error!*Entity { + const self = try gpa.create(Entity); + self.* = .{ .is_dir = true }; + return self; + } + + fn initFile(gpa: Allocator) Allocator.Error!*Entity { + const self = try gpa.create(Entity); + self.* = .{ .is_dir = false }; + return self; + } + + fn ref(self: *Entity) void { + self.ref_count += 1; + } + + fn deref(self: *Entity, gpa: Allocator) void { + std.debug.assert(self.ref_count > 0); + self.ref_count -= 1; + if (self.ref_count == 0) { + for (self.children.items) |child| { + child.addr.deref(gpa); + } + self.children.deinit(gpa); + self.bytes.deinit(gpa); + gpa.destroy(self); + } + } + + // Directory only + fn findChildIndex(self: *Entity, name: []const u8) ?usize { + std.debug.assert(self.is_dir); + for (self.children.items, 0..) |child, i| { + if (std.mem.eql(u8, child.getName(), name)) + return i; + } + return null; + } + + // Directory only + fn addChild(self: *Entity, gpa: Allocator, name: []const u8, addr: *Entity) !void { + std.debug.assert(self.is_dir); + if (self.findChildIndex(name) != null) + return error.ExistsAlready; + try self.children.append(gpa, try EntityChild.init(name, addr)); + addr.ref(); + } + + // Directory only + fn findChild(self: *Entity, name: []const u8) ?*EntityChild { + const index = self.findChildIndex(name) orelse return null; + return &self.children.items[index]; + } + + // Directory only + fn removeChild(self: *Entity, gpa: Allocator, name: []const u8) !void { + const index = self.findChildIndex(name) orelse return error.NotFound; + const removed = self.children.swapRemove(index); + removed.addr.deref(gpa); + } +}; + +pub const OpenDir = struct { + entity: *Entity, + cursor: usize = 0, + name: [ENTITY_NAME_LIMIT]u8 = undefined, + + fn init(entity: *Entity) OpenDir { + return .{ .entity = entity }; + } +}; + +pub const OpenFile = struct { + entity: *Entity, + cursor: usize = 0, + + fn init(entity: *Entity) OpenFile { + return .{ .entity = entity }; + } +}; + +pub const ReadDir = struct { + name: []const u8, + is_dir: bool, +}; + +const ParsePathError = error{ + EmptyPath, + NoRootParent, + TooManyComponents, +}; + +const ResolvePathError = error { + ResolutionLimit, + ComponentNotDirectory, + ComponentNotFound, +}; + +const ResolveParent = struct { + parent: *Entity, + basename: []const u8, +}; + +const ResolveParentError = ParsePathError || ResolvePathError; + +pub const DeleteError = ResolveParentError; + +pub const OpenError = error { + IsDirectory, + NotDirectory, +} || ParsePathError || ResolvePathError; + +pub const ReadDirError = error { + NoMoreItems, +}; + +pub const CreateError = error { + ExistsAlready, +} || ResolveParentError || Allocator.Error; + +root: *Entity, + +pub fn init(self: *FileSystem, gpa: Allocator) !void { + self.root = try Entity.initDir(gpa); + self.root.ref(); +} + +pub fn deinit(self: *FileSystem, gpa: Allocator) void { + self.root.deref(gpa); +} + +fn parsePath(path: []const u8, buffer: [][]const u8) ParsePathError![]const []const u8 { + + var count: usize = 0; + var cursor: usize = 0; + + if (path.len == 0) + return ParsePathError.EmptyPath; + + var absolute = false; + if (path[0] == '/') { + absolute = true; + cursor += 1; + } + + while (cursor < path.len) { + + const start = cursor; + while (cursor < path.len and path[cursor] != '/') + cursor += 1; + const component = path[start..cursor]; + + if (cursor < path.len) + cursor += 1; + + if (std.mem.eql(u8, component, ".")) + continue; + + if (std.mem.eql(u8, component, "..")) { + if (count == 0) + return ParsePathError.NoRootParent; + count -= 1; + continue; + } + + if (count == buffer.len) + return ParsePathError.TooManyComponents; + buffer[count] = component; + count += 1; + } + + return buffer[0..count]; +} + +fn resolvePath( + self : *FileSystem, + components: []const []const u8, + root : ?*Entity, + buffer : []*Entity +) ResolvePathError![]const *Entity { + + var count: usize = 1; + buffer[0] = root orelse self.root; + + for (components) |component| { + if (count == buffer.len) + return ResolvePathError.ResolutionLimit; + if (!buffer[count-1].is_dir) + return ResolvePathError.ComponentNotDirectory; + const child = buffer[count-1].findChild(component) orelse return ResolvePathError.ComponentNotFound; + buffer[count] = child.addr; + count += 1; + } + + return buffer[0..count]; +} + +fn resolveParent(self: *FileSystem, path: []const u8, root: ?*Entity) !ResolveParent { + var component_buffer: [MAX_PATH_COMPONENTS][]const u8 = undefined; + var resolve_buffer: [MAX_PATH_COMPONENTS]*Entity = undefined; + + const components = try parsePath(path, &component_buffer); + if (components.len == 0) + return ResolveParentError.EmptyPath; // TODO: This error may not be right + + const path_to_parent = try self.resolvePath(components[0..components.len-1], root, &resolve_buffer); + const parent = path_to_parent[path_to_parent.len-1]; + if (!parent.is_dir) + return ResolveParentError.ComponentNotDirectory; + + return .{ + .parent = parent, + .basename = components[components.len-1], + }; +} + +fn createAny(self: *FileSystem, path: []const u8, root_dir: ?*OpenDir, is_dir: bool, gpa: Allocator) CreateError!void { + + const result = try self.resolveParent(path, if (root_dir) |r| r.entity else null); + + const entity = try if (is_dir) Entity.initDir(gpa) else Entity.initFile(gpa); + errdefer gpa.destroy(entity); + + try result.parent.addChild(gpa, result.basename, entity); +} + +pub fn createFile(self: *FileSystem, path: []const u8, root_dir: ?*OpenDir, gpa: Allocator) CreateError!void { + return self.createAny(path, root_dir, false, gpa); +} + +pub fn createDir(self: *FileSystem, path: []const u8, root_dir: ?*OpenDir, gpa: Allocator) CreateError!void { + return self.createAny(path, root_dir, true, gpa); +} + +pub fn deleteAny(self: *FileSystem, path: []const u8, root_dir: ?*OpenDir, gpa: Allocator) DeleteError!void { + const result = try self.resolveParent(path, if (root_dir) |r| r.entity else null); + try result.parent.removeChild(gpa, result.basename); +} + +fn openAny( + self : *FileSystem, + path : []const u8, + root_dir : ?*OpenDir, + open_dir : *OpenDir, + open_file: *OpenFile +) !bool { + var component_buffer: [MAX_PATH_COMPONENTS][]const u8 = undefined; + var resolve_buffer: [MAX_PATH_COMPONENTS]*Entity = undefined; + + const components = try parsePath(path, &component_buffer); + if (components.len == 0) + return OpenError.EmptyPath; // TODO: This error is not right + + const resolved = try self.resolvePath(components, if (root_dir) |r| r.entity else null, &resolve_buffer); + if (resolved.len == 0) + return OpenError.EmptyPath; // TODO: This error is not right + + const entity = resolved[resolved.len-1]; + if (entity.is_dir) { + open_dir.* = .init(entity); + } else { + open_file.* = .init(entity); + } + + return entity.is_dir; +} + +pub fn openDir( + self : *FileSystem, + path : []const u8, + root_dir: ?*OpenDir, + open_dir: *OpenDir +) OpenError!void { + + var dummy: OpenFile = undefined; + const is_dir = try self.openAny(path, root_dir, open_dir, &dummy); + if (!is_dir) { + return OpenError.NotDirectory; + } + open_dir.entity.ref(); +} + +pub fn openFile( + self : *FileSystem, + path : []const u8, + root_dir : ?*OpenDir, + open_file: *OpenFile +) OpenError!void { + + var dummy: OpenDir = undefined; + const is_dir = try self.openAny(path, root_dir, &dummy, open_file); + if (is_dir) { + return OpenError.IsDirectory; + } + open_file.entity.ref(); +} + +pub fn closeDir(self: *FileSystem, open_dir: *OpenDir, gpa: Allocator) void { + _ = self; + open_dir.entity.deref(gpa); +} + +pub fn closeFile(self: *FileSystem, open_file: *OpenFile, gpa: Allocator) void { + _ = self; + open_file.entity.deref(gpa); +} + +pub fn readDir(self: *FileSystem, open_dir: *OpenDir) ReadDirError!ReadDir { + _ = self; + + if (open_dir.cursor == 0) { + open_dir.cursor += 1; + return .{ + .name = ".", + .is_dir = true + }; + } + + if (open_dir.cursor == 1) { + open_dir.cursor += 1; + return .{ + .name = "..", + .is_dir = true + }; + } + + const index = open_dir.cursor - 2; + if (index == open_dir.entity.children.items.len) + return ReadDirError.NoMoreItems; + + const child = open_dir.entity.children.items[index]; + const name = child.getName(); + @memcpy(open_dir.name[0..name.len], name); + + open_dir.cursor += 1; + return .{ + .name = open_dir.name[0..name.len], + .is_dir = child.addr.is_dir, + }; +} + +pub fn readFile(self: *FileSystem, open_file: *OpenFile, offset_maybe: ?usize, target: []u8) usize { + _ = self; + const offset = offset_maybe orelse open_file.cursor; + const source = open_file.entity.bytes.items[offset..]; + const num = @min(source.len, target.len); + @memcpy(target[0..num], source[0..num]); + if (offset_maybe == null) + open_file.cursor += num; + return num; +} + +pub fn writeFile( + self : *FileSystem, + open_file : *OpenFile, + gpa : Allocator, + offset_maybe: ?usize, + source : []const u8 +) Allocator.Error!void { + _ = self; + + const offset = offset_maybe orelse open_file.cursor; + + const required_capacity = offset + source.len; + const current_capacity = open_file.entity.bytes.items.len; + if (required_capacity > current_capacity) + try open_file.entity.bytes.appendNTimes(gpa, 0, required_capacity - current_capacity); + + const target = open_file.entity.bytes.items[offset..required_capacity]; + @memcpy(target, source); + + if (offset_maybe == null) + open_file.cursor = required_capacity; +} + +pub fn fileSize(self: *FileSystem, open_file: *OpenFile) usize { + _ = self; + return open_file.entity.bytes.items.len; +} diff --git a/src/io_interface.zig b/src/io_interface.zig new file mode 100644 index 0000000..d4cdbef --- /dev/null +++ b/src/io_interface.zig @@ -0,0 +1,993 @@ +const std = @import("std"); +const Node = @import("node.zig"); + +const Io = std.Io; +const net = Io.net; + +const Dir = Io.Dir; +const File = Io.File; +const Group = Io.Group; +const AnyFuture = Io.AnyFuture; +const ConcurrentError = Io.ConcurrentError; +const CancelProtection = Io.CancelProtection; +const Cancelable = Io.Cancelable; +const Timeout = Io.Timeout; +const Operation = Io.Operation; +const Batch = Io.Batch; +const Terminal = Io.Terminal; +const LockedStderr = Io.LockedStderr; +const Clock = Io.Clock; +const Timestamp = Io.Timestamp; +const Duration = Io.Duration; +const RandomSecureError = Io.RandomSecureError; +const Queue = Io.Queue; +const Allocator = std.mem.Allocator; + +pub fn buildIOInterfaceForNode(node: *Node) Io { + return .{ + .userdata = node, + .vtable = &.{ + .crashHandler = crashHandler, + + .async = async, + .concurrent = concurrent, + .await = await, + .cancel = cancel, + + .groupAsync = groupAsync, + .groupConcurrent = groupConcurrent, + .groupAwait = groupAwait, + .groupCancel = groupCancel, + + .recancel = recancel, + .swapCancelProtection = swapCancelProtection, + .checkCancel = checkCancel, + + .futexWait = futexWait, + .futexWaitUncancelable = futexWaitUncancelable, + .futexWake = futexWake, + + .operate = operate, + .batchAwaitAsync = batchAwaitAsync, + .batchAwaitConcurrent = batchAwaitConcurrent, + .batchCancel = batchCancel, + + .dirCreateDir = dirCreateDir, + .dirCreateDirPath = dirCreateDirPath, + .dirCreateDirPathOpen = dirCreateDirPathOpen, + .dirOpenDir = dirOpenDir, + .dirStat = dirStat, + .dirStatFile = dirStatFile, + .dirAccess = dirAccess, + .dirCreateFile = dirCreateFile, + .dirCreateFileAtomic = dirCreateFileAtomic, + .dirOpenFile = dirOpenFile, + .dirClose = dirClose, + .dirRead = dirRead, + .dirRealPath = dirRealPath, + .dirRealPathFile = dirRealPathFile, + .dirDeleteFile = dirDeleteFile, + .dirDeleteDir = dirDeleteDir, + .dirRename = dirRename, + .dirRenamePreserve = dirRenamePreserve, + .dirSymLink = dirSymLink, + .dirReadLink = dirReadLink, + .dirSetOwner = dirSetOwner, + .dirSetFileOwner = dirSetFileOwner, + .dirSetPermissions = dirSetPermissions, + .dirSetFilePermissions = dirSetFilePermissions, + .dirSetTimestamps = dirSetTimestamps, + .dirHardLink = dirHardLink, + + .fileStat = fileStat, + .fileLength = fileLength, + .fileClose = fileClose, + .fileWritePositional = fileWritePositional, + .fileWriteFileStreaming = fileWriteFileStreaming, + .fileWriteFilePositional = fileWriteFilePositional, + .fileReadPositional = fileReadPositional, + .fileSeekBy = fileSeekBy, + .fileSeekTo = fileSeekTo, + .fileSync = fileSync, + .fileIsTty = fileIsTty, + .fileEnableAnsiEscapeCodes = fileEnableAnsiEscapeCodes, + .fileSupportsAnsiEscapeCodes = fileSupportsAnsiEscapeCodes, + .fileSetLength = fileSetLength, + .fileSetOwner = fileSetOwner, + .fileSetPermissions = fileSetPermissions, + .fileSetTimestamps = fileSetTimestamps, + .fileLock = fileLock, + .fileTryLock = fileTryLock, + .fileUnlock = fileUnlock, + .fileDowngradeLock = fileDowngradeLock, + .fileRealPath = fileRealPath, + .fileHardLink = fileHardLink, + + .fileMemoryMapCreate = fileMemoryMapCreate, + .fileMemoryMapDestroy = fileMemoryMapDestroy, + .fileMemoryMapSetLength = fileMemoryMapSetLength, + .fileMemoryMapRead = fileMemoryMapRead, + .fileMemoryMapWrite = fileMemoryMapWrite, + + .processExecutableOpen = processExecutableOpen, + .processExecutablePath = processExecutablePath, + .lockStderr = lockStderr, + .tryLockStderr = tryLockStderr, + .unlockStderr = unlockStderr, + .processCurrentPath = processCurrentPath, + .processSetCurrentDir = processSetCurrentDir, + .processSetCurrentPath = processSetCurrentPath, + .processReplace = processReplace, + .processReplacePath = processReplacePath, + .processSpawn = processSpawn, + .processSpawnPath = processSpawnPath, + .childWait = childWait, + .childKill = childKill, + + .progressParentFile = progressParentFile, + + .random = random, + .randomSecure = randomSecure, + + .now = now, + .clockResolution = clockResolution, + .sleep = sleep, + + .netListenIp = netListenIp, + .netAccept = netAccept, + .netBindIp = netBindIp, + .netConnectIp = netConnectIp, + .netListenUnix = netListenUnix, + .netConnectUnix = netConnectUnix, + .netSocketCreatePair = netSocketCreatePair, + .netSend = netSend, + .netRead = netRead, + .netWrite = netWrite, + .netWriteFile = netWriteFile, + .netClose = netClose, + .netShutdown = netShutdown, + .netInterfaceNameResolve = netInterfaceNameResolve, + .netInterfaceName = netInterfaceName, + .netLookup = netLookup, + }, + }; +} + +fn crashHandler(userdata: ?*anyopaque) void { + _ = userdata; + @panic("Not implemented yet"); +} + +fn async( + userdata: ?*anyopaque, + result: []u8, + result_alignment: std.mem.Alignment, + context: []const u8, + context_alignment: std.mem.Alignment, + start: *const fn (context: *const anyopaque, result: *anyopaque) void, +) ?*AnyFuture { + _ = userdata; + _ = result; + _ = result_alignment; + _ = context; + _ = context_alignment; + _ = start; + @panic("Not implemented yet"); +} + +fn concurrent( + userdata: ?*anyopaque, + result_len: usize, + result_alignment: std.mem.Alignment, + context: []const u8, + context_alignment: std.mem.Alignment, + start: *const fn (context: *const anyopaque, result: *anyopaque) void, +) ConcurrentError!*AnyFuture { + _ = userdata; + _ = result_len; + _ = result_alignment; + _ = context; + _ = context_alignment; + _ = start; + @panic("Not implemented yet"); +} + +fn await( + userdata: ?*anyopaque, + future: *AnyFuture, + result: []u8, + result_alignment: std.mem.Alignment, +) void { + _ = userdata; + _ = future; + _ = result; + _ = result_alignment; + @panic("Not implemented yet"); +} + +fn cancel( + userdata: ?*anyopaque, + future: *AnyFuture, + result: []u8, + result_alignment: std.mem.Alignment, +) void { + _ = userdata; + _ = future; + _ = result; + _ = result_alignment; + @panic("Not implemented yet"); +} + +fn groupAsync( + userdata: ?*anyopaque, + type_erased: *Group, + context: []const u8, + context_alignment: std.mem.Alignment, + start: *const fn (context: *const anyopaque) void, +) void { + _ = userdata; + _ = type_erased; + _ = context; + _ = context_alignment; + _ = start; + @panic("Not implemented yet"); +} + +fn groupConcurrent( + userdata: ?*anyopaque, + type_erased: *Group, + context: []const u8, + context_alignment: std.mem.Alignment, + start: *const fn (context: *const anyopaque) void, +) ConcurrentError!void { + _ = userdata; + _ = type_erased; + _ = context; + _ = context_alignment; + _ = start; + @panic("Not implemented yet"); +} + +fn groupAwait(userdata: ?*anyopaque, type_erased: *Group, initial_token: *anyopaque) Cancelable!void { + _ = userdata; + _ = type_erased; + _ = initial_token; + @panic("Not implemented yet"); +} + +fn groupCancel(userdata: ?*anyopaque, type_erased: *Group, initial_token: *anyopaque) void { + _ = userdata; + _ = type_erased; + _ = initial_token; + @panic("Not implemented yet"); +} + +fn recancel(userdata: ?*anyopaque) void { + _ = userdata; + @panic("Not implemented yet"); +} + +fn swapCancelProtection(userdata: ?*anyopaque, new: CancelProtection) CancelProtection { + _ = userdata; + _ = new; + @panic("Not implemented yet"); +} + +fn checkCancel(userdata: ?*anyopaque) Cancelable!void { + _ = userdata; + @panic("Not implemented yet"); +} + +fn futexWait(userdata: ?*anyopaque, ptr: *const u32, expected: u32, timeout: Timeout) Cancelable!void { + _ = userdata; + _ = ptr; + _ = expected; + _ = timeout; + @panic("Not implemented yet"); +} + +fn futexWaitUncancelable(userdata: ?*anyopaque, ptr: *const u32, expected: u32) void { + _ = userdata; + _ = ptr; + _ = expected; + @panic("Not implemented yet"); +} + +fn futexWake(userdata: ?*anyopaque, ptr: *const u32, max_waiters: u32) void { + _ = userdata; + _ = ptr; + _ = max_waiters; + @panic("Not implemented yet"); +} + +fn operate(userdata: ?*anyopaque, operation: Operation) Cancelable!Operation.Result { + const node: *Node = @ptrCast(@alignCast(userdata.?)); + return switch (operation) { + .file_write_streaming => |op| .{ + .file_write_streaming = fileWriteStreaming(node, op), + }, + else => @panic("Not implemented yet"), + }; +} + +fn fileWriteStreaming(node: *Node, op: Operation.FileWriteStreaming) Operation.FileWriteStreaming.Result { + var copied: usize = 0; + copied += writeFile(node, op.file, null, op.header, op.data[0 .. op.data.len - 1]) catch |err| return err; + + const pattern = op.data[op.data.len - 1]; + for (0..op.splat) |_| { + copied += writeFile(node, op.file, null, &.{}, &.{pattern}) catch |err| return err; + } + return copied; +} + +fn writeFile(node: *Node, file: File, offset: ?usize, header: []const u8, data: []const []const u8) Operation.FileWriteStreaming.Error!usize { + return node.writeFile(file.handle, offset, header, data) catch |err| switch (err) { + error.InvalidHandle => error.NotOpenForWriting, + error.OutOfMemory => error.SystemResources, + }; +} + +fn batchAwaitAsync(userdata: ?*anyopaque, batch: *Batch) Cancelable!void { + _ = userdata; + _ = batch; + @panic("Not implemented yet"); +} + +fn batchAwaitConcurrent(userdata: ?*anyopaque, batch: *Batch, timeout: Timeout) Batch.AwaitConcurrentError!void { + _ = userdata; + _ = batch; + _ = timeout; + @panic("Not implemented yet"); +} + +fn batchCancel(userdata: ?*anyopaque, batch: *Batch) void { + _ = userdata; + _ = batch; + @panic("Not implemented yet"); +} + +fn dirCreateDir(userdata: ?*anyopaque, dir: Dir, sub_path: []const u8, permissions: Dir.Permissions) Dir.CreateDirError!void { + _ = userdata; + _ = dir; + _ = sub_path; + _ = permissions; + @panic("Not implemented yet"); +} + +fn dirCreateDirPath(userdata: ?*anyopaque, dir: Dir, sub_path: []const u8, permissions: Dir.Permissions) Dir.CreateDirPathError!Dir.CreatePathStatus { + _ = userdata; + _ = dir; + _ = sub_path; + _ = permissions; + @panic("Not implemented yet"); +} + +fn dirCreateDirPathOpen(userdata: ?*anyopaque, dir: Dir, sub_path: []const u8, permissions: Dir.Permissions, options: Dir.OpenOptions) Dir.CreateDirPathOpenError!Dir { + _ = userdata; + _ = dir; + _ = sub_path; + _ = permissions; + _ = options; + @panic("Not implemented yet"); +} + +fn dirOpenDir(userdata: ?*anyopaque, dir: Dir, sub_path: []const u8, options: Dir.OpenOptions) Dir.OpenError!Dir { + _ = userdata; + _ = dir; + _ = sub_path; + _ = options; + @panic("Not implemented yet"); +} + +fn dirStat(userdata: ?*anyopaque, dir: Dir) Dir.StatError!Dir.Stat { + _ = userdata; + _ = dir; + @panic("Not implemented yet"); +} + +fn dirStatFile(userdata: ?*anyopaque, dir: Dir, sub_path: []const u8, options: Dir.StatFileOptions) Dir.StatFileError!File.Stat { + _ = userdata; + _ = dir; + _ = sub_path; + _ = options; + @panic("Not implemented yet"); +} + +fn dirAccess(userdata: ?*anyopaque, dir: Dir, sub_path: []const u8, options: Dir.AccessOptions) Dir.AccessError!void { + _ = userdata; + _ = dir; + _ = sub_path; + _ = options; + @panic("Not implemented yet"); +} + +fn dirCreateFile(userdata: ?*anyopaque, dir: Dir, sub_path: []const u8, flags: Dir.CreateFileOptions) File.OpenError!File { + _ = userdata; + _ = dir; + _ = sub_path; + _ = flags; + @panic("Not implemented yet"); +} + +fn dirCreateFileAtomic(userdata: ?*anyopaque, dir: Dir, dest_path: []const u8, options: Dir.CreateFileAtomicOptions) Dir.CreateFileAtomicError!File.Atomic { + _ = userdata; + _ = dir; + _ = dest_path; + _ = options; + @panic("Not implemented yet"); +} + +fn dirOpenFile(userdata: ?*anyopaque, dir: Dir, sub_path: []const u8, flags: Dir.OpenFileOptions) File.OpenError!File { + _ = userdata; + _ = dir; + _ = sub_path; + _ = flags; + @panic("Not implemented yet"); +} + +fn dirClose(userdata: ?*anyopaque, dirs: []const Dir) void { + _ = userdata; + _ = dirs; + @panic("Not implemented yet"); +} + +fn dirRead(userdata: ?*anyopaque, dr: *Dir.Reader, buffer: []Dir.Entry) Dir.Reader.Error!usize { + _ = userdata; + _ = dr; + _ = buffer; + @panic("Not implemented yet"); +} + +fn dirRealPath(userdata: ?*anyopaque, dir: Dir, out_buffer: []u8) Dir.RealPathError!usize { + _ = userdata; + _ = dir; + _ = out_buffer; + @panic("Not implemented yet"); +} + +fn dirRealPathFile(userdata: ?*anyopaque, dir: Dir, sub_path: []const u8, out_buffer: []u8) Dir.RealPathFileError!usize { + _ = userdata; + _ = dir; + _ = sub_path; + _ = out_buffer; + @panic("Not implemented yet"); +} + +fn dirDeleteFile(userdata: ?*anyopaque, dir: Dir, sub_path: []const u8) Dir.DeleteFileError!void { + _ = userdata; + _ = dir; + _ = sub_path; + @panic("Not implemented yet"); +} + +fn dirDeleteDir(userdata: ?*anyopaque, dir: Dir, sub_path: []const u8) Dir.DeleteDirError!void { + _ = userdata; + _ = dir; + _ = sub_path; + @panic("Not implemented yet"); +} + +fn dirRename(userdata: ?*anyopaque, old_dir: Dir, old_sub_path: []const u8, new_dir: Dir, new_sub_path: []const u8) Dir.RenameError!void { + _ = userdata; + _ = old_dir; + _ = old_sub_path; + _ = new_dir; + _ = new_sub_path; + @panic("Not implemented yet"); +} + +fn dirRenamePreserve(userdata: ?*anyopaque, old_dir: Dir, old_sub_path: []const u8, new_dir: Dir, new_sub_path: []const u8) Dir.RenamePreserveError!void { + _ = userdata; + _ = old_dir; + _ = old_sub_path; + _ = new_dir; + _ = new_sub_path; + @panic("Not implemented yet"); +} + +fn dirSymLink(userdata: ?*anyopaque, dir: Dir, target_path: []const u8, sym_link_path: []const u8, flags: Dir.SymLinkFlags) Dir.SymLinkError!void { + _ = userdata; + _ = dir; + _ = target_path; + _ = sym_link_path; + _ = flags; + @panic("Not implemented yet"); +} + +fn dirReadLink(userdata: ?*anyopaque, dir: Dir, sub_path: []const u8, buffer: []u8) Dir.ReadLinkError!usize { + _ = userdata; + _ = dir; + _ = sub_path; + _ = buffer; + @panic("Not implemented yet"); +} + +fn dirSetOwner(userdata: ?*anyopaque, dir: Dir, owner: ?File.Uid, group: ?File.Gid) Dir.SetOwnerError!void { + _ = userdata; + _ = dir; + _ = owner; + _ = group; + @panic("Not implemented yet"); +} + +fn dirSetFileOwner(userdata: ?*anyopaque, dir: Dir, sub_path: []const u8, owner: ?File.Uid, group: ?File.Gid, options: Dir.SetFileOwnerOptions) Dir.SetFileOwnerError!void { + _ = userdata; + _ = dir; + _ = sub_path; + _ = owner; + _ = group; + _ = options; + @panic("Not implemented yet"); +} + +fn dirSetPermissions(userdata: ?*anyopaque, dir: Dir, permissions: Dir.Permissions) Dir.SetPermissionsError!void { + _ = userdata; + _ = dir; + _ = permissions; + @panic("Not implemented yet"); +} + +fn dirSetFilePermissions(userdata: ?*anyopaque, dir: Dir, sub_path: []const u8, permissions: File.Permissions, options: Dir.SetFilePermissionsOptions) Dir.SetFilePermissionsError!void { + _ = userdata; + _ = dir; + _ = sub_path; + _ = permissions; + _ = options; + @panic("Not implemented yet"); +} + +fn dirSetTimestamps(userdata: ?*anyopaque, dir: Dir, sub_path: []const u8, options: Dir.SetTimestampsOptions) Dir.SetTimestampsError!void { + _ = userdata; + _ = dir; + _ = sub_path; + _ = options; + @panic("Not implemented yet"); +} + +fn dirHardLink(userdata: ?*anyopaque, old_dir: Dir, old_sub_path: []const u8, new_dir: Dir, new_sub_path: []const u8, options: Dir.HardLinkOptions) Dir.HardLinkError!void { + _ = userdata; + _ = old_dir; + _ = old_sub_path; + _ = new_dir; + _ = new_sub_path; + _ = options; + @panic("Not implemented yet"); +} + +fn fileStat(userdata: ?*anyopaque, file: File) File.StatError!File.Stat { + _ = userdata; + _ = file; + @panic("Not implemented yet"); +} + +fn fileLength(userdata: ?*anyopaque, file: File) File.LengthError!u64 { + _ = userdata; + _ = file; + @panic("Not implemented yet"); +} + +fn fileClose(userdata: ?*anyopaque, files: []const File) void { + _ = userdata; + _ = files; + @panic("Not implemented yet"); +} + +fn fileWritePositional( + userdata: ?*anyopaque, + file : File, + header : []const u8, + data : []const []const u8, + splat : usize, + offset : u64 +) File.WritePositionalError!usize { + _ = userdata; + _ = file; + _ = header; + _ = data; + _ = splat; + _ = offset; + @panic("Not implemented yet"); +} + +fn fileWriteFileStreaming(userdata: ?*anyopaque, file: File, header: []const u8, file_reader: *Io.File.Reader, limit: Io.Limit) File.Writer.WriteFileError!usize { + _ = userdata; + _ = file; + _ = header; + _ = file_reader; + _ = limit; + @panic("Not implemented yet"); +} + +fn fileWriteFilePositional(userdata: ?*anyopaque, file: File, header: []const u8, file_reader: *Io.File.Reader, limit: Io.Limit, offset: u64) File.WriteFilePositionalError!usize { + _ = userdata; + _ = file; + _ = header; + _ = file_reader; + _ = limit; + _ = offset; + @panic("Not implemented yet"); +} + +fn fileReadPositional(userdata: ?*anyopaque, file: File, data: []const []u8, offset: u64) File.ReadPositionalError!usize { + _ = userdata; + _ = file; + _ = data; + _ = offset; + @panic("Not implemented yet"); +} + +fn fileSeekBy(userdata: ?*anyopaque, file: File, offset: i64) File.SeekError!void { + _ = userdata; + _ = file; + _ = offset; + @panic("Not implemented yet"); +} + +fn fileSeekTo(userdata: ?*anyopaque, file: File, offset: u64) File.SeekError!void { + _ = userdata; + _ = file; + _ = offset; + @panic("Not implemented yet"); +} + +fn fileSync(userdata: ?*anyopaque, file: File) File.SyncError!void { + _ = userdata; + _ = file; + @panic("Not implemented yet"); +} + +fn fileIsTty(userdata: ?*anyopaque, file: File) Cancelable!bool { + _ = userdata; + _ = file; + @panic("Not implemented yet"); +} + +fn fileEnableAnsiEscapeCodes(userdata: ?*anyopaque, file: File) File.EnableAnsiEscapeCodesError!void { + _ = userdata; + _ = file; + @panic("Not implemented yet"); +} + +fn fileSupportsAnsiEscapeCodes(userdata: ?*anyopaque, file: File) Cancelable!bool { + _ = userdata; + _ = file; + @panic("Not implemented yet"); +} + +fn fileSetLength(userdata: ?*anyopaque, file: File, length: u64) File.SetLengthError!void { + _ = userdata; + _ = file; + _ = length; + @panic("Not implemented yet"); +} + +fn fileSetOwner(userdata: ?*anyopaque, file: File, owner: ?File.Uid, group: ?File.Gid) File.SetOwnerError!void { + _ = userdata; + _ = file; + _ = owner; + _ = group; + @panic("Not implemented yet"); +} + +fn fileSetPermissions(userdata: ?*anyopaque, file: File, permissions: File.Permissions) File.SetPermissionsError!void { + _ = userdata; + _ = file; + _ = permissions; + @panic("Not implemented yet"); +} + +fn fileSetTimestamps(userdata: ?*anyopaque, file: File, options: File.SetTimestampsOptions) File.SetTimestampsError!void { + _ = userdata; + _ = file; + _ = options; + @panic("Not implemented yet"); +} + +fn fileLock(userdata: ?*anyopaque, file: File, lock: File.Lock) File.LockError!void { + _ = userdata; + _ = file; + _ = lock; + @panic("Not implemented yet"); +} + +fn fileTryLock(userdata: ?*anyopaque, file: File, lock: File.Lock) File.LockError!bool { + _ = userdata; + _ = file; + _ = lock; + @panic("Not implemented yet"); +} + +fn fileUnlock(userdata: ?*anyopaque, file: File) void { + _ = userdata; + _ = file; + @panic("Not implemented yet"); +} + +fn fileDowngradeLock(userdata: ?*anyopaque, file: File) File.DowngradeLockError!void { + _ = userdata; + _ = file; + @panic("Not implemented yet"); +} + +fn fileRealPath(userdata: ?*anyopaque, file: File, out_buffer: []u8) File.RealPathError!usize { + _ = userdata; + _ = file; + _ = out_buffer; + @panic("Not implemented yet"); +} + +fn fileHardLink(userdata: ?*anyopaque, file: File, new_dir: Dir, new_sub_path: []const u8, options: File.HardLinkOptions) File.HardLinkError!void { + _ = userdata; + _ = file; + _ = new_dir; + _ = new_sub_path; + _ = options; + @panic("Not implemented yet"); +} + +fn fileMemoryMapCreate(userdata: ?*anyopaque, file: File, options: File.MemoryMap.CreateOptions) File.MemoryMap.CreateError!File.MemoryMap { + _ = userdata; + _ = file; + _ = options; + @panic("Not implemented yet"); +} + +fn fileMemoryMapDestroy(userdata: ?*anyopaque, mm: *File.MemoryMap) void { + _ = userdata; + _ = mm; + @panic("Not implemented yet"); +} + +fn fileMemoryMapSetLength(userdata: ?*anyopaque, mm: *File.MemoryMap, new_len: usize) File.MemoryMap.SetLengthError!void { + _ = userdata; + _ = mm; + _ = new_len; + @panic("Not implemented yet"); +} + +fn fileMemoryMapRead(userdata: ?*anyopaque, mm: *File.MemoryMap) File.ReadPositionalError!void { + _ = userdata; + _ = mm; + @panic("Not implemented yet"); +} + +fn fileMemoryMapWrite(userdata: ?*anyopaque, mm: *File.MemoryMap) File.WritePositionalError!void { + _ = userdata; + _ = mm; + @panic("Not implemented yet"); +} + +fn processExecutableOpen(userdata: ?*anyopaque, flags: Dir.OpenFileOptions) std.process.OpenExecutableError!File { + _ = userdata; + _ = flags; + @panic("Not implemented yet"); +} + +fn processExecutablePath(userdata: ?*anyopaque, out_buffer: []u8) std.process.ExecutablePathError!usize { + _ = userdata; + _ = out_buffer; + @panic("Not implemented yet"); +} + +fn lockStderr(userdata: ?*anyopaque, terminal_mode: ?Terminal.Mode) Cancelable!LockedStderr { + _ = userdata; + _ = terminal_mode; + @panic("Not implemented yet"); +} + +fn tryLockStderr(userdata: ?*anyopaque, terminal_mode: ?Terminal.Mode) Cancelable!?LockedStderr { + _ = userdata; + _ = terminal_mode; + @panic("Not implemented yet"); +} + +fn unlockStderr(userdata: ?*anyopaque) void { + _ = userdata; + @panic("Not implemented yet"); +} + +fn processCurrentPath(userdata: ?*anyopaque, buffer: []u8) std.process.CurrentPathError!usize { + _ = userdata; + _ = buffer; + @panic("Not implemented yet"); +} + +fn processSetCurrentDir(userdata: ?*anyopaque, dir: Dir) std.process.SetCurrentDirError!void { + _ = userdata; + _ = dir; + @panic("Not implemented yet"); +} + +fn processSetCurrentPath(userdata: ?*anyopaque, dir_path: []const u8) std.process.SetCurrentPathError!void { + _ = userdata; + _ = dir_path; + @panic("Not implemented yet"); +} + +fn processReplace(userdata: ?*anyopaque, options: std.process.ReplaceOptions) std.process.ReplaceError { + _ = userdata; + _ = options; + @panic("Not implemented yet"); +} + +fn processReplacePath(userdata: ?*anyopaque, dir: Dir, options: std.process.ReplaceOptions) std.process.ReplaceError { + _ = userdata; + _ = dir; + _ = options; + @panic("Not implemented yet"); +} + +fn processSpawn(userdata: ?*anyopaque, options: std.process.SpawnOptions) std.process.SpawnError!std.process.Child { + _ = userdata; + _ = options; + @panic("Not implemented yet"); +} + +fn processSpawnPath(userdata: ?*anyopaque, dir: Dir, options: std.process.SpawnOptions) std.process.SpawnError!std.process.Child { + _ = userdata; + _ = dir; + _ = options; + @panic("Not implemented yet"); +} + +fn childWait(userdata: ?*anyopaque, child: *std.process.Child) std.process.Child.WaitError!std.process.Child.Term { + _ = userdata; + _ = child; + @panic("Not implemented yet"); +} + +fn childKill(userdata: ?*anyopaque, child: *std.process.Child) void { + _ = userdata; + _ = child; + @panic("Not implemented yet"); +} + +fn progressParentFile(userdata: ?*anyopaque) std.Progress.ParentFileError!File { + _ = userdata; + @panic("Not implemented yet"); +} + +fn now(userdata: ?*anyopaque, clock: Clock) Timestamp { + _ = userdata; + _ = clock; + @panic("Not implemented yet"); +} + +fn clockResolution(userdata: ?*anyopaque, clock: Clock) Clock.ResolutionError!Duration { + _ = userdata; + _ = clock; + @panic("Not implemented yet"); +} + +fn sleep(userdata: ?*anyopaque, timeout: Timeout) Cancelable!void { + _ = userdata; + _ = timeout; + @panic("Not implemented yet"); +} + +fn random(userdata: ?*anyopaque, buffer: []u8) void { + _ = userdata; + _ = buffer; + @panic("Not implemented yet"); +} + +fn randomSecure(userdata: ?*anyopaque, buffer: []u8) RandomSecureError!void { + _ = userdata; + _ = buffer; + @panic("Not implemented yet"); +} + +fn netListenIp(userdata: ?*anyopaque, address: *const net.IpAddress, options: net.IpAddress.ListenOptions) net.IpAddress.ListenError!net.Socket { + _ = userdata; + _ = address; + _ = options; + @panic("Not implemented yet"); +} + +fn netAccept(userdata: ?*anyopaque, listen_handle: net.Socket.Handle, options: net.Server.AcceptOptions) net.Server.AcceptError!net.Socket { + _ = userdata; + _ = listen_handle; + _ = options; + @panic("Not implemented yet"); +} + +fn netBindIp(userdata: ?*anyopaque, address: *const net.IpAddress, options: net.IpAddress.BindOptions) net.IpAddress.BindError!net.Socket { + _ = userdata; + _ = address; + _ = options; + @panic("Not implemented yet"); +} + +fn netConnectIp(userdata: ?*anyopaque, address: *const net.IpAddress, options: net.IpAddress.ConnectOptions) net.IpAddress.ConnectError!net.Socket { + _ = userdata; + _ = address; + _ = options; + @panic("Not implemented yet"); +} + +fn netListenUnix(userdata: ?*anyopaque, address: *const net.UnixAddress, options: net.UnixAddress.ListenOptions) net.UnixAddress.ListenError!net.Socket.Handle { + _ = userdata; + _ = address; + _ = options; + @panic("Not implemented yet"); +} + +fn netConnectUnix(userdata: ?*anyopaque, address: *const net.UnixAddress) net.UnixAddress.ConnectError!net.Socket.Handle { + _ = userdata; + _ = address; + @panic("Not implemented yet"); +} + +fn netSocketCreatePair(userdata: ?*anyopaque, options: net.Socket.CreatePairOptions) net.Socket.CreatePairError![2]net.Socket { + _ = userdata; + _ = options; + @panic("Not implemented yet"); +} + +fn netSend(userdata: ?*anyopaque, handle: net.Socket.Handle, messages: []net.OutgoingMessage, flags: net.SendFlags) struct { ?net.Socket.SendError, usize } { + _ = userdata; + _ = handle; + _ = messages; + _ = flags; + @panic("Not implemented yet"); +} + +/// Returns 0 on end of stream. +fn netRead(userdata: ?*anyopaque, fd: net.Socket.Handle, data: [][]u8) net.Stream.Reader.Error!usize { + _ = userdata; + _ = fd; + _ = data; + @panic("Not implemented yet"); +} + +fn netWrite(userdata: ?*anyopaque, handle: net.Socket.Handle, header: []const u8, data: []const []const u8, splat: usize) net.Stream.Writer.Error!usize { + _ = userdata; + _ = handle; + _ = header; + _ = data; + _ = splat; + @panic("Not implemented yet"); +} + +fn netWriteFile(userdata: ?*anyopaque, socket_handle: net.Socket.Handle, header: []const u8, file_reader: *Io.File.Reader, limit: Io.Limit) net.Stream.Writer.WriteFileError!usize { + _ = userdata; + _ = socket_handle; + _ = header; + _ = file_reader; + _ = limit; + @panic("Not implemented yet"); +} + +fn netClose(userdata: ?*anyopaque, handles: []const net.Socket.Handle) void { + _ = userdata; + _ = handles; + @panic("Not implemented yet"); +} + +fn netShutdown(userdata: ?*anyopaque, handle: net.Socket.Handle, how: net.ShutdownHow) net.ShutdownError!void { + _ = userdata; + _ = handle; + _ = how; + @panic("Not implemented yet"); +} + +fn netInterfaceNameResolve(userdata: ?*anyopaque, name: *const net.Interface.Name) net.Interface.Name.ResolveError!net.Interface { + _ = userdata; + _ = name; + @panic("Not implemented yet"); +} + +fn netInterfaceName(userdata: ?*anyopaque, interface: net.Interface) net.Interface.NameError!net.Interface.Name { + _ = userdata; + _ = interface; + @panic("Not implemented yet"); +} + +fn netLookup(userdata: ?*anyopaque, host_name: net.HostName, resolved: *Queue(net.HostName.LookupResult), options: net.HostName.LookupOptions) net.HostName.LookupError!void { + _ = userdata; + _ = host_name; + _ = resolved; + _ = options; + @panic("Not implemented yet"); +} diff --git a/src/main.zig b/src/main.zig new file mode 100644 index 0000000..a4de78a --- /dev/null +++ b/src/main.zig @@ -0,0 +1,53 @@ +const std = @import("std"); +const Io = std.Io; + +const Simulator = @import("simulator.zig"); + +fn programA(init: std.process.Init) anyerror!void { + var stdout = Io.File.stdout().writerStreaming(init.io, &.{}); + for (0..3) |_| { + try stdout.interface.print("Hello from program A!\n", .{}); + try stdout.interface.flush(); + } +} + +fn programB(init: std.process.Init) anyerror!void { + var stdout = Io.File.stdout().writerStreaming(init.io, &.{}); + for (0..3) |_| { + try stdout.interface.print("Hello from program B!\n", .{}); + try stdout.interface.flush(); + } +} + +fn programC(init: std.process.Init) anyerror!void { + var stdout = Io.File.stdout().writerStreaming(init.io, &.{}); + for (0..3) |_| { + try stdout.interface.print("Hello from program C!\n", .{}); + try stdout.interface.flush(); + } +} + +pub fn main(init: std.process.Init) !void { + var sim: Simulator = undefined; + sim.init(std.heap.page_allocator, init.io); + defer sim.deinit(); + + // Associate executable names to zig entry functions + try sim.addExecutable("program_a", programA); + try sim.addExecutable("program_b", programB); + try sim.addExecutable("program_c", programC); + + // Now run commands in the form: + // program_name arg1 arg2 arg3 ... + // where program_name is one of the registered functions. + try sim.spawn("program_a", .{}); + try sim.spawn("program_b", .{}); + try sim.spawn("program_c", .{}); + + // Advance the cluster's state by advancing the program's + // states one by one. Exits when all programs have returned + // or failed. + while (sim.scheduleOne()) {} + + std.debug.print("Simulation ended\n", .{}); +} diff --git a/src/network.zig b/src/network.zig new file mode 100644 index 0000000..71dddcf --- /dev/null +++ b/src/network.zig @@ -0,0 +1,247 @@ + +const std = @import("std"); + +const Network = @This(); +const Allocator = std.mem.Allocator; + +pub const Address = struct { + ipv4: u32, + port: u16, + pub fn eql(self: Address, other: Address) bool { + return self.ipv4 == other.ipv4 + and self.port == other.port; + } +}; + +pub const ListenError = error { + AddressNotAvailable, + AddressAlreadyUsed, +} || Allocator.Error; + +pub const AcceptError = error { + AcceptQueueEmpty, +} || Allocator.Error; + +pub const ConnectError = error { + UnavailableHost, + PeerNotListeningOnAddress, +} || Allocator.Error; + +pub const SendError = Allocator.Error; +pub const ReadError = error {}; + +pub const ListenSocket = struct { + + next: ?*ListenSocket, + + address: Address, + accept_queue: std.ArrayList(*ConnSocket), +}; + +pub const ConnSocket = struct { + + next: ?*ConnSocket, + + peer_listen: ?*ListenSocket, + peer_conn : ?*ConnSocket, + + input_buffer: std.ArrayList(u8), + output_buffer: std.ArrayList(u8), +}; + +pub const Host = struct { + + gpa: Allocator, + + // Parent network system + network: *Network, + + listen_list: ?*ListenSocket, + conn_list: ?*ConnSocket, + + available_addresses_ipv4: []const u32, + + pub fn init(self: *Host, network: *Network, addresses: []const u32, gpa: Allocator) void { + self.gpa = gpa; + self.network = network; + self.listen_list = null; + self.conn_list = null; + self.available_addresses_ipv4 = addresses; + } + + pub fn deinit(self: *Host) void { + _ = self; + } + + fn linkConnSocket(self: *Host, socket: *ConnSocket) void { + socket.next = self.conn_list; + self.conn_list = socket; + } + + fn unlinkConnSocket(self: *Host, socket: *ConnSocket) void { + var cursor = self.conn_list.*; // Note that the list can't be empty or we wouldn't be unlinking + if (cursor == socket) { + self.conn_list = null; + } else { + while (cursor.next != socket) + cursor = cursor.next.*; + } + } + + fn addressCurrentlyUsed(self: *Host, address: Address) bool { + var socket = self.listen_list; + while (socket) |s| { + if (s.address.eql(address)) + return true; + socket = s.next; + } + return false; + } + + pub fn isAddressAvailable(self: *Host, ipv4: u32) bool { + for (self.available_addresses_ipv4) |item| { + if (ipv4 == item) + return true; + } + return false; + } + + pub fn listen(self: *Host, address: Address, socket: *ListenSocket) ListenError!void { + + if (!self.isAddressAvailable(address.ipv4)) + return ListenError.AddressNotAvailable; + + if (self.addressCurrentlyUsed(address)) + return ListenError.AddressAlreadyUsed; + + socket.address = address; + socket.accept_queue = .empty; + + // Add listen socket to the list + socket.next = self.listen_list; + self.listen_list = socket; + } + + pub fn accept(self: *Host, socket: *ListenSocket, new_socket: *ConnSocket) AcceptError!void { + + // Pop a connectioon from the listener's accept queue + if (socket.accept_queue.items.len == 0) + return AcceptError.AcceptQueueEmpty; + const peer_socket = socket.accept_queue.orderedRemove(0); + + // Add newly created socket to the connection socket list + self.linkConnSocket(new_socket); + errdefer self.unlinkConnSocket(new_socket); + + // Link the bound sockets and remove the reference to the listener + new_socket.peer_conn = peer_socket; + peer_socket.peer_conn = new_socket; + peer_socket.peer_listen = null; + + // Initialize other fields + new_socket.peer_listen = null; + new_socket.input_buffer = .empty; + new_socket.output_buffer = .empty; + } + + fn findListenSocket(self: *Host, address: Address) ?*ListenSocket { + var socket = self.listen_list; + while (socket) |s| { + if (s.address.eql(address)) + return s; + socket = s.next; + } + return null; + } + + pub fn connect(self: *Host, address: Address, new_socket: *ConnSocket) ConnectError!void { + + const host = self.network.findHostByIPv4(address.ipv4) orelse return ConnectError.UnavailableHost; + const listen_socket = host.findListenSocket(address) orelse return ConnectError.PeerNotListeningOnAddress; + + // Add newly created socket to the connection socket list + self.linkConnSocket(new_socket); + errdefer self.unlinkConnSocket(new_socket); + + new_socket.peer_listen = listen_socket; + new_socket.peer_conn = null; + new_socket.input_buffer = .empty; + new_socket.output_buffer = .empty; + + // Add socket to the peer's accept queue + try listen_socket.accept_queue.append(self.gpa, new_socket); + } + + pub fn closeConnSocket(self: *Host, socket: *ConnSocket) void { + + if (socket.peer_listen) |peer| { + for (peer.accept_queue.items, 0..) |item, i| { + if (item == socket) { + _ = peer.accept_queue.orderedRemove(i); + break; + } + } + } + + if (socket.peer_conn) |peer| { + peer.peer_conn = null; + } + + self.unlinkConnSocket(socket); + socket.input_buffer.deinit(self.gpa); + socket.output_buffer.deinit(self.gpa); + } + + pub fn closeListenSocket(self: *Host, socket: *ListenSocket) void { + + for (socket.accept_queue.items) |peer| { + peer.peer_listen = null; + } + + self.unlinkListenSocket(socket); + socket.accept_queue.deinit(self.gpa); + } + + pub fn send(self: *Host, socket: *ConnSocket, source: []const u8) usize { + var n: usize = 0; + for (source) |c| { + socket.output_buffer.append(self.gpa, c) catch break; + n += 1; + } + return n; + } + + pub fn read(_: *Host, socket: *ConnSocket, target: []u8) usize { + const num = @min(target.len, socket.input_buffer.items.len); + @memcpy(target[0..num], socket.input_buffer.items[0..num]); + for (0..num) |_| { + socket.input_buffer.orderedRemove(0); + } + return num; + } +}; + +gpa: Allocator, +hosts: std.ArrayList(*Host), + +pub fn init(self: *Network, gpa: Allocator) void { + self.gpa = gpa; + self.hosts = .empty; +} + +pub fn deinit(self: *Network) void { + self.hosts.deinit(self.gpa); +} + +pub fn registerHost(self: *Network, host: *Host) Allocator.Error!void { + try self.hosts.append(self.gpa, host); + host.network = self; +} + +pub fn findHostByIPv4(self: *Network, ipv4: u32) ?*Host { + for (self.hosts.items) |host| { + if (host.isAddressAvailable(ipv4)) + return host; + } + return null; +} diff --git a/src/node.zig b/src/node.zig new file mode 100644 index 0000000..5480120 --- /dev/null +++ b/src/node.zig @@ -0,0 +1,383 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; + +const FileSystem = @import("file_system.zig"); +const Network = @import("network.zig"); +const Scheduler = @import("scheduler.zig"); +const ioInterface = @import("io_interface.zig"); + +const MAX_DESCRIPTORS = 1<<10; + +const Node = @This(); +const Handle = i32; + +const Descriptor = struct { + + const Kind = enum { + dir, + file, + listen, + conn, + unused, + }; + + kind : Kind = .unused, + dir : FileSystem.OpenDir = undefined, + file : FileSystem.OpenFile = undefined, + listen: Network.ListenSocket = undefined, + conn : Network.ConnSocket = undefined, +}; + +gpa: Allocator, +arena: std.heap.ArenaAllocator, +argv: [][*:0]const u8, +environ_map: std.process.Environ.Map, +scheduler: *Scheduler, +file_system: FileSystem, +network_host: Network.Host, +descriptors: [MAX_DESCRIPTORS]Descriptor, +real_io: std.Io, +stdin_reader: std.Io.File.Reader, +stderr_writer: std.Io.File.Writer, +stdout_writer: std.Io.File.Writer, +stderr_buffer: [1024]u8, +stdout_buffer: [1024]u8, + +fn splitCommandArguments(command: []const u8, arena: Allocator) Allocator.Error![][*:0]const u8 { + var cursor: usize = 0; + + // Count how many arguments there are + var count: usize = 0; + while (cursor < command.len) { + if (command[cursor] != ' ' and (cursor == 0 or command[cursor-1] == ' ')) + count += 1; + cursor += 1; + } + + const result = try arena.alloc([*:0]const u8, count); + + count = 0; + cursor = 0; + while (true) { + while (cursor < command.len and command[cursor] == ' ') + cursor += 1; + + if (cursor == command.len) + break; + + const offset = cursor; + while (cursor < command.len and command[cursor] != ' ') + cursor += 1; + const arg = command[offset..cursor]; + + result[count] = (try arena.dupeZ(u8, arg)).ptr; + count += 1; + } + + return result; +} + +pub fn init( + self : *Node, + real_io : std.Io, + scheduler: *Scheduler, + network : *Network, + command : []const u8, + addresses: []const u32, + gpa : Allocator +) !void { + self.gpa = gpa; + self.arena = .init(gpa); + self.scheduler = scheduler; + try self.file_system.init(gpa); + + self.network_host.init(network, addresses, gpa); + try network.registerHost(&self.network_host); + + for (&self.descriptors) |*desc| { + desc.kind = .unused; + } + + self.argv = try splitCommandArguments(command, self.arena.allocator()); + self.environ_map = try std.process.Environ.createMap(.empty, gpa); + + self.real_io = real_io; + self.stdin_reader = std.Io.File.stdin().readerStreaming(real_io, &.{}); + self.stdout_writer = std.Io.File.stdout().writerStreaming(real_io, &self.stdout_buffer); + self.stderr_writer = std.Io.File.stderr().writerStreaming(real_io, &self.stderr_buffer); +} + +pub fn deinit(self: *Node) void { + self.environ_map.deinit(); + self.network_host.deinit(); + self.file_system.deinit(self.gpa); + self.arena.deinit(); +} + +pub fn io(self: *Node) std.Io { + return ioInterface.buildIOInterfaceForNode(self); +} + +pub fn processInit(self: *Node) std.process.Init { + return .{ + .minimal = .{ + .environ = .empty, + .args = .{ .vector = self.argv }, + }, + .arena = &self.arena, + .gpa = self.gpa, // TODO: Should use a per-task allocator + .io = self.io(), + .environ_map = &self.environ_map, + .preopens = .empty, + }; +} + +fn unusedDesc(self: *Node) ?*Descriptor { + for (&self.descriptors) |*desc| { + if (desc.kind == .unused) + return desc; + } + return null; +} + +const HandleError = error { + InvalidHandle, +}; + +const NUM_SPECIAL_HANDLES = 3; + +fn handleToDesc(self: *Node, handle: Handle) HandleError!*Descriptor { + + // Any special handle (stdin, stdout, stderr) must be handled + // as special cases before this point. + std.debug.assert(handle >= NUM_SPECIAL_HANDLES); + const index = handle - NUM_SPECIAL_HANDLES; + + if (index < 0 or index >= MAX_DESCRIPTORS) + return HandleError.InvalidHandle; + const desc = &self.descriptors[@intCast(index)]; + if (desc.kind == .unused) + return HandleError.InvalidHandle; + return desc; +} + +fn handleToDescOfType(self: *Node, handle: Handle, kind: Descriptor.Kind) HandleError!*Descriptor { + const desc = try self.handleToDesc(handle); + if (desc.kind != kind) + return HandleError.InvalidHandle; + return desc; +} + +fn descToHandle(self: *Node, desc: *Descriptor) Handle { + // TODO: This loop is extremely dumb + for (&self.descriptors, 0..) |*item, i| { + if (item == desc) + return @intCast(i + NUM_SPECIAL_HANDLES); + } + unreachable; +} + +pub fn closeDir(self: *Node, handle: Handle) HandleError!void { + self.scheduler.sleep(10); + const desc = try self.handleToDescOfType(handle, .dir); + self.file_system.closeDir(&desc.dir, self.gpa); + desc.kind = .unused; +} + +pub fn readDir(self: *Node, handle: Handle) HandleError!FileSystem.ReadDir { + self.scheduler.sleep(10); + const desc = try self.handleToDescOfType(handle, .dir); + return self.file_system.readDir(&desc.dir); +} + +fn handleToOpenDirOrNULL(self: *Node, handle: ?Handle) HandleError!?*FileSystem.OpenDir { + if (handle) |h| { + const desc = try self.handleToDescOfType(h, .dir); + return &desc.dir; + } else { + return null; + } +} + +pub const CreateFileError = HandleError || FileSystem.CreateError; + +pub fn createFile(self: *Node, parent: ?Handle, path: []const u8) CreateFileError!void { + self.scheduler.sleep(10); + return self.file_system.createFile( + path, + try self.handleToOpenDirOrNULL(parent), + self.gpa + ); +} + +pub const DeleteFileError = HandleError || FileSystem.DeleteError; + +pub fn deleteFile(self: *Node, parent: ?Handle, path: []const u8) DeleteFileError!void { + self.scheduler.sleep(10); + return self.file_system.deleteAny( + path, + try self.handleToOpenDirOrNULL(parent), + self.gpa + ); +} + +pub const OpenFileError = error { + DescriptorLimit, +} || FileSystem.OpenError; + +pub fn openFile(self: *Node, parent: ?Handle, path: []const u8) !Handle { + self.scheduler.sleep(10); + const desc = self.unusedDesc() orelse return OpenFileError.DescriptorLimit; + try self.file_system.openFile(path, try self.handleToOpenDirOrNULL(parent), &desc.file); + desc.kind = .file; + return self.descToHandle(desc); +} + +pub fn closeFile(self: *Node, handle: Handle) HandleError!void { + self.scheduler.sleep(10); + const desc = try self.handleToDescOfType(handle, .file); + self.file_system.closeFile(&desc.file, self.gpa); + desc.kind = .unused; +} + +pub fn fileSize(self: *Node, handle: Handle) HandleError!u64 { + self.scheduler.sleep(2); + const desc = try self.handleToDescOfType(handle, .file); + return @intCast(self.file_system.fileSize(&desc.file)); +} + +pub fn readFile(self: *Node, handle: Handle, offset: ?usize, target: []u8) HandleError!usize { + self.scheduler.sleep(100); + if (handle == 0) { + @panic("Not implemented yet"); // TODO: stdin + } else if (handle == 1) { + return HandleError.InvalidHandle; + } else if (handle == 2) { + return HandleError.InvalidHandle; + } else { + const desc = try self.handleToDescOfType(handle, .file); + return self.file_system.readFile(&desc.file, offset, target); + } +} + +// It's important this function and the stderr version do not +// return a value back to the simulation or determinism would +// be broken. +fn writeToStdout(self: *Node, source: []const u8) void { + self.stdout_writer.interface.writeAll(source) catch {}; + self.stdout_writer.interface.flush() catch {}; +} + +// See comment on writeToStdout +fn writeToStderr(self: *Node, source: []const u8) void { + self.stderr_writer.interface.writeAll(source) catch {}; + self.stderr_writer.interface.flush() catch {}; +} + +pub const WriteFileError = HandleError || Allocator.Error; + +pub fn writeFile( + self : *Node, + handle: Handle, + offset: ?usize, + header: []const u8, + source: []const []const u8 +) WriteFileError!usize { + + self.scheduler.sleep(100); + + if (handle == 0) { + return HandleError.InvalidHandle; + } else if (handle == 1) { + var copied: usize = 0; + self.writeToStdout(header); + copied += header.len; + for (source) |item| { + self.writeToStdout(item); + copied += item.len; + } + return copied; + } else if (handle == 2) { + var copied: usize = 0; + self.writeToStderr(header); + copied += header.len; + for (source) |item| { + self.writeToStderr(item); + copied += item.len; + } + return copied; + } else { + var copied: usize = 0; + const desc = try self.handleToDescOfType(handle, .file); + try self.file_system.writeFile(&desc.file, self.gpa, offset, header); + copied += header.len; + for (source) |item| { + try self.file_system.writeFile(&desc.file, self.gpa, null, item); + copied += item.len; + } + return copied; + } +} + +pub const Address = Network.Address; +pub const ListenError = error { + DescriptorLimit, +} || Network.ListenError; + +pub fn listen(self: *Node, address: Address) ListenError!Handle { + self.scheduler.sleep(10); + const desc = self.unusedDesc() orelse return ListenError.DescriptorLimit; + try self.network_host.listen(address, &desc.listen); + desc.kind = .listen; + return self.descToHandle(desc); +} + +pub const AcceptError = error { + DescriptorLimit, +} || HandleError || Network.AcceptError; + +pub fn accept(self: *Node, handle: Handle) AcceptError!Handle { + self.scheduler.sleep(10); + const old_desc = try self.handleToDescOfType(handle, .listen); + const new_desc = self.unusedDesc() orelse return AcceptError.DescriptorLimit; + try self.network_host.accept(&old_desc.listen, &new_desc.conn); + new_desc.kind = .conn; + return self.descToHandle(new_desc); +} + +pub const ConnectError = error { + DescriptorLimit, +} || Network.ConnectError; + +pub fn connect(self: *Node, address: Address) ConnectError!Handle { + self.scheduler.sleep(10); + const desc = self.unusedDesc() orelse return ConnectError.DescriptorLimit; + try self.network_host.connect(address, &desc.conn); + desc.kind = .conn; + return self.descToHandle(desc); +} + +pub fn readSocket(self: *Node, handle: Handle, target: []u8) HandleError!usize { + self.scheduler.sleep(10); + const desc = try self.handleToDescOfType(handle, .conn); + return self.network_host.read(&desc.conn, target); +} + +pub fn writeSocket(self: *Node, handle: Handle, source: []const u8) HandleError!usize { + self.scheduler.sleep(10); + const desc = try self.handleToDescOfType(handle, .conn); + return self.network_host.send(&desc.conn, source); +} + +pub fn closeSocket(self: *Node, handle: Handle) HandleError!void { + self.scheduler.sleep(10); + const desc = try self.handleToDesc(handle); + if (desc.kind == .conn) { + self.network_host.closeConnSocket(&desc.conn); + } else if (desc.kind == .listen) { + self.network_host.closeListenSocket(&desc.listen); + } else { + return HandleError.InvalidHandle; + } + desc.kind = .unused; +} diff --git a/src/scheduler.zig b/src/scheduler.zig new file mode 100644 index 0000000..f45f06c --- /dev/null +++ b/src/scheduler.zig @@ -0,0 +1,192 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const Node = @import("node.zig"); + +const Scheduler = @This(); +pub const EntryPoint = *const fn(std.process.Init) anyerror!void; + +const Registers = struct { + rsp: usize, + rbp: usize, + rip: usize, + rdi: usize, +}; + +const ContextSwitch = extern struct { + old: *Registers, + new: *Registers, +}; + +const State = enum { + ready, + running, + blocked, + failed, + returned, +}; + +const Task = struct { + regs : Registers, + stack : []align(16) u8, + entry : EntryPoint, + state : State, + wakeup: ?u64, + node : *Node, +}; + +gpa: Allocator, +tasks: std.ArrayList(Task), +regs: Registers, +current: ?*Task, +current_time: u64, + +pub fn init(self: *Scheduler, gpa: Allocator) void { + self.gpa = gpa; + self.tasks = .empty; + self.current = null; + self.current_time = 0; +} + +pub fn deinit(self: *Scheduler) void { + for (self.tasks.items) |task| { + std.heap.page_allocator.free(task.stack); + } + self.tasks.deinit(self.gpa); +} + +pub fn spawn(self: *Scheduler, node: *Node, entry: EntryPoint, stack_size: usize) !void { + + const stack = try std.heap.page_allocator.alignedAlloc(u8, .fromByteUnits(16), stack_size); + errdefer std.heap.page_allocator.free(stack); + + var stack_top = @intFromPtr(stack.ptr) + stack.len; + stack_top &= ~@as(usize, 0xf); + stack_top -= @sizeOf(usize); + + // If the entry point returns, `ret` jumps here instead of into nowhere. + @as(*usize, @ptrFromInt(stack_top)).* = @intFromPtr(&taskReturned); + + const task = Task { + .regs = .{ + .rsp = stack_top, + .rbp = 0, + .rip = @intFromPtr(&taskStart), + .rdi = @intFromPtr(self), + }, + .stack = stack, + .entry = entry, + .state = .ready, + .wakeup = null, + .node = node, + }; + + try self.tasks.append(self.gpa, task); +} + +fn findTaskWithState(self: *Scheduler, state: State) ?*Task { + for (self.tasks.items) |*task| { + if (task.state == state) + return task; + } + return null; +} + +fn findBlockedTaskWithLowestWakeupTime(self: *Scheduler) ?*Task { + var task = self.findTaskWithState(.blocked) orelse return null; + for (self.tasks.items) |*t| { + // If the state is blocked, wakeup MUST be set + std.debug.assert(t.state != .blocked or t.wakeup != null); + if (t.state == .blocked and t.wakeup.? < task.wakeup.?) { + task = t; + } + } + return task; +} + +fn advanceTimeAndUnblockTasks(self: *Scheduler, new_time: u64) void { + std.debug.assert(self.current_time < new_time); + self.current_time = new_time; + for (self.tasks.items) |*task| { + if (task.state == .blocked and task.wakeup.? <= new_time) { + task.state = .ready; + task.wakeup = null; + } + } +} + +fn advanceTimeAndPickTask(self: *Scheduler) ?*Task { + const task = self.findBlockedTaskWithLowestWakeupTime() orelse return null; + self.advanceTimeAndUnblockTasks(task.wakeup.?); + return task; +} + +pub fn scheduleOne(self: *Scheduler) bool { + const task = self.findTaskWithState(.ready) + orelse self.advanceTimeAndPickTask() + orelse return false; + self.current = task; + task.state = .running; + contextSwitch(&self.regs, &task.regs); + return true; +} + +fn contextSwitch(old: *Registers, new: *Registers) void { + asm volatile ( + \\ movq 0(%%rsi), %%rax + \\ movq 8(%%rsi), %%rcx + \\ leaq 0f(%%rip), %%rdx + \\ movq %%rsp, 0(%%rax) + \\ movq %%rbp, 8(%%rax) + \\ movq %%rdx, 16(%%rax) + \\ movq 0(%%rcx), %%rsp + \\ movq 8(%%rcx), %%rbp + \\ movq 24(%%rcx), %%rdi + \\ jmpq *16(%%rcx) + \\0: + : + : [message] "{rsi}" (&ContextSwitch{ .old = old, .new = new }), + : .{ + .rax = true, + .rcx = true, + .rdx = true, + .rbx = true, + .rsi = true, + .rdi = true, + .r8 = true, + .r9 = true, + .r10 = true, + .r11 = true, + .r12 = true, + .r13 = true, + .r14 = true, + .r15 = true, + .memory = true, + }); +} + +fn taskStart(self: *Scheduler) callconv(.c) noreturn { + const task = self.current.?; + + task.entry(task.node.processInit()) catch { + task.state = .failed; + contextSwitch(&task.regs, &self.regs); + unreachable; + }; + + task.state = .returned; + contextSwitch(&task.regs, &self.regs); + unreachable; +} + +// Dummy return address on the task's stack. Should never be reached. +fn taskReturned() callconv(.c) noreturn { + @panic("Task returned through the fake return address"); +} + +// Called by the current task to return control to the scheduler +pub fn sleep(self: *Scheduler, delta_us: u64) void { + const task = self.current.?; + task.state = .blocked; + task.wakeup = self.current_time + delta_us; + contextSwitch(&task.regs, &self.regs); +} diff --git a/src/simulator.zig b/src/simulator.zig new file mode 100644 index 0000000..efc42f9 --- /dev/null +++ b/src/simulator.zig @@ -0,0 +1,98 @@ +const Simulator = @This(); + +const std = @import("std"); +const Allocator = std.mem.Allocator; + +const Scheduler = @import("scheduler.zig"); +const Network = @import("network.zig"); +const Node = @import("node.zig"); + +pub const EntryPoint = Scheduler.EntryPoint; + +const SpawnOptions = struct { + stack_size: usize = 64 * 1024, + addresses: []const u32 = &[0]u32 {}, // TODO: How do I make an empty slice? +}; + +const SpawnError = error { + InvalidCommand, + NoSuchProgram, +} || Allocator.Error || std.process.Environ.CreateMapError; + +const ExecutableName = struct { + name : []const u8, + entry: EntryPoint, +}; + +gpa: Allocator, +scheduler: Scheduler, +network: Network, +nodes: std.ArrayList(*Node), +executables: std.ArrayList(ExecutableName), +real_io: std.Io, + +pub fn init(self: *Simulator, gpa: Allocator, real_io: std.Io) void { + self.gpa = gpa; + self.scheduler.init(gpa); + self.network.init(gpa); + self.nodes = .empty; + self.executables = .empty; + self.real_io = real_io; +} + +pub fn deinit(self: *Simulator) void { + for (self.nodes.items) |node| { + node.deinit(); + self.gpa.destroy(node); + } + self.executables.deinit(self.gpa); + self.nodes.deinit(self.gpa); + self.network.deinit(); + self.scheduler.deinit(); +} + +pub fn addExecutable(self: *Simulator, name: []const u8, entry: EntryPoint) Allocator.Error!void { + try self.executables.append(self.gpa, ExecutableName { + .name = name, + .entry = entry, + }); +} + +pub fn spawn(self: *Simulator, command: []const u8, options: SpawnOptions) SpawnError!void { + const name = extractProgramNameFromCommand(command) orelse return SpawnError.InvalidCommand; + const entry = self.getExecutableEntryPoint(name) orelse return SpawnError.NoSuchProgram; + + const node = try self.gpa.create(Node); + errdefer self.gpa.destroy(node); + + try node.init(self.real_io, &self.scheduler, &self.network, command, options.addresses, self.gpa); + + try self.nodes.append(self.gpa, node); + errdefer _ = self.nodes.swapRemove(self.nodes.items.len-1); + + try self.scheduler.spawn(node, entry, options.stack_size); +} + +pub fn scheduleOne(self: *Simulator) bool { + return self.scheduler.scheduleOne(); +} + +// Reads the first word of a command +// "program arg1 arg2 arg3" -> "program" +fn extractProgramNameFromCommand(command: []const u8) ?[]const u8 { + var cur: usize = 0; + while (cur < command.len and command[cur] != ' ') + cur += 1; + + if (cur == 0) + return null; + return command[0..cur]; +} + +fn getExecutableEntryPoint(self: *Simulator, name: []const u8) ?EntryPoint { + for (self.executables.items) |executable| { + if (std.mem.eql(u8, executable.name, name)) + return executable.entry; + } + return null; +}