Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
f2a6e94
regz: Move VirtualFilesystem into /modules
piotrfila Jul 15, 2026
91a34c1
virtual-io: rename VirtualFilesystem to VirtualIo
piotrfila Jul 15, 2026
387b0e6
virtual-io: create vtable from std.Io.failing
piotrfila Jul 15, 2026
08a5af2
virtual-io: rename vfs and fs to vio
piotrfila Jul 15, 2026
9d4400c
virtual-io: make root_dir a decl instead of a function
piotrfila Jul 15, 2026
5a0e71b
virtual-io: use raw file descriptors instead of ID
piotrfila Jul 15, 2026
3fd8778
virtual-io: redo error handling
piotrfila Jul 16, 2026
00434be
virtual-io: improve file/dir creation
piotrfila Jul 16, 2026
5b3f408
virtual-io: move VTable functions to one place
piotrfila Jul 16, 2026
65bb7e4
virtual-io: unify files and directories into nodes
piotrfila Jul 16, 2026
0406dc4
virtual-io: style improvements
piotrfila Jul 16, 2026
eaf681a
virtual-io: reduce allocations
piotrfila Jul 16, 2026
763a581
virtual-io: improve error handling
piotrfila Jul 16, 2026
c0933fe
virtual-io: create root dir during initialization
piotrfila Jul 16, 2026
405afb6
virtual-io: add dirOpenDir to VTable
piotrfila Jul 16, 2026
badc08b
virtual-io: store directory contents instead of relationships
piotrfila Jul 16, 2026
26f0b9e
virtual-io: store files as arrayList instead of Writer.Allocating
piotrfila Jul 16, 2026
5260788
virtual-io: Use enums as IDs again
piotrfila Jul 16, 2026
9aaffeb
virtual-io: Type safety around directory
piotrfila Jul 16, 2026
94c72ae
virtual-io: Type safety around file
piotrfila Jul 16, 2026
fa330ed
virtual-io: Redo namespacing
piotrfila Jul 16, 2026
fe49b56
virtual-io: Separate IDs for files and directories
piotrfila Jul 17, 2026
b75acc6
virtual-io: Additional error checking
piotrfila Jul 17, 2026
2c6fe8c
Merge branch 'ZigEmbeddedGroup:main' into move-into-modules
piotrfila Jul 17, 2026
9f6357a
regz: Rename VirtualFilesystem to VirtualIo
piotrfila Jul 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions modules/virtual-io/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Virtual Io interface for Zig
A Zig Io implemention that keeps the directory structure and file contents in ram.
- All Io operations that would touch the disk go into ram, backed by a hash map
- Everything else fails
9 changes: 9 additions & 0 deletions modules/virtual-io/build.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
const std = @import("std");

pub fn build(b: *std.Build) void {
_ = b.addModule("virtual-io", .{
.root_source_file = b.path("src/root.zig"),
.target = b.standardTargetOptions(.{}),
.optimize = b.standardOptimizeOption(.{}),
});
}
11 changes: 11 additions & 0 deletions modules/virtual-io/build.zig.zon
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
.{
.name = .virtual_io,
.fingerprint = 0x9ed0871d23642986,
.version = "0.0.1",
.paths = .{
"src",
"build.zig",
"build.zig.zon",
"README.md",
},
}
291 changes: 291 additions & 0 deletions modules/virtual-io/src/root.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,291 @@
//! In-memory filesystem implementation
const std = @import("std");
const builtin = @import("builtin");

const Allocator = std.mem.Allocator;
const assert = std.debug.assert;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is unused according to our check

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only assert is unused, fixed

const Io = std.Io;
const log = std.log.scoped(.vio);

pub fn ResultID(T: type) type {
return switch (T) {
Dir, File => T.ID,
else => @compileError("Expected File or Dir, got " ++ @typeName(T)),
};
}

pub const Dir = struct {
pub const empty: Node = .{ .dir = .{ .inner = .empty } };
pub const kind: Node.Kind = empty;

pub const ID = enum(Node.ID) {
// Reserve first 256 file handles for os-specific values, such as sitdin/out/err

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// Reserve first 256 file handles for os-specific values, such as sitdin/out/err
// Reserve first 256 file handles for os-specific values, such as stdin/out/err

// on posix, cwd on wasm and the reserved NULL value on windows.
root = 0x100,
_,

pub fn from_std(dir: Io.Dir) !ID {
return from_handle(ID, dir.handle);
}

pub fn to_std(id: ID) Io.Dir {
return .{ .handle = to_handle(id) };
}

pub fn get(id: ID, vio: *const VirtualIo) !*Dir {
const node = vio.nodes.getPtr(@intFromEnum(id)) orelse
return error.Unexpected;
return switch (node.*) {
.dir => |*ret| ret,
else => error.NotDir,
};
}
};

inner: std.StringHashMapUnmanaged(Node.ID) = .empty,

pub fn open(parent: *Dir, vio: *const VirtualIo, sub_path: []const u8, T: type) !ResultID(T) {
var dir = parent;
var path_tail = sub_path;
while (std.mem.findScalar(u8, path_tail, '/')) |idx| {
const next = dir.inner.get(path_tail[0..idx]) orelse
return error.FileNotFound;
const node = vio.nodes.getPtr(next) orelse
return error.Unexpected;
switch (node.*) {
.dir => |*d| dir = d,
.file => return error.FileNotFound,
}
path_tail = path_tail[idx + 1 ..];
}
const id = dir.inner.get(path_tail) orelse
return error.FileNotFound;

// Check that node exists and is the right type
const node = vio.nodes.get(id);
if (node == null or node.? != T.kind)
return error.Unexpected;

return @enumFromInt(id);
}

pub fn create(dir: *Dir, vio: *VirtualIo, name: []const u8, T: type) !ResultID(T) {
if (std.mem.findScalar(u8, name, '/') != null) {
log.err("name includes '/': '{s}'", .{name});
return error.BadPathName;
}

const id = vio.last_id + 1;
vio.last_id = id;

const name_owned = vio.gpa.dupe(u8, name) catch return error.NoSpaceLeft;
errdefer vio.gpa.free(name_owned);

if (dir.inner.getOrPut(vio.gpa, name_owned)) |result| {
if (result.found_existing)
return error.PathAlreadyExists;

result.value_ptr.* = id;
} else |_| return error.NoSpaceLeft;

if (vio.nodes.getOrPut(vio.gpa, id)) |result| {
if (result.found_existing)
return error.PathAlreadyExists;

result.value_ptr.* = T.empty;
} else |_| return error.NoSpaceLeft;

return @enumFromInt(id);
}
};

pub const File = struct {
pub const empty: Node = .{ .file = .{ .inner = .empty } };
pub const kind: Node.Kind = empty;

pub const ID = enum(Node.ID) {
_,

pub fn from_std(file: Io.File) !ID {
return from_handle(ID, file.handle);
}

pub fn to_std(id: ID) Io.File {
return .{
.handle = to_handle(id),
.flags = .{ .nonblocking = false },
};
}

pub fn get(id: ID, vio: *const VirtualIo) !*File {
const node = vio.nodes.getPtr(@intFromEnum(id)) orelse
return error.Unexpected;
return switch (node.*) {
.file => |*ret| ret,
.dir => error.IsDir,
};
}
};

inner: std.ArrayList(u8) = .empty,

pub fn write_streaming(
vio: *const VirtualIo,
op: Io.Operation.FileWriteStreaming,
) Io.Operation.FileWriteStreaming.Result {
const file = (try File.ID.from_std(op.file)).get(vio) catch
return error.Unexpected;
var allocating: Io.Writer.Allocating = .fromArrayList(vio.gpa, &file.inner);
defer file.inner = allocating.toArrayList();
return allocating.writer.writeSplatHeader(op.header, op.data, op.splat) catch
error.NoSpaceLeft;
}
};

pub const Node = union(enum) {
pub const ID = u16;
pub const Kind = std.meta.Tag(Node);
pub const Map = std.AutoArrayHashMapUnmanaged(ID, Node);

file: File,
dir: Dir,

pub fn destroy(node: *Node, gpa: Allocator) void {
switch (node.*) {
.file => |*file| file.inner.deinit(gpa),
.dir => |*dir| {
var it = dir.inner.keyIterator();
while (it.next()) |name|
gpa.free(name.*);
dir.inner.deinit(gpa);
},
}
}
};

const VTable = struct {
fn operate(userdata: ?*anyopaque, op: Io.Operation) Io.Cancelable!Io.Operation.Result {
const vio: *VirtualIo = @ptrCast(@alignCast(userdata.?));
return switch (op) {
.file_write_streaming => |write_op| .{
.file_write_streaming = File.write_streaming(vio, write_op),
},
.file_read_streaming => .{ .file_read_streaming = error.InputOutput },
.device_io_control => unreachable,
.net_receive => .{ .net_receive = .{ error.NetworkDown, 0 } },
.net_read => .{ .net_read = error.NetworkDown },
};
}

fn create_dir_path_open(
userdata: ?*anyopaque,
dir: Io.Dir,
sub_path: []const u8,
_: Io.Dir.Permissions,
_: Io.Dir.OpenOptions,
) Io.Dir.CreateDirPathOpenError!Io.Dir {
const vio: *VirtualIo = @ptrCast(@alignCast(userdata.?));
const parent = try (try Dir.ID.from_std(dir)).get(vio);
return (try parent.create(vio, sub_path, Dir)).to_std();
}

fn dir_close(_: ?*anyopaque, _: []const Io.Dir) void {}

fn dir_create_file(
userdata: ?*anyopaque,
dir: Io.Dir,
sub_path: []const u8,
_: Io.Dir.CreateFileOptions,
) Io.File.OpenError!Io.File {
const vio: *VirtualIo = @ptrCast(@alignCast(userdata.?));
const parent = try (try Dir.ID.from_std(dir)).get(vio);
return (try parent.create(vio, sub_path, File)).to_std();
}

fn dir_open_file(
userdata: ?*anyopaque,
dir: Io.Dir,
sub_path: []const u8,
_: Io.Dir.OpenFileOptions,
) Io.File.OpenError!Io.File {
const vio: *VirtualIo = @ptrCast(@alignCast(userdata.?));
const parent = try (try Dir.ID.from_std(dir)).get(vio);
return (try parent.open(vio, sub_path, File)).to_std();
}

fn file_close(_: ?*anyopaque, _: []const Io.File) void {}
};

pub const VirtualIo = struct {
pub const root_dir = Dir.ID.root.to_std();

pub const vtable: *const Io.VTable = blk: {
var ret = Io.failing.vtable.*;
ret.operate = VTable.operate;
ret.dirCreateDirPathOpen = VTable.create_dir_path_open;
ret.dirClose = VTable.dir_close;
ret.dirCreateFile = VTable.dir_create_file;
ret.dirOpenFile = VTable.dir_open_file;
ret.fileClose = VTable.file_close;

const ret_const = ret;
break :blk &ret_const;
};

gpa: Allocator,
nodes: Node.Map,
last_id: Node.ID,

pub fn io(vio: *VirtualIo) Io {
return .{ .userdata = vio, .vtable = vtable };
}

pub fn init(gpa: Allocator) !VirtualIo {
var ret: VirtualIo = .{
.gpa = gpa,
.nodes = .empty,
.last_id = @intFromEnum(Dir.ID.root),
};
try ret.nodes.put(gpa, ret.last_id, Dir.empty);
return ret;
}

pub fn deinit(vio: *VirtualIo) void {
for (vio.nodes.values()) |*node|
node.destroy(vio.gpa);
vio.nodes.deinit(vio.gpa);
}

pub fn total_file_count(vio: *const VirtualIo) usize {
var ret: usize = 0;
var it = vio.nodes.iterator();
while (it.next()) |entry| {
if (entry.value_ptr.* == .file)
ret += 1;
}
return ret;
}

pub fn file_contents(vio: *const VirtualIo, file: Io.File) !*std.ArrayList(u8) {
return &(try (try File.ID.from_std(file)).get(vio)).inner;
}
};

pub fn from_handle(T: type, handle: std.posix.fd_t) !T {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't these return std.Io.Operation.FileWriteStreaming.Error? Since we try in write_streaming which returns that error type.

@piotrfila piotrfila Jul 17, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

from_handle is used in a few places and FileWriteStreaming.Error set just does not contain the FileNotFound error. I guess the closest would be NotOpenForWriting?
I didn't spend too much time picking these error names, in case they are encountered the trace should point to the exact error location anyway.

const Backing = @Int(.unsigned, @bitSizeOf(T));
return if (std.math.cast(Backing, switch (builtin.os.tag) {
.windows => @intFromPtr(handle),
else => handle,
})) |int|
@enumFromInt(int)
else
error.Unexpected;
}

pub fn to_handle(id: anytype) std.posix.fd_t {
// const ID = @TypeOf(id);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

xxx

return switch (builtin.os.tag) {
.windows => @ptrFromInt(@intFromEnum(id)),
else => @intFromEnum(id),
};
}
30 changes: 11 additions & 19 deletions tools/regz/build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,15 @@ pub fn build(b: *Build) !void {
.optimize = .ReleaseSafe,
});

const zqlite_dep = b.dependency("zqlite", .{
const virtual_io = b.dependency("virtual_io", .{
.target = target,
.optimize = optimize,
});
}).module("virtual-io");

const zqlite = zqlite_dep.module("zqlite");
const zqlite = b.dependency("zqlite", .{
.target = target,
.optimize = optimize,
}).module("zqlite");

const xml_module = b.createModule(.{
.root_source_file = b.path("src/xml.zig"),
Expand All @@ -33,14 +36,9 @@ pub fn build(b: *Build) !void {
.target = target,
.optimize = optimize,
.imports = &.{
.{
.name = "zqlite",
.module = zqlite,
},
.{
.name = "xml",
.module = xml_module,
},
.{ .name = "virtual-io", .module = virtual_io },
.{ .name = "xml", .module = xml_module },
.{ .name = "zqlite", .module = zqlite },
},
});
regz_module.linkLibrary(libxml2_dep.artifact("xml"));
Expand All @@ -52,14 +50,8 @@ pub fn build(b: *Build) !void {
.target = target,
.optimize = optimize,
.imports = &.{
.{
.name = "regz",
.module = regz_module,
},
.{
.name = "xml",
.module = xml_module,
},
.{ .name = "regz", .module = regz_module },
.{ .name = "xml", .module = xml_module },
},
}),
.use_llvm = true,
Expand Down
1 change: 1 addition & 0 deletions tools/regz/build.zig.zon
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
.url = "git+https://github.com/allyourcodebase/libxml2.git#e2c881ccb72dc96b03bb80f1f4c4b386969b76f7",
.hash = "libxml2-2.15.1-2-qHdjhjJXAAAF6fWNrfH_GFaXDHRNjlYQoeKF_RBnWtVu",
},
.virtual_io = .{ .path = "../../modules/virtual-io" },
.zqlite = .{
.url = "git+https://github.com/karlseguin/zqlite.zig#95054418207705ed9188b4d7467fec5136100f13",
.hash = "zqlite-0.0.1-RWLaY9UynADU9w6axCjGV9--FpxxKO_qu1qmorhTj2D9",
Expand Down
Loading
Loading