First commit

This commit is contained in:
2026-05-21 21:31:17 +02:00
commit 6f14bec123
10 changed files with 2508 additions and 0 deletions
+7
View File
@@ -0,0 +1,7 @@
.zig-cache
zig-out
.codex
.agents
ignored
main
mock_fs_dump
+66
View File
@@ -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.
+53
View File
@@ -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", .{});
}
+416
View File
@@ -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;
}
+993
View File
@@ -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");
}
+53
View File
@@ -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", .{});
}
+247
View File
@@ -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;
}
+383
View File
@@ -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;
}
+192
View File
@@ -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);
}
+98
View File
@@ -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;
}