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");
///
/// 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 {
existing: ?[*]u8,
alignment: u29,
size: usize,
pub const AccessError = error {
Inaccessible,
};
///
/// 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);
///
/// [MakeError.OutOfMemory] if the requested amount of memory could not be allocated.
///
pub const MakeError = error {
pub const AllocationError = error {
OutOfMemory,
};
///
/// Closure that captures a reference to readable resources like block devices, memory buffers,
/// network sockets, and more.
/// Memory layout description for a memory allocation.
///
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
@ -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.
///
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 {
kayomn marked this conversation as resolved Outdated

Worth mentioning that it performs linear time, making it O(n) time complexity?

Worth mentioning that it performs linear time, making it O(n) time complexity?
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"),
kayomn marked this conversation as resolved Outdated

Worth mentioning that this uses linear search?

Worth mentioning that this uses linear search?
},
};
}
///
/// 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`.
@ -320,19 +465,14 @@ test "findFirstOf" {
/// `allocated_memory`. Anything else will result is considered unreachable logic.
///
pub fn free(allocator: Allocator, allocated_memory: anytype) void {
if (allocator.call(.{
.existing = @ptrCast([*]u8, switch (@typeInfo(@TypeOf(allocated_memory))) {
allocator.dealloc(@ptrCast([*]u8, switch (@typeInfo(@TypeOf(allocated_memory))) {
.Pointer => |info| switch (info.size) {
.One, .Many, .C => allocated_memory,
.Slice => allocated_memory.ptr,
},
else => @compileError("`allocated_memory` must be a pointer"),
}),
.size = 0,
.alignment = 0,
}) != null) unreachable;
}));
}
test "free" {
@ -369,14 +509,13 @@ test "hashBytes" {
/// Attempts to allocate a buffer of `size` `Element`s using `allocator`, returning it or a
/// [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);
return @ptrCast([*]Element, @alignCast(alignment, allocator.call(.{
.existing = null,
.size = @sizeOf(Element) * size,
return @ptrCast([*]Element, @alignCast(alignment, try allocator.alloc(.{
.length = @sizeOf(Element) * size,
.alignment = alignment,
}) orelse return error.OutOfMemory));
})));
}
test "makeMany" {
@ -392,14 +531,13 @@ test "makeMany" {
/// Attempts to allocate a buffer of `1` `Element` using `allocator`, returning it or a [MakeError]
/// 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);
return @ptrCast(*Element, @alignCast(alignment, allocator.call(.{
.existing = null,
.size = @sizeOf(Element),
return @ptrCast(*Element, @alignCast(alignment, try allocator.alloc(.{
.length = @sizeOf(Element),
.alignment = alignment,
}) orelse return error.OutOfMemory));
})));
}
test "makeOne" {
@ -429,6 +567,11 @@ test "swap" {
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
/// 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
/// sent somewhere for whatever reason.
///
pub const null_writer = Writer.from(struct {
fn write(buffer: []const u8) usize {
pub const null_writer = Writer.wrap(@ptrCast(*const opaque {
const Self = @This();
fn write(_: Self, buffer: []const u8) usize {
return buffer.len;
}
}.write);
}, &null_context));
test "null_writer" {
const sequence = "foo";

View File

@ -8,77 +8,3 @@ pub fn FnReturn(comptime Fn: type) type {
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.
///
pub fn fixedAllocator(fixed_stack: *Fixed(u8)) io.Allocator {
return io.Allocator.fromClosure(fixed_stack, struct {
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);
const FixedStack = @TypeOf(fixed_stack.*);
// Check the buffer is within the address space of the stack buffer. If not, it
// should just be returned to let the caller know it cannot be freed.
if (buffer_address < stack_address or buffer_address >=
(stack_address + stack.filled)) return buffer;
return io.Allocator.wrap(@ptrCast(*opaque {
const Self = @This();
// TODO: Investigate ways of actually freeing if it is the last allocation.
return null;
} else {
// TODO: Investigate ways of in-place relocating if it is the last allocation.
};
// Reallocate / allocate the memory.
pub fn alloc(self: *Self, layout: io.AllocationLayout) io.AllocationError![*]u8 {
// TODO: Remove stdlib dependency.
const stack = self.stackCast();
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;
kayomn marked this conversation as resolved Outdated

Reallocation could benefit from the same kind of last-alloc check optimization as deallocation.

May be worth clarifying that in the comment and code structure.

Reallocation could benefit from the same kind of last-alloc check optimization as deallocation. May be worth clarifying that in the comment and code structure.
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;
}
}.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" {
@ -185,14 +216,11 @@ test "fixedAllocator" {
const allocator = fixedAllocator(&stack);
// Allocation
var block_memory = allocator.call(.{
.existing = null,
var block_memory = try allocator.alloc(.{
.alignment = @alignOf(u64),
.size = @sizeOf(u64),
.length = @sizeOf(u64),
});
try testing.expect(block_memory != null);
const buffer_address_head = @ptrToInt(&buffer);
const buffer_address_tail = @ptrToInt(&buffer) + buffer.len;
@ -204,14 +232,11 @@ test "fixedAllocator" {
}
// Reallocation.
block_memory = allocator.call(.{
.existing = block_memory,
block_memory = try allocator.realloc(block_memory, .{
.alignment = @alignOf(u64),
.size = @sizeOf(u64),
.length = @sizeOf(u64),
});
try testing.expect(block_memory != null);
{
const block_memory_address = @ptrToInt(block_memory);
@ -220,11 +245,7 @@ test "fixedAllocator" {
}
// Deallocation.
try testing.expect(allocator.call(.{
.existing = block_memory,
.alignment = 0,
.size = 0,
}) == null);
allocator.dealloc(block_memory);
}
///
@ -234,15 +255,23 @@ test "fixedAllocator" {
/// referenced by `fixed_stack` until it is full.
///
pub fn fixedWriter(fixed_stack: *Fixed(u8)) io.Writer {
return io.Writer.fromClosure(fixed_stack, struct {
fn write(stack: *Fixed(u8), buffer: []const u8) usize {
stack.pushAll(buffer) catch |err| switch (err) {
const FixedStack = @TypeOf(fixed_stack.*);
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,
};
return buffer.len;
}
}.write);
}, fixed_stack));
}
test "fixedWriter" {
@ -250,6 +279,8 @@ test "fixedWriter" {
var sequence_stack = Fixed(u8){.buffer = &buffer};
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));
}

View File

@ -29,6 +29,11 @@ pub fn Hashed(comptime Key: type, comptime Value: type,
maybe_next_index: ?usize = null,
};
///
/// Errors that may occur during initialization of a hash table.
///
pub const InitError = io.AllocationError;
///
/// 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.
///
/// 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;
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
/// already present.
///
pub const InsertError = io.MakeError || error {
pub const InsertError = io.AllocationError || error {
KeyExists,
};

View File

@ -7,7 +7,7 @@ const testing = @import("./testing.zig");
/// [PrintError.WriteFailure] occurs when the underlying [io.Writer] implementation failed to write
/// the entirety of a the requested print operation.
///
pub const PrintError = error {
pub const PrintError = io.AccessError || error {
WriteFailure,
};
@ -69,7 +69,7 @@ pub fn printInt(writer: io.Writer, radix: Radix, value: anytype) PrintError!void
if (value == 0) {
const zero = "0";
if (writer.call(zero) != zero.len) return error.WriteFailure;
if ((try writer.write(zero)) != zero.len) return error.WriteFailure;
} else {
// Big enough to hold the hexadecimal representation of the integer type, which is
// 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|
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;
}
},

View File

@ -13,22 +13,19 @@ pub fn main() anyerror!void {
/// Runs the game engine.
///
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();
const file_size = try file_reader.size();
const allocator = gpa.allocator();
const buffer = try allocator.alloc(u8, file_size);
const file_size = (try app.data.query(path)).length;
const allocator = sys.threadSafeAllocator();
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);
}

View File

@ -2,13 +2,13 @@ const core = @import("core");
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,
path: sys.Path = sys.Path.empty,
data_size: u64 = 0,
data_head: u64 = 0,
data_offset: u64 = 0,
data_length: u64 = 0,
padding: [232]u8 = [_]u8{0} ** 232,
comptime {
@ -19,60 +19,68 @@ const Block = extern struct {
};
///
/// Reference to a file entry in an Oar archive, denoting the starting offset from the top of head
/// of the file and its size.
/// [FindError.ArchiveUnsupported] occurs when trying to read a file that does not follow an Oar
/// archive format considered valid by this implemenatation.
///
pub const Entry = struct {
head: u64,
size: u64,
/// [FindError.EntryNotFound] occurs when the queried entry was not found in the archive file.
kayomn marked this conversation as resolved Outdated

Missing documentation comment.

Missing documentation comment.
///
/// [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,
pub const FindError = core.io.AccessError || error {
ArchiveUnsupported,
EntryNotFound,
};
///
kayomn marked this conversation as resolved Outdated

Missing documentation comment.

Missing documentation comment.
/// 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.
/// Header data that every Oar archive file starts with at byte offset `0`.
///
/// **Note** that this operation has `O(log n)` time complexity.
const Header = extern struct {
signature: [signature_magic.len]u8 = signature_magic,
revision: u8 = revision_magic,
entry_count: u32 = 0,
padding: [502]u8 = [_]u8{0} ** 502,
kayomn marked this conversation as resolved Outdated

Missing documentation comment.

Missing documentation comment.
comptime {
const size = @sizeOf(@This());
if (size != 512) @compileError("Header is not 512 bytes");
}
};
///
pub fn find(archive_file: *sys.ReadableFile, entry_path: sys.Path) FindError!Entry {
/// 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 (((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;
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: usize = 0;
var tail: usize = (header.entry_count - 1);
const block_size = @sizeOf(Block);
var head: u64 = 0;
var tail: u64 = (header.entry_count - 1);
const entry_size = @sizeOf(Entry);
while (head <= tail) {
var block = Block{};
const midpoint = (head + (tail - head) / 2);
var entry = Entry{};
const midpoint = head + ((tail - head) / 2);
const offset = header_size + (entry_size * midpoint);
if ((archive_file.read(header.entry_head + (block_size * midpoint), io.bytesOf(&block))
catch return error.ArchiveUnsupported) != block_size) return error.EntryNotFound;
try archive_reader.seek(offset);
const comparison = entry_path.compare(block.path);
if ((try archive_reader.read(io.bytesOf(&entry))) != entry_size)
return error.ArchiveUnsupported;
if (comparison == 0) return Entry{
.head = block.data_head,
.size = block.data_size,
};
const comparison = path.compare(entry.path);
if (comparison == 0) return entry;
kayomn marked this conversation as resolved Outdated

Missing documentation comment.

Missing documentation comment.
if (comparison > 0) {
head = (midpoint + 1);
@ -85,39 +93,7 @@ pub const Entry = struct {
}
kayomn marked this conversation as resolved Outdated

Missing documentation comment.

Missing documentation comment.
///
/// 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)]);
}
};
///
/// Header data that every Oar archive file starts with at byte offset `0`.
///
const Header = extern struct {
signature: [signature_magic.len]u8 = signature_magic,
revision: u8 = revision_magic,
entry_count: u32 = 0,
entry_head: u64 = 0,
padding: [496]u8 = [_]u8{0} ** 496,
comptime {
const size = @sizeOf(@This());
if (size != 512) @compileError("Header is not 512 bytes");
}
};
///
/// The magic revision number that this Oar software implementation understands.
/// Magic revision number that this Oar software implementation understands.
///
const revision_magic = 0;

View File

@ -93,86 +93,106 @@ pub const App = struct {
};
///
/// Snapshotted information about the status of a file.
///
///
pub const ReadableFile = opaque {
///
///
///
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);
}
pub const FileStatus = struct {
kayomn marked this conversation as resolved Outdated

Missing documentation comment.

Missing documentation comment.
length: u64,
};
///
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
/// to a file or the file becomming invalid for whatever reason.
/// Interface for working with bi-directional, streamable resources accessed through a file-system.
///
pub const FileError = error {
FileInaccessible,
pub const FileReader = struct {
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,
kayomn marked this conversation as resolved Outdated

Missing documentation comment.

Missing documentation comment.
},
///
/// 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();
kayomn marked this conversation as resolved Outdated

Missing documentation comment.

Missing documentation comment.

Also, should this be public?

Also, should this be public?
}
}.call,
.read = struct {
fn call(context: *anyopaque, buffer: []u8) core.io.AccessError!u64 {
return @ptrCast(*Implementation, @alignCast(
@alignOf(Implementation), context)).read(buffer);
kayomn marked this conversation as resolved Outdated

Missing documentation comment.

Missing documentation comment.
}
}.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) {
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 -
/// from physical to virtual errors - these are all encapsulated by the API as general
/// [OpenError.FileNotFound] errors.
/// [AccessError.FileNotFound] occurs when a queried file could not be found on the file-system
kayomn marked this conversation as resolved Outdated

Out of date documentation comment.

Out of date documentation comment.
/// by the process. This may mean the file does not exist, however it may also mean that the
/// process does not have sufficient rights to read it.
///
/// When a given [FileSystem] does not support a specified [OpenMode],
/// [OpenError.ModeUnsupported] is used to inform the consuming code that another [OpenMode]
/// should be tried or, if no mode other is suitable, that the resource is effectively
/// unavailable.
/// [AccessError.FileSystemfailure] denotes a file-system implementation-specific failure to
/// access resources has occured and therefore cannot proceed to access the file.
///
kayomn marked this conversation as resolved Outdated

This should be resolved as part of the PR.

This should be resolved as part of the PR.
/// If the number of known [FileAccess] handles has been exhausted, [OpenError.OutOfFiles] is
/// used to communicate this.
///
pub const OpenError = error {
pub const AccessError = error {
FileNotFound,
ModeUnsupported,
OutOfFiles,
FileSystemFailure,
};
///
/// 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
/// [OpenError] if it failed.
///
pub fn openRead(file_system: *const FileSystem, path: Path) OpenError!*ReadableFile {
switch (file_system.*) {
.archive_file => |archive_file| {
const entry = oar.Entry.find(archive_file, path) catch return error.FileNotFound;
pub fn openFileReader(file_system: FileSystem, path: Path) AccessError!FileReader {
switch (file_system) {
.archive => |archive| {
const archive_reader = try archive.file_system.openFileReader(archive.path);
_ = entry;
// TODO: Alloc file context.
errdefer archive_reader.close();
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| {
@ -239,8 +301,124 @@ pub const FileSystem = union(enum) {
ext.SDL_ClearError();
return @ptrCast(*ReadableFile, ext.SDL_RWFromFile(&path_buffer, "rb")
orelse return error.FileNotFound);
const rw_ops =
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;
}
kayomn marked this conversation as resolved Outdated

Missing documentation comment.

Missing documentation comment.
}
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 {
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 {
// TODO: Add leak detection.
return .{
.bound = .{
.alloc = ext.SDL_alloc,
.dealloc = ext.SDL_free,
},
};
pub fn threadSafeAllocator() core.io.Allocator {
const io = core.io;
return io.Allocator.wrap(@as(*opaque {
const Self = @This();
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,
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;
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{
.user = .{.native = std.mem.sliceTo(user_prefix, 0)},
@ -480,17 +667,13 @@ pub fn display(comptime Error: anytype,
return error.InitFailure;
},
.data = .{
.archive_file = cwd.openRead(try Path.from(&.{"./data.oar"})) catch {
ext.SDL_LogCritical(ext.SDL_LOG_CATEGORY_APPLICATION, "Failed to load ./data.oar");
return error.InitFailure;
},
},
.data = .{.archive = .{
.file_system = &cwd,
.path = try Path.from(&.{"./data.oar"}),
}},
};
defer {
app.data.archive_file.close();
ext.SDL_DestroySemaphore(app.message_semaphore);
ext.SDL_DestroyMutex(app.message_mutex);
}