Application Context Implementation #4

Closed
kayomn wants to merge 93 commits from event-loop-dev into main
8 changed files with 647 additions and 384 deletions
Showing only changes of commit 47a997b0ec - Show all commits

View File

@ -4,31 +4,129 @@ const stack = @import("./stack.zig");
const testing = @import("./testing.zig"); const testing = @import("./testing.zig");
/// ///
/// Allocation options for an [Allocator]. /// [AccessError.Inacessible] is a generic catch-all for IO resources that are inaccessible for
/// implementation-specific reasons.
/// ///
pub const Allocation = struct { pub const AccessError = error {
existing: ?[*]u8, Inaccessible,
alignment: u29,
size: usize,
}; };
/// ///
/// Closure for dynamic memory allocation through the referenced allocator state machine capture. /// [AllocationError.OutOfMemory] if the requested amount of memory could not be allocated.
/// ///
pub const Allocator = meta.Function(Allocation, ?[*]u8); pub const AllocationError = error {
///
/// [MakeError.OutOfMemory] if the requested amount of memory could not be allocated.
///
pub const MakeError = error {
OutOfMemory, OutOfMemory,
}; };
/// ///
/// Closure that captures a reference to readable resources like block devices, memory buffers, /// Memory layout description for a memory allocation.
/// network sockets, and more.
/// ///
pub const Reader = meta.Function([]u8, usize); pub const AllocationLayout = struct {
length: usize,
alignment: u29 = 8,
};
///
/// Interface for dynamic memory allocation through the state machine of the wrapped allocator
/// implementation.
///
pub const Allocator = struct {
context: *anyopaque,
vtable: *const struct {
alloc: fn (*anyopaque, AllocationLayout) AllocationError![*]u8,
dealloc: fn (*anyopaque, [*]u8) void,
realloc: fn (*anyopaque, [*]u8, AllocationLayout) AllocationError![*]u8,
},
///
/// Attempts to allocate a block of memory from `allocator` according to `layout`, returning it
/// or [AllocationError] if it failed.
///
pub fn alloc(allocator: Allocator, layout: AllocationLayout) AllocationError![*]u8 {
return allocator.vtable.alloc(allocator.context, layout);
}
///
/// Deallocates the block of memory from `allocator` referenced by `allocation`.
///
pub fn dealloc(allocator: Allocator, allocation: [*]u8) void {
allocator.vtable.dealloc(allocator.context, allocation);
}
///
/// Attempts to reallocate the existing block of memory from `allocator` referenced by
/// `allocation` according to `layout`, returning it or [AllocationError] if it failed.
///
pub fn realloc(allocator: Allocator, allocation: [*]u8,
layout: AllocationLayout) AllocationError![*]u8 {
return allocator.vtable.realloc(allocator.context, allocation, layout);
}
///
/// Wraps `implementation`, returning the [Allocator] value.
///
pub fn wrap(implementation: anytype) Allocator {
const Implementation = @TypeOf(implementation.*);
return .{
.context = @ptrCast(*anyopaque, implementation),
.vtable = switch (@typeInfo(Implementation)) {
.Struct => &.{
.alloc = struct {
fn call(context: *anyopaque, layout: AllocationLayout) AllocationError![*]u8 {
return @ptrCast(*Implementation, @alignCast(
@alignOf(Implementation), context)).alloc(layout);
}
}.call,
.dealloc = struct {
fn call(context: *anyopaque, allocation: [*]u8) void {
return @ptrCast(*Implementation, @alignCast(
@alignOf(Implementation), context)).dealloc(allocation);
}
}.call,
.realloc = struct {
fn call(context: *anyopaque, allocation: [*]u8,
layout: AllocationLayout) AllocationError![*]u8 {
return @ptrCast(*Implementation, @alignCast(
@alignOf(Implementation), context)).realloc(allocation, layout);
}
}.call,
},
.Opaque => &.{
.alloc = struct {
fn call(context: *anyopaque, layout: AllocationLayout) AllocationError![*]u8 {
return @ptrCast(*Implementation, context).alloc(layout);
}
}.call,
.dealloc = struct {
fn call(context: *anyopaque, allocation: [*]u8) void {
return @ptrCast(*Implementation, context).dealloc(allocation);
}
}.call,
.realloc = struct {
fn call(context: *anyopaque, allocation: [*]u8,
layout: AllocationLayout) AllocationError![*]u8 {
return @ptrCast(*Implementation, context).realloc(allocation, layout);
}
}.call,
},
else => @compileError(
"`context` must a single-element pointer referencing a struct or opaque type"),
},
};
}
};
/// ///
/// Returns a state machine for lazily computing all `Element` components of a given source input /// Returns a state machine for lazily computing all `Element` components of a given source input
@ -143,10 +241,57 @@ test "Spliterator(u8)" {
} }
/// ///
/// Closure that captures a reference to writable resources like block devices, memory buffers, /// Interface for capturing a reference to a writable resource like block devices, memory buffers,
/// network sockets, and more. /// network sockets, and more.
/// ///
pub const Writer = meta.Function([]const u8, usize); pub const Writer = struct {
context: *anyopaque,
vtable: *const struct {
write: fn (*anyopaque, []const u8) AccessError!usize,
},
///
/// Wraps `implementation`, returning the [Writer] value.
///
pub fn wrap(implementation: anytype) Writer {
const Implementation = @TypeOf(implementation.*);
return .{
.context = @ptrCast(*anyopaque, implementation),
.vtable = switch (@typeInfo(Implementation)) {
.Struct => &.{
.write = struct {
fn call(context: *anyopaque, buffer: []const u8) AccessError!usize {
return @ptrCast(*Implementation,
@alignCast(@alignOf(Implementation), context)).write(buffer);
}
}.call,
},
.Opaque => &.{
.write = struct {
fn call(context: *anyopaque, buffer: []const u8) AccessError!usize {
return @ptrCast(*Implementation, context).write(buffer);
}
}.call,
},
else => @compileError(
"`context` must a single-element pointer referencing a struct or opaque type"),
},
};
}
///
/// Attempts to write to `buffer` to `writer`, returning the number of successfully written or
/// [AccessError] if it failed.
///
pub fn write(writer: Writer, buffer: []const u8) AccessError!usize {
return writer.vtable.write(writer.context, buffer);
}
};
/// ///
/// Returns a sliced reference of the raw bytes in `pointer`. /// Returns a sliced reference of the raw bytes in `pointer`.
@ -320,19 +465,14 @@ test "findFirstOf" {
/// `allocated_memory`. Anything else will result is considered unreachable logic. /// `allocated_memory`. Anything else will result is considered unreachable logic.
/// ///
pub fn free(allocator: Allocator, allocated_memory: anytype) void { pub fn free(allocator: Allocator, allocated_memory: anytype) void {
if (allocator.call(.{ allocator.dealloc(@ptrCast([*]u8, switch (@typeInfo(@TypeOf(allocated_memory))) {
.existing = @ptrCast([*]u8, switch (@typeInfo(@TypeOf(allocated_memory))) {
.Pointer => |info| switch (info.size) { .Pointer => |info| switch (info.size) {
.One, .Many, .C => allocated_memory, .One, .Many, .C => allocated_memory,
.Slice => allocated_memory.ptr, .Slice => allocated_memory.ptr,
}, },
else => @compileError("`allocated_memory` must be a pointer"), else => @compileError("`allocated_memory` must be a pointer"),
}), }));
.size = 0,
.alignment = 0,
}) != null) unreachable;
} }
test "free" { test "free" {
@ -369,14 +509,13 @@ test "hashBytes" {
/// Attempts to allocate a buffer of `size` `Element`s using `allocator`, returning it or a /// Attempts to allocate a buffer of `size` `Element`s using `allocator`, returning it or a
/// [MakeError] if it failed. /// [MakeError] if it failed.
/// ///
pub fn makeMany(comptime Element: type, allocator: Allocator, size: usize) MakeError![*]Element { pub fn makeMany(comptime Element: type, allocator: Allocator, size: usize) AllocationError![*]Element {
const alignment = @alignOf(Element); const alignment = @alignOf(Element);
return @ptrCast([*]Element, @alignCast(alignment, allocator.call(.{ return @ptrCast([*]Element, @alignCast(alignment, try allocator.alloc(.{
.existing = null, .length = @sizeOf(Element) * size,
.size = @sizeOf(Element) * size,
.alignment = alignment, .alignment = alignment,
}) orelse return error.OutOfMemory)); })));
} }
test "makeMany" { test "makeMany" {
@ -392,14 +531,13 @@ test "makeMany" {
/// Attempts to allocate a buffer of `1` `Element` using `allocator`, returning it or a [MakeError] /// Attempts to allocate a buffer of `1` `Element` using `allocator`, returning it or a [MakeError]
/// if it failed. /// if it failed.
/// ///
pub fn makeOne(comptime Element: type, allocator: Allocator) MakeError!*Element { pub fn makeOne(comptime Element: type, allocator: Allocator) AllocationError!*Element {
const alignment = @alignOf(Element); const alignment = @alignOf(Element);
return @ptrCast(*Element, @alignCast(alignment, allocator.call(.{ return @ptrCast(*Element, @alignCast(alignment, try allocator.alloc(.{
.existing = null, .length = @sizeOf(Element),
.size = @sizeOf(Element),
.alignment = alignment, .alignment = alignment,
}) orelse return error.OutOfMemory)); })));
} }
test "makeOne" { test "makeOne" {
@ -429,6 +567,11 @@ test "swap" {
try testing.expect(b == 0); try testing.expect(b == 0);
} }
///
/// Mandatory context variable used by [null_writer].
///
const null_context: u64 = 0;
/// ///
/// Thread-safe and lock-free [Writer] that silently consumes all given data without failure and /// Thread-safe and lock-free [Writer] that silently consumes all given data without failure and
/// throws it away. /// throws it away.
@ -436,11 +579,13 @@ test "swap" {
/// This is commonly used for testing or redirected otherwise unwanted output data that has to be /// This is commonly used for testing or redirected otherwise unwanted output data that has to be
/// sent somewhere for whatever reason. /// sent somewhere for whatever reason.
/// ///
pub const null_writer = Writer.from(struct { pub const null_writer = Writer.wrap(@ptrCast(*const opaque {
fn write(buffer: []const u8) usize { const Self = @This();
fn write(_: Self, buffer: []const u8) usize {
return buffer.len; return buffer.len;
} }
}.write); }, &null_context));
test "null_writer" { test "null_writer" {
const sequence = "foo"; const sequence = "foo";

View File

@ -8,77 +8,3 @@ pub fn FnReturn(comptime Fn: type) type {
return type_info.Fn.return_type orelse void; return type_info.Fn.return_type orelse void;
} }
///
/// Returns a single-input single-output closure type where `In` represents the input type, `Out`
/// represents the output type, and `captures_size` represents the size of the closure context.
///
pub fn Function(comptime In: type, comptime Out: type) type {
return struct {
callErased: fn (*anyopaque, In) Out,
context: *anyopaque,
fn Invoker(comptime Context: type) type {
return if (Context == void) fn (In) Out else fn (Context, In) Out;
}
///
/// Function type.
///
const Self = @This();
///
/// Invokes `self` with `input`, producing a result according to the current context data.
///
pub fn call(self: Self, input: In) Out {
return self.callErased(self.context, input);
}
///
/// Creates and returns a [Self] using the `invoke` as the behavior executed when [call] or
/// [callErased] is called.
///
/// For creating a closure-style function, see [fromClosure].
///
pub fn from(comptime invoke: fn (In) Out) Self {
return .{
.context = undefined,
.callErased = struct {
fn callErased(_: *anyopaque, input: In) Out {
return invoke(input);
}
}.callErased,
};
}
///
/// Creates and returns a [Self] by capturing the `context` value as the capture context and
/// `invoke` as the behavior executed when [call] or [callErased] is called.
///
/// The newly created [Self] is returned.
///
pub fn fromClosure(context: anytype, comptime invoke: fn (@TypeOf(context), In) Out) Self {
const Context = @TypeOf(context);
switch (@typeInfo(Context)) {
.Pointer => |info| if (info.size == .Slice)
@compileError("`context` cannot be a slice"),
.Void => {},
else => @compileError("`context` must be a pointer"),
}
return Self{
.context = @ptrCast(*anyopaque, context),
.callErased = struct {
fn callErased(erased: *anyopaque, input: In) Out {
return if (Context == void) invoke(input) else invoke(@ptrCast(
Context, @alignCast(@alignOf(Context), erased)), input);
}
}.callErased,
};
}
};
}

View File

@ -145,38 +145,69 @@ pub const FixedPushError = error {
/// memory pool to linearly allocate memory from. /// memory pool to linearly allocate memory from.
/// ///
pub fn fixedAllocator(fixed_stack: *Fixed(u8)) io.Allocator { pub fn fixedAllocator(fixed_stack: *Fixed(u8)) io.Allocator {
return io.Allocator.fromClosure(fixed_stack, struct { const FixedStack = @TypeOf(fixed_stack.*);
fn alloc(stack: *Fixed(u8), allocation: io.Allocation) ?[*]u8 {
if (allocation.existing) |buffer| if (allocation.size == 0) {
// Deallocate the memory.
const buffer_address = @ptrToInt(buffer);
const stack_address = @ptrToInt(stack.buffer.ptr);
// Check the buffer is within the address space of the stack buffer. If not, it return io.Allocator.wrap(@ptrCast(*opaque {
// should just be returned to let the caller know it cannot be freed. const Self = @This();
if (buffer_address < stack_address or buffer_address >=
(stack_address + stack.filled)) return buffer;
// TODO: Investigate ways of actually freeing if it is the last allocation. pub fn alloc(self: *Self, layout: io.AllocationLayout) io.AllocationError![*]u8 {
return null;
} else {
// TODO: Investigate ways of in-place relocating if it is the last allocation.
};
// Reallocate / allocate the memory.
// TODO: Remove stdlib dependency. // TODO: Remove stdlib dependency.
const stack = self.stackCast();
const adjusted_offset = @import("std").mem.alignPointerOffset(stack.buffer.ptr + const adjusted_offset = @import("std").mem.alignPointerOffset(stack.buffer.ptr +
stack.filled, allocation.alignment) orelse return null; stack.filled, layout.alignment) orelse return error.OutOfMemory;
const head = stack.filled + adjusted_offset; const head = stack.filled + adjusted_offset;
const tail = head + allocation.size; const tail = head + layout.length;
stack.pushMany(0, tail) catch return null; stack.pushMany(0, tail) catch return error.OutOfMemory;
return stack.buffer[head .. tail].ptr; return stack.buffer[head .. tail].ptr;
} }
}.alloc);
pub fn dealloc(self: *Self, allocation: [*]u8) void {
// Deallocate the memory.
const stack = self.stackCast();
const allocation_address = @ptrToInt(allocation);
const stack_address = @ptrToInt(stack.buffer.ptr);
// Check the buffer is within the address space of the stack buffer. If not, it cannot
// be freed.
if (allocation_address < stack_address or allocation_address >=
(stack_address + stack.filled)) unreachable;
// TODO: Investigate ways of actually freeing if it is the last allocation.
}
pub fn realloc(self: *Self, allocation: [*]u8,
layout: io.AllocationLayout) io.AllocationError![*]u8 {
// TODO: Investigate ways of in-place relocating if it is the last allocation.
// TODO: Remove stdlib dependency.
const stack = self.stackCast();
const allocation_address = @ptrToInt(allocation);
const stack_address = @ptrToInt(stack.buffer.ptr);
// Check the buffer is within the address space of the stack buffer. If not, it cannot
// be reallocated.
if (allocation_address < stack_address or allocation_address >=
(stack_address + stack.filled)) unreachable;
const adjusted_offset = @import("std").mem.alignPointerOffset(stack.buffer.ptr +
stack.filled, layout.alignment) orelse return error.OutOfMemory;
const head = stack.filled + adjusted_offset;
const tail = head + layout.length;
stack.pushMany(0, tail) catch return error.OutOfMemory;
return stack.buffer[head .. tail].ptr;
}
fn stackCast(self: *Self) *Fixed(u8) {
return @ptrCast(*FixedStack, @alignCast(@alignOf(FixedStack), self));
}
}, fixed_stack));
} }
test "fixedAllocator" { test "fixedAllocator" {
@ -185,14 +216,11 @@ test "fixedAllocator" {
const allocator = fixedAllocator(&stack); const allocator = fixedAllocator(&stack);
// Allocation // Allocation
var block_memory = allocator.call(.{ var block_memory = try allocator.alloc(.{
.existing = null,
.alignment = @alignOf(u64), .alignment = @alignOf(u64),
.size = @sizeOf(u64), .length = @sizeOf(u64),
}); });
try testing.expect(block_memory != null);
const buffer_address_head = @ptrToInt(&buffer); const buffer_address_head = @ptrToInt(&buffer);
const buffer_address_tail = @ptrToInt(&buffer) + buffer.len; const buffer_address_tail = @ptrToInt(&buffer) + buffer.len;
@ -204,14 +232,11 @@ test "fixedAllocator" {
} }
// Reallocation. // Reallocation.
block_memory = allocator.call(.{ block_memory = try allocator.realloc(block_memory, .{
.existing = block_memory,
.alignment = @alignOf(u64), .alignment = @alignOf(u64),
.size = @sizeOf(u64), .length = @sizeOf(u64),
}); });
try testing.expect(block_memory != null);
{ {
const block_memory_address = @ptrToInt(block_memory); const block_memory_address = @ptrToInt(block_memory);
@ -220,11 +245,7 @@ test "fixedAllocator" {
} }
// Deallocation. // Deallocation.
try testing.expect(allocator.call(.{ allocator.dealloc(block_memory);
.existing = block_memory,
.alignment = 0,
.size = 0,
}) == null);
} }
/// ///
@ -234,15 +255,23 @@ test "fixedAllocator" {
/// referenced by `fixed_stack` until it is full. /// referenced by `fixed_stack` until it is full.
/// ///
pub fn fixedWriter(fixed_stack: *Fixed(u8)) io.Writer { pub fn fixedWriter(fixed_stack: *Fixed(u8)) io.Writer {
return io.Writer.fromClosure(fixed_stack, struct { const FixedStack = @TypeOf(fixed_stack.*);
fn write(stack: *Fixed(u8), buffer: []const u8) usize {
stack.pushAll(buffer) catch |err| switch (err) { return io.Writer.wrap(@ptrCast(*opaque {
const Self = @This();
fn stackCast(self: *Self) *Fixed(u8) {
return @ptrCast(*FixedStack, @alignCast(@alignOf(FixedStack), self));
}
pub fn write(self: *Self, buffer: []const u8) io.AccessError!usize {
self.stackCast().pushAll(buffer) catch |err| switch (err) {
error.BufferOverflow => return 0, error.BufferOverflow => return 0,
}; };
return buffer.len; return buffer.len;
} }
}.write); }, fixed_stack));
} }
test "fixedWriter" { test "fixedWriter" {
@ -250,6 +279,8 @@ test "fixedWriter" {
var sequence_stack = Fixed(u8){.buffer = &buffer}; var sequence_stack = Fixed(u8){.buffer = &buffer};
const sequence_data = [_]u8{8, 16, 32, 64}; const sequence_data = [_]u8{8, 16, 32, 64};
try testing.expect(fixedWriter(&sequence_stack).call(&sequence_data) == sequence_data.len); try testing.expect((try fixedWriter(&sequence_stack).
write(&sequence_data)) == sequence_data.len);
try testing.expect(io.equals(u8, sequence_stack.buffer, &sequence_data)); try testing.expect(io.equals(u8, sequence_stack.buffer, &sequence_data));
} }

View File

@ -29,6 +29,11 @@ pub fn Hashed(comptime Key: type, comptime Value: type,
maybe_next_index: ?usize = null, maybe_next_index: ?usize = null,
}; };
///
/// Errors that may occur during initialization of a hash table.
///
pub const InitError = io.AllocationError;
/// ///
/// Hash table type. /// Hash table type.
/// ///
@ -46,9 +51,9 @@ pub fn Hashed(comptime Key: type, comptime Value: type,
/// ///
/// Initializes a [Self] using `allocator` as the memory allocation strategy. /// Initializes a [Self] using `allocator` as the memory allocation strategy.
/// ///
/// Returns a new [Self] value or an [io.MakeError] if initializing failed. /// Returns a new [Self] value or an [InitError] if initializing failed.
/// ///
pub fn init(allocator: Allocator) io.MakeError!Self { pub fn init(allocator: Allocator) InitError!Self {
const capacity = 4; const capacity = 4;
return Self{ return Self{
@ -166,7 +171,7 @@ pub fn Hashed(comptime Key: type, comptime Value: type,
/// [InsertError.KeyExists] occurs when an insertion was attempted on a table with a matching key /// [InsertError.KeyExists] occurs when an insertion was attempted on a table with a matching key
/// already present. /// already present.
/// ///
pub const InsertError = io.MakeError || error { pub const InsertError = io.AllocationError || error {
KeyExists, KeyExists,
}; };

View File

@ -7,7 +7,7 @@ const testing = @import("./testing.zig");
/// [PrintError.WriteFailure] occurs when the underlying [io.Writer] implementation failed to write /// [PrintError.WriteFailure] occurs when the underlying [io.Writer] implementation failed to write
/// the entirety of a the requested print operation. /// the entirety of a the requested print operation.
/// ///
pub const PrintError = error { pub const PrintError = io.AccessError || error {
WriteFailure, WriteFailure,
}; };
@ -69,7 +69,7 @@ pub fn printInt(writer: io.Writer, radix: Radix, value: anytype) PrintError!void
if (value == 0) { if (value == 0) {
const zero = "0"; const zero = "0";
if (writer.call(zero) != zero.len) return error.WriteFailure; if ((try writer.write(zero)) != zero.len) return error.WriteFailure;
} else { } else {
// Big enough to hold the hexadecimal representation of the integer type, which is // Big enough to hold the hexadecimal representation of the integer type, which is
// the largest number format accomodated for in [Radix]. // the largest number format accomodated for in [Radix].
@ -95,7 +95,7 @@ pub fn printInt(writer: io.Writer, radix: Radix, value: anytype) PrintError!void
for (buffer[0 .. (buffer_count / 2)]) |_, i| for (buffer[0 .. (buffer_count / 2)]) |_, i|
io.swap(u8, &buffer[i], &buffer[buffer_count - i - 1]); io.swap(u8, &buffer[i], &buffer[buffer_count - i - 1]);
if (writer.call(buffer[0 .. buffer_count]) != buffer_count) if ((try writer.write(buffer[0 .. buffer_count])) != buffer_count)
return error.WriteFailure; return error.WriteFailure;
} }
}, },

View File

@ -13,22 +13,19 @@ pub fn main() anyerror!void {
/// Runs the game engine. /// Runs the game engine.
/// ///
fn runEngine(app: *sys.App, graphics: *sys.Graphics) anyerror!void { fn runEngine(app: *sys.App, graphics: *sys.Graphics) anyerror!void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
{ {
var file_reader = try app.data.openRead(try sys.Path.from(&.{"ona.lua"})); const path = try sys.Path.from(&.{"ona.lua"});
var file_reader = try app.data.openFileReader(path);
defer file_reader.close(); defer file_reader.close();
const file_size = try file_reader.size(); const file_size = (try app.data.query(path)).length;
const allocator = gpa.allocator(); const allocator = sys.threadSafeAllocator();
const buffer = try allocator.alloc(u8, file_size); const buffer = (try core.io.makeMany(u8, allocator, file_size))[0 .. file_size];
defer allocator.free(buffer); defer core.io.free(allocator, buffer);
if ((try file_reader.read(0, buffer)) != file_size) return error.ScriptLoadFailure; if ((try file_reader.read(buffer)) != file_size) return error.ScriptLoadFailure;
app.log(.debug, buffer); app.log(.debug, buffer);
} }

View File

@ -2,13 +2,13 @@ const core = @import("core");
const sys = @import("./sys.zig"); const sys = @import("./sys.zig");
/// ///
/// Metadata of an Oar archive entry. /// Metadata of an Oar archive file entry.
/// ///
const Block = extern struct { const Entry = extern struct {
signature: [signature_magic.len]u8 = signature_magic, signature: [signature_magic.len]u8 = signature_magic,
path: sys.Path = sys.Path.empty, path: sys.Path = sys.Path.empty,
data_size: u64 = 0, data_offset: u64 = 0,
data_head: u64 = 0, data_length: u64 = 0,
padding: [232]u8 = [_]u8{0} ** 232, padding: [232]u8 = [_]u8{0} ** 232,
comptime { comptime {
@ -19,84 +19,14 @@ const Block = extern struct {
}; };
/// ///
/// Reference to a file entry in an Oar archive, denoting the starting offset from the top of head /// [FindError.ArchiveUnsupported] occurs when trying to read a file that does not follow an Oar
/// of the file and its size. /// archive format considered valid by this implemenatation.
/// ///
pub const Entry = struct { /// [FindError.EntryNotFound] occurs when the queried entry was not found in the archive file.
head: u64, ///
size: u64, pub const FindError = core.io.AccessError || error {
///
/// [FindError.EntryNotFound] occurs when no entry matched the parameters of the find operation.
///
/// [FindError.ArchiveUnsupported] occurs if the file provided to the find operation is not a
/// valid archive file.
///
pub const FindError = error {
EntryNotFound,
ArchiveUnsupported, ArchiveUnsupported,
}; EntryNotFound,
///
/// Attempts to perform a binary search on the entry blocks defined in `archive_file` for one
/// matching `entry_path`, returning an [Entry] referencing its data or a [FindError] if it
/// failed.
///
/// **Note** that this operation has `O(log n)` time complexity.
///
pub fn find(archive_file: *sys.ReadableFile, entry_path: sys.Path) FindError!Entry {
var header = Header{};
const header_size = @sizeOf(Header);
const io = core.io;
if (((archive_file.read(0, io.bytesOf(&header)) catch
return error.ArchiveUnsupported) != header_size) or
(!io.equals(u8, &header.signature, &signature_magic)) or
(header.revision != revision_magic) or
(header.entry_head <= header_size)) return error.ArchiveUnsupported;
// Read file table.
var head: usize = 0;
var tail: usize = (header.entry_count - 1);
const block_size = @sizeOf(Block);
while (head <= tail) {
var block = Block{};
const midpoint = (head + (tail - head) / 2);
if ((archive_file.read(header.entry_head + (block_size * midpoint), io.bytesOf(&block))
catch return error.ArchiveUnsupported) != block_size) return error.EntryNotFound;
const comparison = entry_path.compare(block.path);
if (comparison == 0) return Entry{
.head = block.data_head,
.size = block.data_size,
};
if (comparison > 0) {
head = (midpoint + 1);
} else {
tail = (midpoint - 1);
}
}
return error.EntryNotFound;
}
///
/// Reads the data from `entry` in `archive_file` from the byte at the entry-relative `offset`
/// into `buffer` until either the end of the entry data, end of archive file, or end of buffer
/// is reached.
///
/// The number of bytes read is returned or [sys.FileError] if it failed.
///
pub fn read(entry: Entry, archive_file: *sys.ReadableFile,
offset: u64, buffer: []u8) sys.FileError!usize {
return archive_file.read(entry.head + offset,
buffer[0 .. core.math.min(usize, buffer.len, entry.size)]);
}
}; };
/// ///
@ -106,8 +36,7 @@ const Header = extern struct {
signature: [signature_magic.len]u8 = signature_magic, signature: [signature_magic.len]u8 = signature_magic,
revision: u8 = revision_magic, revision: u8 = revision_magic,
entry_count: u32 = 0, entry_count: u32 = 0,
entry_head: u64 = 0, padding: [502]u8 = [_]u8{0} ** 502,
padding: [496]u8 = [_]u8{0} ** 496,
comptime { comptime {
const size = @sizeOf(@This()); const size = @sizeOf(@This());
@ -117,7 +46,54 @@ const Header = extern struct {
}; };
/// ///
/// The magic revision number that this Oar software implementation understands. /// Attempts to find an [Entry] with a path name matching `path` in `archive_reader`.
///
/// An [Entry] value is returned if a match was found, otherwise [FindError] if it failed.
///
pub fn findEntry(archive_reader: sys.FileReader, path: sys.Path) FindError!Entry {
var header = Header{};
const header_size = @sizeOf(Header);
const io = core.io;
if ((try archive_reader.read(io.bytesOf(&header))) != header_size)
return error.ArchiveUnsupported;
if (!io.equals(u8, &header.signature, &signature_magic))
return error.ArchiveUnsupported;
if (header.revision != revision_magic) return error.ArchiveUnsupported;
// Read file table.
var head: u64 = 0;
var tail: u64 = (header.entry_count - 1);
const entry_size = @sizeOf(Entry);
while (head <= tail) {
var entry = Entry{};
const midpoint = head + ((tail - head) / 2);
const offset = header_size + (entry_size * midpoint);
try archive_reader.seek(offset);
if ((try archive_reader.read(io.bytesOf(&entry))) != entry_size)
return error.ArchiveUnsupported;
const comparison = path.compare(entry.path);
if (comparison == 0) return entry;
if (comparison > 0) {
head = (midpoint + 1);
} else {
tail = (midpoint - 1);
}
}
return error.EntryNotFound;
}
///
/// Magic revision number that this Oar software implementation understands.
/// ///
const revision_magic = 0; const revision_magic = 0;

View File

@ -93,86 +93,106 @@ pub const App = struct {
}; };
/// ///
/// Snapshotted information about the status of a file.
/// ///
/// pub const FileStatus = struct {
pub const ReadableFile = opaque { length: u64,
///
///
///
pub fn close(readable_file: *ReadableFile) void {
if (ext.SDL_RWclose(readable_file.rwOpsCast()) != 0)
return ext.SDL_LogWarn(ext.SDL_LOG_CATEGORY_APPLICATION,
"Attempt to close an invalid file reference");
}
///
///
///
pub fn read(readable_file: *ReadableFile, offset: u64, buffer: []u8) FileError!u64 {
const rw_ops = readable_file.rwOpsCast();
{
ext.SDL_ClearError();
const math = core.math;
const min = math.min;
const maxIntValue = math.maxIntValue;
var sought = min(u64, offset, maxIntValue(i64));
if (ext.SDL_RWseek(rw_ops, @intCast(i64, sought), ext.RW_SEEK_SET) < 0)
return error.FileInaccessible;
var to_seek = offset - sought;
while (to_seek != 0) {
sought = min(u64, to_seek, maxIntValue(i64));
ext.SDL_ClearError();
if (ext.SDL_RWseek(rw_ops, @intCast(i64, sought), ext.RW_SEEK_CUR) < 0)
return error.FileInaccessible;
to_seek -= sought;
}
}
ext.SDL_ClearError();
const buffer_read = ext.SDL_RWread(rw_ops, buffer.ptr, @sizeOf(u8), buffer.len);
if ((buffer_read == 0) and (ext.SDL_GetError() != null))
return error.FileInaccessible;
return buffer_read;
}
///
///
///
pub fn rwOpsCast(readable_file: *ReadableFile) *ext.SDL_RWops {
return @ptrCast(*ext.SDL_RWops, @alignCast(@alignOf(ext.SDL_RWops), readable_file));
}
///
///
///
pub fn size(readable_file: *ReadableFile) FileError!u64 {
ext.SDL_ClearError();
const byte_size = ext.SDL_RWsize(readable_file.rwOpsCast());
if (byte_size < 0) return error.FileInaccessible;
return @intCast(u64, byte_size);
}
}; };
/// ///
kayomn marked this conversation as resolved
Review

Missing documentation comment.

Missing documentation comment.
/// [Error.FileInaccessible] is a generic catch-all for a [FileAccess] reference no longer pointing /// Interface for working with bi-directional, streamable resources accessed through a file-system.
/// to a file or the file becomming invalid for whatever reason.
/// ///
pub const FileError = error { pub const FileReader = struct {
FileInaccessible, context: *anyopaque,
vtable: *const struct {
close: fn (*anyopaque) void,
read: fn (*anyopaque, []u8) core.io.AccessError!u64,
seek: fn (*anyopaque, u64) core.io.AccessError!void,
},
///
/// Closes the `file_reader`, logging a wraning if the `file_reader` is already considered
/// closed.
///
pub fn close(file_reader: FileReader) void {
file_reader.vtable.close(file_reader.context);
}
///
/// Attempts to read from `file_reader` into `buffer`, returning the number of bytes
/// successfully read or [core.io.AccessError] if it failed.
///
pub fn read(file_reader: FileReader, buffer: []u8) core.io.AccessError!u64 {
return file_reader.vtable.read(file_reader.context, buffer);
}
///
/// Attempts to seek from the beginning of `file_reader` to `cursor` bytes in, returning
/// [core.io.AccessError] if it failed.
///
pub fn seek(file_reader: FileReader, cursor: u64) core.io.AccessError!void {
return file_reader.vtable.seek(file_reader.context, cursor);
}
///
/// Wraps `implementation`, returning a [FileReader] value.
///
pub fn wrap(implementation: anytype) FileReader {
const Implementation = @TypeOf(implementation.*);
return .{
.context = @ptrCast(*anyopaque, implementation),
.vtable = switch (@typeInfo(Implementation)) {
.Struct => &.{
.close = struct {
fn call(context: *anyopaque) void {
@ptrCast(*Implementation, @alignCast(
@alignOf(Implementation), context)).close();
}
}.call,
.read = struct {
fn call(context: *anyopaque, buffer: []u8) core.io.AccessError!u64 {
return @ptrCast(*Implementation, @alignCast(
@alignOf(Implementation), context)).read(buffer);
}
}.call,
.seek = struct {
fn call(context: *anyopaque, cursor: u64) core.io.AccessError!void {
return @ptrCast(*Implementation, @alignCast(
@alignOf(Implementation), context)).seek(cursor);
}
}.call,
},
.Opaque => &.{
.close = struct {
fn call(context: *anyopaque) void {
@ptrCast(*Implementation, context).close();
}
}.call,
.read = struct {
fn call(context: *anyopaque, buffer: []u8) core.io.AccessError!u64 {
return @ptrCast(*Implementation, context).read(buffer);
}
}.call,
.seek = struct {
fn call(context: *anyopaque, cursor: u64) core.io.AccessError!void {
return @ptrCast(*Implementation, context).seek(cursor);
}
}.call,
},
else => @compileError(
"`context` must a single-element pointer referencing a struct or opaque type"),
},
};
}
}; };
/// ///
@ -181,42 +201,84 @@ pub const FileError = error {
/// ///
pub const FileSystem = union(enum) { pub const FileSystem = union(enum) {
native: []const u8, native: []const u8,
archive_file: *ReadableFile,
archive: struct {
file_system: *const FileSystem,
path: Path,
},
/// ///
/// With files typically being backed by a block device, they can produce a variety of errors - /// [AccessError.FileNotFound] occurs when a queried file could not be found on the file-system
/// from physical to virtual errors - these are all encapsulated by the API as general /// by the process. This may mean the file does not exist, however it may also mean that the
/// [OpenError.FileNotFound] errors. /// process does not have sufficient rights to read it.
/// ///
/// When a given [FileSystem] does not support a specified [OpenMode], /// [AccessError.FileSystemfailure] denotes a file-system implementation-specific failure to
/// [OpenError.ModeUnsupported] is used to inform the consuming code that another [OpenMode] /// access resources has occured and therefore cannot proceed to access the file.
/// should be tried or, if no mode other is suitable, that the resource is effectively
/// unavailable.
/// ///
/// If the number of known [FileAccess] handles has been exhausted, [OpenError.OutOfFiles] is pub const AccessError = error {
/// used to communicate this.
///
pub const OpenError = error {
FileNotFound, FileNotFound,
ModeUnsupported, FileSystemFailure,
OutOfFiles,
}; };
/// ///
/// Attempts to open the file identified by `path` with `mode` as the mode for opening the file. /// Attempts to open the file identified by `path` on `file_system` for reading, returning a
/// [FileReader] value that provides access to the opened file or [AccessError] if it failed.
/// ///
/// Returns a [ReadableFile] reference that provides access to the file referenced by `path`or a pub fn openFileReader(file_system: FileSystem, path: Path) AccessError!FileReader {
/// [OpenError] if it failed. switch (file_system) {
/// .archive => |archive| {
pub fn openRead(file_system: *const FileSystem, path: Path) OpenError!*ReadableFile { const archive_reader = try archive.file_system.openFileReader(archive.path);
switch (file_system.*) {
.archive_file => |archive_file| {
const entry = oar.Entry.find(archive_file, path) catch return error.FileNotFound;
_ = entry; errdefer archive_reader.close();
// TODO: Alloc file context.
return error.FileNotFound; const entry = oar.findEntry(archive_reader, path) catch |err| return switch (err) {
error.ArchiveUnsupported, error.Inaccessible => error.FileSystemFailure,
error.EntryNotFound => error.FileNotFound,
};
archive_reader.seek(entry.data_offset) catch return error.FileSystemFailure;
const io = core.io;
const allocator = threadSafeAllocator();
const entry_reader = io.makeOne(struct {
allocator: io.Allocator,
base_reader: FileReader,
cursor: u64,
offset: u64,
length: u64,
const Self = @This();
pub fn close(self: *Self) void {
self.base_reader.close();
io.free(self.allocator, self);
}
pub fn read(self: *Self, buffer: []u8) io.AccessError!u64 {
try self.base_reader.seek(self.offset + self.cursor);
return self.base_reader.read(buffer[0 ..
core.math.min(usize, buffer.len, self.length)]);
}
pub fn seek(self: *Self, cursor: u64) io.AccessError!void {
self.cursor = cursor;
}
}, allocator) catch return error.FileSystemFailure;
errdefer io.free(allocator, entry_reader);
entry_reader.* = .{
.allocator = allocator,
.base_reader = archive_reader,
.cursor = 0,
.offset = entry.data_offset,
.length = entry.data_length,
};
return FileReader.wrap(entry_reader);
}, },
.native => |native| { .native => |native| {
@ -239,8 +301,124 @@ pub const FileSystem = union(enum) {
ext.SDL_ClearError(); ext.SDL_ClearError();
return @ptrCast(*ReadableFile, ext.SDL_RWFromFile(&path_buffer, "rb") const rw_ops =
orelse return error.FileNotFound); ext.SDL_RWFromFile(&path_buffer, "rb") orelse return error.FileNotFound;
errdefer _ = ext.SDL_RWclose(rw_ops);
return FileReader.wrap(@ptrCast(*opaque {
const Self = @This();
fn rwOpsCast(self: *Self) *ext.SDL_RWops {
return @ptrCast(*ext.SDL_RWops, @alignCast(@alignOf(ext.SDL_RWops), self));
}
pub fn read(self: *Self, buffer: []u8) core.io.AccessError!u64 {
ext.SDL_ClearError();
const bytes_read =
ext.SDL_RWread(self.rwOpsCast(), buffer.ptr, @sizeOf(u8), buffer.len);
if ((bytes_read == 0) and (ext.SDL_GetError() != null))
return error.Inaccessible;
return bytes_read;
}
pub fn seek(self: *Self, cursor: u64) core.io.AccessError!void {
ext.SDL_ClearError();
const math = core.math;
const min = math.min;
const maxIntValue = math.maxIntValue;
var sought = min(u64, cursor, maxIntValue(i64));
const ops = self.rwOpsCast();
if (ext.SDL_RWseek(ops, @intCast(i64, sought), ext.RW_SEEK_SET) < 0)
return error.Inaccessible;
var to_seek = cursor - sought;
while (to_seek != 0) {
sought = min(u64, to_seek, maxIntValue(i64));
ext.SDL_ClearError();
if (ext.SDL_RWseek(ops, @intCast(i64, sought), ext.RW_SEEK_CUR) < 0)
return error.Inaccessible;
to_seek -= sought;
}
}
pub fn close(self: *Self) void {
ext.SDL_ClearError();
if (ext.SDL_RWclose(self.rwOpsCast()) != 0)
return ext.SDL_LogWarn(ext.SDL_LOG_CATEGORY_APPLICATION,
"Attempt to close an invalid file reference");
}
}, rw_ops));
},
}
}
///
/// Attempts to query the status of the file identified by `path` on `file_system` for reading,
/// returning a [FileStatus] value containing a the state of the file at the moment or
/// [AccessError] if it failed.
///
pub fn query(file_system: FileSystem, path: Path) AccessError!FileStatus {
switch (file_system) {
.archive => |archive| {
const archive_reader = try archive.file_system.openFileReader(archive.path);
defer archive_reader.close();
const entry = oar.findEntry(archive_reader, path) catch |err| return switch (err) {
error.ArchiveUnsupported, error.Inaccessible => error.FileSystemFailure,
error.EntryNotFound => error.FileNotFound,
};
return FileStatus{
.length = entry.data_length,
};
},
.native => |native| {
if (native.len == 0) return error.FileNotFound;
var path_buffer = [_]u8{0} ** 4096;
const seperator_length = @boolToInt(native[native.len - 1] != Path.seperator);
if ((native.len + seperator_length + path.length) >= path_buffer.len)
return error.FileNotFound;
const io = core.io;
io.copy(u8, &path_buffer, native);
if (seperator_length != 0) path_buffer[native.len] = Path.seperator;
io.copy(u8, path_buffer[native.len .. path_buffer.len],
path.buffer[0 .. path.length]);
ext.SDL_ClearError();
const rw_ops =
ext.SDL_RWFromFile(&path_buffer, "rb") orelse return error.FileSystemFailure;
defer if (ext.SDL_RWclose(rw_ops) != 0) unreachable;
ext.SDL_ClearError();
const size = ext.SDL_RWsize(rw_ops);
if (size < 0) return error.FileSystemFailure;
return FileStatus{
.length = @intCast(u64, size),
};
}, },
} }
} }
@ -346,7 +524,8 @@ pub const Path = extern struct {
}; };
/// ///
/// /// Returns a value above `0` if the path of `this` is greater than `that`, below `0` if it is
/// less, or `0` if they are identical.
/// ///
pub fn compare(this: Path, that: Path) isize { pub fn compare(this: Path, that: Path) isize {
return core.io.compareBytes(this.buffer[0 ..this.length], that.buffer[0 .. that.length]); return core.io.compareBytes(this.buffer[0 ..this.length], that.buffer[0 .. that.length]);
@ -432,16 +611,29 @@ pub const RunError = error {
}; };
/// ///
/// Returns a [core.io.Allocator] bound to the underlying system allocator. /// Returns a thread-safe [core.io.Allocator] value based on the default system allocation strategy.
/// ///
pub fn allocator() core.io.Allocator { pub fn threadSafeAllocator() core.io.Allocator {
// TODO: Add leak detection. const io = core.io;
return .{
.bound = .{ return io.Allocator.wrap(@as(*opaque {
.alloc = ext.SDL_alloc, const Self = @This();
.dealloc = ext.SDL_free,
}, pub fn alloc(_: *Self, layout: io.AllocationLayout) io.AllocationError![*]u8 {
}; return @ptrCast([*]u8, ext.SDL_malloc(layout.length) orelse return error.OutOfMemory);
}
pub fn realloc(_: *Self, allocation: [*]u8,
layout: io.AllocationLayout) io.AllocationError![*]u8 {
return @ptrCast([*]u8, ext.SDL_realloc(allocation, layout.length)
orelse return error.OutOfMemory);
}
pub fn dealloc(_: *Self, allocation: [*]u8) void {
ext.SDL_free(allocation);
}
}, undefined));
} }
/// ///
@ -453,16 +645,11 @@ pub fn allocator() core.io.Allocator {
pub fn display(comptime Error: anytype, pub fn display(comptime Error: anytype,
comptime run: fn (*App, *Graphics) callconv(.Async) Error!void) (RunError || Error)!void { comptime run: fn (*App, *Graphics) callconv(.Async) Error!void) (RunError || Error)!void {
var cwd = FileSystem{.native = "./"}; const cwd = FileSystem{.native = "./"};
const user_prefix = ext.SDL_GetPrefPath("ona", "ona") orelse return error.InitFailure; const user_prefix = ext.SDL_GetPrefPath("ona", "ona") orelse return error.InitFailure;
defer ext.SDL_free(user_prefix); defer ext.SDL_free(user_prefix);
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer if (gpa.deinit())
ext.SDL_LogWarn(ext.SDL_LOG_CATEGORY_APPLICATION, "Runtime allocator leaked memory");
var app = App{ var app = App{
.user = .{.native = std.mem.sliceTo(user_prefix, 0)}, .user = .{.native = std.mem.sliceTo(user_prefix, 0)},
@ -480,17 +667,13 @@ pub fn display(comptime Error: anytype,
return error.InitFailure; return error.InitFailure;
}, },
.data = .{ .data = .{.archive = .{
.archive_file = cwd.openRead(try Path.from(&.{"./data.oar"})) catch { .file_system = &cwd,
ext.SDL_LogCritical(ext.SDL_LOG_CATEGORY_APPLICATION, "Failed to load ./data.oar"); .path = try Path.from(&.{"./data.oar"}),
}},
return error.InitFailure;
},
},
}; };
defer { defer {
app.data.archive_file.close();
ext.SDL_DestroySemaphore(app.message_semaphore); ext.SDL_DestroySemaphore(app.message_semaphore);
ext.SDL_DestroyMutex(app.message_mutex); ext.SDL_DestroyMutex(app.message_mutex);
} }