Application Context Implementation #4
|
@ -5,5 +5,6 @@ steps:
|
|||
- name: build & test
|
||||
image: euantorano/zig:0.9.1
|
||||
commands:
|
||||
- zig build test
|
||||
- $(find zig-cache -name test) main.zig
|
||||
- apk --no-cache add build-base sdl2-dev
|
||||
- zig build
|
||||
- ./zig-out/bin/test main.zig
|
||||
|
|
|
@ -2,23 +2,26 @@
|
|||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Build",
|
||||
"name": "Ona",
|
||||
"type": "gdb",
|
||||
"request": "launch",
|
||||
"target": "${workspaceFolder}/zig-out/bin/ona",
|
||||
"cwd": "${workspaceRoot}",
|
||||
"valuesFormatting": "parseText",
|
||||
"preLaunchTask": "Build",
|
||||
"preLaunchTask": "Build Debug",
|
||||
"internalConsoleOptions": "openOnSessionStart",
|
||||
},
|
||||
|
||||
{
|
||||
"name": "Test",
|
||||
"type": "gdb",
|
||||
"request": "launch",
|
||||
"target": "${workspaceFolder}/zig-cache/o/b57ef32c79a05339fbe4a8eb648ff6df/test",
|
||||
"target": "${workspaceFolder}/zig-out/bin/test",
|
||||
"arguments": "main.zig",
|
||||
"cwd": "${workspaceRoot}",
|
||||
"valuesFormatting": "parseText",
|
||||
"preLaunchTask": "Build Test",
|
||||
"preLaunchTask": "Build Debug",
|
||||
"internalConsoleOptions": "openOnSessionStart",
|
||||
},
|
||||
]
|
||||
}
|
||||
|
|
|
@ -14,4 +14,5 @@
|
|||
|
||||
"git.detectSubmodulesLimit": 0,
|
||||
"git.ignoreSubmodules": true,
|
||||
"debug.onTaskErrors": "showErrors",
|
||||
}
|
||||
|
|
|
@ -1,29 +1,8 @@
|
|||
{
|
||||
"version": "2.0.0",
|
||||
|
||||
"tasks": [
|
||||
{
|
||||
"label": "Build",
|
||||
"type": "shell",
|
||||
"command": "zig build",
|
||||
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
},
|
||||
|
||||
"presentation": {
|
||||
"echo": true,
|
||||
"reveal": "always",
|
||||
"focus": true,
|
||||
"panel": "shared",
|
||||
"showReuseMessage": true,
|
||||
"clear": true,
|
||||
"revealProblems": "onProblem",
|
||||
},
|
||||
|
||||
"problemMatcher": {
|
||||
"source": "gcc",
|
||||
"source": "zig",
|
||||
"owner": "cpptools",
|
||||
|
||||
"fileLocation": [
|
||||
|
@ -39,22 +18,23 @@
|
|||
"severity": 4,
|
||||
"message": 5,
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"tasks": [
|
||||
{
|
||||
"label": "Test",
|
||||
"label": "Build Debug",
|
||||
"type": "shell",
|
||||
"command": "$(find zig-cache -name test) src/main.zig",
|
||||
"group": {
|
||||
"kind": "test",
|
||||
"isDefault": true
|
||||
"command": "zig build",
|
||||
"group": "build",
|
||||
"presentation": {
|
||||
"echo": true,
|
||||
"reveal": "silent",
|
||||
"focus": true,
|
||||
"panel": "shared",
|
||||
"showReuseMessage": true,
|
||||
"clear": true,
|
||||
"revealProblems": "onProblem",
|
||||
},
|
||||
},
|
||||
{
|
||||
"label": "Build Test",
|
||||
"type": "shell",
|
||||
"command": "zig build test",
|
||||
"group": "test"
|
||||
},
|
||||
],
|
||||
}
|
||||
|
|
49
build.zig
49
build.zig
|
@ -1,34 +1,45 @@
|
|||
const std = @import("std");
|
||||
|
||||
///
|
||||
/// Builds the engine, tools, and dependencies of all.
|
||||
///
|
||||
pub fn build(builder: *std.build.Builder) void {
|
||||
const target = builder.standardTargetOptions(.{});
|
||||
const mode = builder.standardReleaseOptions();
|
||||
const core_pkg = projectPkg("core", &.{});
|
||||
|
||||
// Ona executable.
|
||||
{
|
||||
const ona_exe = builder.addExecutable("ona", "./src/main.zig");
|
||||
const ona = builder.addExecutable("ona", "./src/ona/main.zig");
|
||||
|
||||
ona_exe.setTarget(target);
|
||||
ona_exe.setBuildMode(mode);
|
||||
ona_exe.install();
|
||||
ona_exe.addIncludeDir("./ext");
|
||||
ona_exe.linkSystemLibrary("SDL2");
|
||||
|
||||
const run_cmd = ona_exe.run();
|
||||
|
||||
run_cmd.step.dependOn(builder.getInstallStep());
|
||||
|
||||
if (builder.args) |args| run_cmd.addArgs(args);
|
||||
|
||||
builder.step("run", "Run Ona application").dependOn(&run_cmd.step);
|
||||
ona.addPackage(core_pkg);
|
||||
ona.setTarget(target);
|
||||
ona.setBuildMode(mode);
|
||||
ona.install();
|
||||
ona.addIncludeDir("./ext");
|
||||
ona.linkSystemLibrary("SDL2");
|
||||
ona.linkLibC();
|
||||
}
|
||||
|
||||
// Ona tests.
|
||||
// Tests executable.
|
||||
{
|
||||
const ona_tests = builder.addTestExe("test", "./src/main.zig");
|
||||
const tests = builder.addTestExe("test", "./src/tests.zig");
|
||||
|
||||
ona_tests.setTarget(target);
|
||||
ona_tests.setBuildMode(mode);
|
||||
builder.step("test", "Run Ona unit tests").dependOn(&ona_tests.step);
|
||||
tests.addPackage(core_pkg);
|
||||
tests.setTarget(target);
|
||||
tests.setBuildMode(mode);
|
||||
tests.install();
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
/// Returns a [std.build.Pkg] within the project codebase path at `name` with `dependencies` as its
|
||||
/// dependencies.
|
||||
///
|
||||
fn projectPkg(comptime name: []const u8, dependencies: []const std.build.Pkg) std.build.Pkg {
|
||||
return std.build.Pkg{
|
||||
.name = name,
|
||||
.path = .{.path = "./src/" ++ name ++ "/main.zig"},
|
||||
.dependencies = dependencies,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -0,0 +1,594 @@
|
|||
const math = @import("./math.zig");
|
||||
const meta = @import("./meta.zig");
|
||||
const stack = @import("./stack.zig");
|
||||
const testing = @import("./testing.zig");
|
||||
|
||||
///
|
||||
/// [AccessError.Inacessible] is a generic catch-all for IO resources that are inaccessible for
|
||||
/// implementation-specific reasons.
|
||||
///
|
||||
pub const AccessError = error {
|
||||
Inaccessible,
|
||||
};
|
||||
|
||||
///
|
||||
/// [AllocationError.OutOfMemory] if the requested amount of memory could not be allocated.
|
||||
///
|
||||
pub const AllocationError = error {
|
||||
OutOfMemory,
|
||||
};
|
||||
|
||||
///
|
||||
/// Memory layout description for a memory allocation.
|
||||
///
|
||||
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
|
||||
/// that match a delimiting pattern.
|
||||
///
|
||||
pub fn Spliterator(comptime Element: type) type {
|
||||
return struct {
|
||||
source: []const Element,
|
||||
delimiter: []const Element,
|
||||
|
||||
const Self = @This();
|
||||
|
||||
///
|
||||
/// Returns `true` if there is more data to be processed, otherwise `false`.
|
||||
///
|
||||
pub fn hasNext(self: Self) bool {
|
||||
return (self.source.len != 0);
|
||||
}
|
||||
|
||||
///
|
||||
/// Iterates on `self` and returns the next view of [Spliterator.source] that matches
|
||||
/// [Spliterator.delimiter], or `null` if there is no more data to be processed.
|
||||
///
|
||||
pub fn next(self: *Self) ?[]const Element {
|
||||
if (!self.hasNext()) return null;
|
||||
|
||||
if (self.delimiter.len == 0) {
|
||||
defer self.source = self.source[self.source.len .. self.source.len];
|
||||
|
||||
return self.source[0 .. self.source.len];
|
||||
}
|
||||
|
||||
if (findFirstOf(Element, self.source, self.delimiter, struct {
|
||||
fn testEquality(this: Element, that: Element) bool {
|
||||
return this == that;
|
||||
}
|
||||
}.testEquality)) |head| {
|
||||
defer self.source = self.source[(head + self.delimiter.len) .. self.source.len];
|
||||
|
||||
return self.source[0 .. head];
|
||||
}
|
||||
|
||||
defer self.source = self.source[self.source.len .. self.source.len];
|
||||
|
||||
return self.source;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
test "Spliterator(u8)" {
|
||||
// Empty source.
|
||||
{
|
||||
var spliterator = Spliterator(u8){
|
||||
.source = "",
|
||||
.delimiter = " ",
|
||||
};
|
||||
|
||||
try testing.expect(!spliterator.hasNext());
|
||||
}
|
||||
|
||||
// Empty delimiter.
|
||||
{
|
||||
var spliterator = Spliterator(u8){
|
||||
.source = "aaa",
|
||||
.delimiter = "",
|
||||
};
|
||||
|
||||
try testing.expect(spliterator.hasNext());
|
||||
try testing.expect(equals(u8, spliterator.next().?, "aaa"));
|
||||
try testing.expect(!spliterator.hasNext());
|
||||
}
|
||||
|
||||
// Single-character delimiter.
|
||||
{
|
||||
var spliterator = Spliterator(u8){
|
||||
.source = "single.character.separated.hello.world",
|
||||
.delimiter = ".",
|
||||
};
|
||||
|
||||
const components = [_][]const u8{"single",
|
||||
"character", "separated", "hello", "world"};
|
||||
|
||||
var index = @as(usize, 0);
|
||||
const components_tail = components.len - 1;
|
||||
|
||||
while (spliterator.next()) |split| : (index += 1) {
|
||||
try testing.expect(spliterator.hasNext() == (index < components_tail));
|
||||
try testing.expect(equals(u8, split, components[index]));
|
||||
}
|
||||
|
||||
try testing.expect(!spliterator.hasNext());
|
||||
}
|
||||
|
||||
// Multi-character delimiter.
|
||||
{
|
||||
var spliterator = Spliterator(u8){
|
||||
.source = "finding a needle in a needle stack",
|
||||
.delimiter = "needle",
|
||||
};
|
||||
|
||||
const components = [_][]const u8{"finding a ", " in a ", " stack"};
|
||||
var index = @as(usize, 0);
|
||||
const components_tail = components.len - 1;
|
||||
|
||||
while (spliterator.next()) |split| : (index += 1) {
|
||||
try testing.expect(spliterator.hasNext() == (index < components_tail));
|
||||
try testing.expect(equals(u8, split, components[index]));
|
||||
}
|
||||
|
||||
try testing.expect(!spliterator.hasNext());
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
/// Interface for capturing a reference to a writable resource like block devices, memory buffers,
|
||||
/// network sockets, and more.
|
||||
///
|
||||
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`.
|
||||
///
|
||||
pub fn bytesOf(pointer: anytype) switch (@typeInfo(@TypeOf(pointer))) {
|
||||
.Pointer => |info| if (info.is_const) []const u8 else []u8,
|
||||
else => @compileError("`pointer` must be a pointer type"),
|
||||
} {
|
||||
const Pointer = @TypeOf(pointer);
|
||||
const pointer_info = @typeInfo(Pointer).Pointer;
|
||||
|
||||
switch (pointer_info.size) {
|
||||
.Many => @compileError("`pointer` cannot be an unbound pointer type"),
|
||||
.C => @compileError("`pointer` cannot be a C-style pointer"),
|
||||
|
||||
.One => return @ptrCast(if (pointer_info.is_const) [*]const u8
|
||||
else [*]u8, pointer)[0 .. @sizeOf(Pointer)],
|
||||
|
||||
.Slice => return @ptrCast(if (pointer_info.is_const) [*]const u8 else
|
||||
[*]u8, pointer.ptr)[0 .. (@sizeOf(Pointer) * pointer.len)],
|
||||
}
|
||||
}
|
||||
|
||||
test "bytesOf" {
|
||||
var foo: u32 = 10;
|
||||
|
||||
try testing.expect(bytesOf(&foo)[0] == 0x0a);
|
||||
}
|
||||
|
||||
///
|
||||
/// Compares `this` to `that`, returning the difference between the first byte deviation in the two
|
||||
/// sequences, otherwise `0` if they are identical.
|
||||
///
|
||||
pub fn compareBytes(this: []const u8, that: []const u8) isize {
|
||||
const range = math.min(usize, this.len, that.len);
|
||||
var index: usize = 0;
|
||||
|
||||
while (index < range) : (index += 1) {
|
||||
const difference = (this[index] - that[index]);
|
||||
|
||||
if (difference != 0) return difference;
|
||||
}
|
||||
|
||||
return (@intCast(isize, this.len) - @intCast(isize, that.len));
|
||||
}
|
||||
|
||||
test "compareBytes" {
|
||||
try testing.expect(compareBytes(&.{69, 42, 0}, &.{69, 42, 0}) == 0);
|
||||
try testing.expect(compareBytes(&.{69, 42, 11}, &.{69, 42}) == 1);
|
||||
try testing.expect(compareBytes(&.{69, 42}, &.{69, 42, 11}) == -1);
|
||||
}
|
||||
|
||||
///
|
||||
/// Copies the contents of `source` into `target`
|
||||
///
|
||||
pub fn copy(comptime Element: type, target: []Element, source: []const Element) void {
|
||||
for (source) |element, index| target[index] = element;
|
||||
}
|
||||
|
||||
test "copy" {
|
||||
var buffer = [_]u32{0} ** 20;
|
||||
const data = [_]u32{3, 20, 8000};
|
||||
|
||||
copy(u32, &buffer, &data);
|
||||
|
||||
for (data) |datum, index| try testing.expect(buffer[index] == datum);
|
||||
}
|
||||
|
||||
///
|
||||
/// Returns `true` if `this` is the same length and contains the same data as `that`, otherwise
|
||||
/// `false`.
|
||||
///
|
||||
pub fn equals(comptime Element: type, this: []const Element, that: []const Element) bool {
|
||||
if (this.len != that.len) return false;
|
||||
|
||||
var index: usize = 0;
|
||||
|
||||
while (index < this.len) : (index += 1) if (this[index] != that[index]) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
test "equals" {
|
||||
const bytes_sequence = &.{69, 42, 0};
|
||||
|
||||
try testing.expect(equals(u8, bytes_sequence, bytes_sequence));
|
||||
try testing.expect(!equals(u8, bytes_sequence, &.{69, 42}));
|
||||
}
|
||||
|
||||
///
|
||||
/// Fills the contents of `target` with `source`.
|
||||
///
|
||||
pub fn fill(comptime Element: type, target: []Element, source: Element) void {
|
||||
for (target) |_, index| target[index] = source;
|
||||
}
|
||||
|
||||
test "fill" {
|
||||
var buffer = [_]u32{0} ** 8;
|
||||
|
||||
fill(u32, &buffer, 1);
|
||||
|
||||
for (buffer) |element| try testing.expect(element == 1);
|
||||
}
|
||||
|
||||
///
|
||||
/// Linearly searches for the first instance of an `Element` equal to `needle` in `haystack`,
|
||||
/// returning its index or `null` if nothing was found.
|
||||
///
|
||||
/// **Note** that this operation has `O(n)` time complexity.
|
||||
///
|
||||
pub fn findFirst(comptime Element: type, haystack: []const Element,
|
||||
needle: Element, comptime testEquality: fn (Element, Element) bool) ?usize {
|
||||
|
||||
for (haystack) |element, index| if (testEquality(element, needle)) return index;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
test "findFirst" {
|
||||
const haystack = &.{"", "", "foo"};
|
||||
|
||||
const testEquality = struct {
|
||||
fn testEquality(this: []const u8, that: []const u8) bool {
|
||||
return equals(u8, this, that);
|
||||
}
|
||||
}.testEquality;
|
||||
|
||||
try testing.expect(findFirst([]const u8, haystack, "foo", testEquality).? == 2);
|
||||
try testing.expect(findFirst([]const u8, haystack, "bar", testEquality) == null);
|
||||
}
|
||||
|
||||
///
|
||||
/// Searches for the first instance of an `Element` sequence equal to the contents of `needle` in
|
||||
/// `haystack`, returning the starting index or `null` if nothing was found.
|
||||
///
|
||||
/// **Note** that this operation has `O(nm)` time complexity.
|
||||
///
|
||||
pub fn findFirstOf(comptime Element: type, haystack: []const Element,
|
||||
needle: []const Element, comptime testEquality: fn (Element, Element) bool) ?usize {
|
||||
|
||||
var head: usize = 0;
|
||||
const tail = (haystack.len - needle.len);
|
||||
|
||||
walk_haystack: while (head <= tail) : (head += 1) {
|
||||
for (needle) |element, index|
|
||||
if (!testEquality(haystack[head + index], element)) continue: walk_haystack;
|
||||
|
||||
return head;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
test "findFirstOf" {
|
||||
const haystack = &.{"foo", "bar", "baz"};
|
||||
|
||||
const testEquality = struct {
|
||||
fn testEquality(this: []const u8, that: []const u8) bool {
|
||||
return equals(u8, this, that);
|
||||
}
|
||||
}.testEquality;
|
||||
|
||||
try testing.expect(findFirstOf([]const u8, haystack, &.{"bar", "baz"}, testEquality).? == 1);
|
||||
try testing.expect(findFirstOf([]const u8, haystack, &.{"baz", "bar"}, testEquality) == null);
|
||||
}
|
||||
|
||||
///
|
||||
/// Frees `allocated_memory` using `allocator`.
|
||||
///
|
||||
/// *Note* that only memory known to be freeable by `allocator` should be passed via
|
||||
/// `allocated_memory`. Anything else will result is considered unreachable logic.
|
||||
///
|
||||
pub fn free(allocator: Allocator, allocated_memory: anytype) void {
|
||||
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"),
|
||||
}));
|
||||
}
|
||||
|
||||
test "free" {
|
||||
var buffer = [_]u8{0} ** 4096;
|
||||
var memory = stack.Fixed(u8){.buffer = &buffer};
|
||||
const fixed_allocator = stack.fixedAllocator(&memory);
|
||||
const block_size = 8;
|
||||
const allocated_block = (try makeMany(u8, fixed_allocator, block_size))[0 .. block_size];
|
||||
|
||||
defer free(fixed_allocator, allocated_block);
|
||||
}
|
||||
|
||||
///
|
||||
/// Returns a deterministic hash code compiled from each byte in `bytes`.
|
||||
///
|
||||
/// **Note** that this operation has `O(n)` time complexity.
|
||||
///
|
||||
pub fn hashBytes(bytes: []const u8) usize {
|
||||
var hash = @as(usize, 5381);
|
||||
|
||||
for (bytes) |byte| hash = ((hash << 5) + hash) + byte;
|
||||
|
||||
return hash;
|
||||
}
|
||||
|
||||
test "hashBytes" {
|
||||
const bytes_sequence = &.{69, 42, 0};
|
||||
|
||||
try testing.expect(hashBytes(bytes_sequence) == hashBytes(bytes_sequence));
|
||||
try testing.expect(hashBytes(bytes_sequence) != hashBytes(&.{69, 42}));
|
||||
}
|
||||
|
||||
///
|
||||
/// 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) AllocationError![*]Element {
|
||||
const alignment = @alignOf(Element);
|
||||
|
||||
return @ptrCast([*]Element, @alignCast(alignment, try allocator.alloc(.{
|
||||
.length = @sizeOf(Element) * size,
|
||||
.alignment = alignment,
|
||||
})));
|
||||
}
|
||||
|
||||
test "makeMany" {
|
||||
var buffer = [_]u8{0} ** 4096;
|
||||
var memory = stack.Fixed(u8){.buffer = &buffer};
|
||||
const block_size = 8;
|
||||
|
||||
// Don't care about the actual allocation - just assertions about it.
|
||||
_ = (try makeMany(u8, stack.fixedAllocator(&memory), block_size))[0 .. block_size];
|
||||
}
|
||||
|
||||
///
|
||||
/// 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) AllocationError!*Element {
|
||||
const alignment = @alignOf(Element);
|
||||
|
||||
return @ptrCast(*Element, @alignCast(alignment, try allocator.alloc(.{
|
||||
.length = @sizeOf(Element),
|
||||
.alignment = alignment,
|
||||
})));
|
||||
}
|
||||
|
||||
test "makeOne" {
|
||||
var buffer = [_]u8{0} ** 4096;
|
||||
var memory = stack.Fixed(u8){.buffer = &buffer};
|
||||
|
||||
// Don't care about the actual allocation - just assertions about it.
|
||||
_ = try makeOne(u8, stack.fixedAllocator(&memory));
|
||||
}
|
||||
|
||||
///
|
||||
/// Swaps the `Data` in `this` with `that`.
|
||||
///
|
||||
pub fn swap(comptime Data: type, this: *Data, that: *Data) void {
|
||||
const temp = this.*;
|
||||
this.* = that.*;
|
||||
that.* = temp;
|
||||
}
|
||||
|
||||
test "swap" {
|
||||
var a: u64 = 0;
|
||||
var b: u64 = 1;
|
||||
|
||||
swap(u64, &a, &b);
|
||||
|
||||
try testing.expect(a == 1);
|
||||
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.
|
||||
///
|
||||
/// 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.wrap(@ptrCast(*const opaque {
|
||||
const Self = @This();
|
||||
|
||||
fn write(_: Self, buffer: []const u8) usize {
|
||||
return buffer.len;
|
||||
}
|
||||
}, &null_context));
|
||||
|
||||
test "null_writer" {
|
||||
const sequence = "foo";
|
||||
|
||||
try testing.expect(null_writer.call(sequence) == sequence.len);
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
|
||||
///
|
||||
/// Platform-agnostic input and output interfaces for working with memory, files, and networks.
|
||||
///
|
||||
pub const io = @import("./io.zig");
|
||||
|
||||
///
|
||||
/// Math types and functions with a focus on graphics-specific linear algebra.
|
||||
///
|
||||
pub const math = @import("./math.zig");
|
||||
|
||||
///
|
||||
/// Metaprogramming introspection and generation.
|
||||
///
|
||||
pub const meta = @import("./meta.zig");
|
||||
|
||||
///
|
||||
/// Sequential, last-in first-out data structures.
|
||||
///
|
||||
pub const stack = @import("./stack.zig");
|
||||
|
||||
///
|
||||
/// Unordered key-value association data structures.
|
||||
///
|
||||
pub const table = @import("./table.zig");
|
||||
|
||||
///
|
||||
/// Unit testing suite utilities.
|
||||
///
|
||||
pub const testing = @import("./testing.zig");
|
||||
|
||||
///
|
||||
/// Unicode-encoded string analysis and processing with a focus on UTF-8 encoded text.
|
||||
///
|
||||
pub const unicode = @import("./unicode.zig");
|
||||
|
||||
test {
|
||||
_ = io;
|
||||
_ = math;
|
||||
_ = meta;
|
||||
_ = stack;
|
||||
_ = table;
|
||||
_ = testing;
|
||||
_ = unicode;
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
const std = @import("std");
|
||||
const testing = @import("./testing.zig");
|
||||
|
||||
// TODO: Remove stdlib dependency.
|
||||
pub const IntFittingRange = std.math.IntFittingRange;
|
||||
|
||||
///
|
||||
/// Returns the highest integer value representable by `Integer`.
|
||||
///
|
||||
pub fn maxIntValue(comptime Integer: type) comptime_int {
|
||||
return switch (@typeInfo(Integer)) {
|
||||
.Int => |info| if (info.bits == 0) 0 else
|
||||
((1 << (info.bits - @boolToInt(info.signedness == .signed))) - 1),
|
||||
|
||||
else => @compileError("`" ++ @typeName(Integer) ++ "` must be an int"),
|
||||
};
|
||||
}
|
||||
|
||||
test "maxIntValue" {
|
||||
try testing.expect(maxIntValue(u8) == 255);
|
||||
try testing.expect(maxIntValue(i8) == 127);
|
||||
|
||||
try testing.expect(maxIntValue(u16) == 65535);
|
||||
try testing.expect(maxIntValue(i16) == 32767);
|
||||
}
|
||||
|
||||
///
|
||||
/// Returns the highest `Number` value between `this` and `that`.
|
||||
///
|
||||
pub fn max(comptime Number: type, this: Number, that: Number) Number {
|
||||
return switch (@typeInfo(Number)) {
|
||||
.Int, .Float, .ComptimeInt, .ComptimeFloat => if (this > that) this else that,
|
||||
|
||||
else => @compileError("`" ++ @typeName(Number) ++
|
||||
"` must be an int, float, comptime_int, or comptime_float"),
|
||||
};
|
||||
}
|
||||
|
||||
test "max" {
|
||||
try testing.expect(max(f32, 0.1, 1.0) == 1.0);
|
||||
try testing.expect(max(f64, 1.0, 1.01) == 1.01);
|
||||
try testing.expect(max(u32, 35615, 2873) == 35615);
|
||||
}
|
||||
|
||||
///
|
||||
/// Returns the lowest `Number` value between `this` and `that`.
|
||||
///
|
||||
pub fn min(comptime Number: type, this: Number, that: Number) Number {
|
||||
return switch (@typeInfo(Number)) {
|
||||
.Int, .Float, .ComptimeInt, .ComptimeFloat => if (this < that) this else that,
|
||||
|
||||
else => @compileError("`" ++ @typeName(Number) ++
|
||||
"` must be an int, float, comptime_int, or comptime_float"),
|
||||
};
|
||||
}
|
||||
|
||||
test "min" {
|
||||
try testing.expect(min(f32, 0.1, 1.0) == 0.1);
|
||||
try testing.expect(min(f64, 1.0, 1.01) == 1.0);
|
||||
try testing.expect(min(u32, 35615, 2873) == 2873);
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
///
|
||||
/// Returns the return type of the function type `Fn`.
|
||||
///
|
||||
pub fn FnReturn(comptime Fn: type) type {
|
||||
const type_info = @typeInfo(Fn);
|
||||
|
||||
if (type_info != .Fn) @compileError("`Fn` must be a function type");
|
||||
|
||||
return type_info.Fn.return_type orelse void;
|
||||
}
|
|
@ -0,0 +1,286 @@
|
|||
const io = @import("./io.zig");
|
||||
const testing = @import("./testing.zig");
|
||||
|
||||
///
|
||||
/// Returns a fixed-size stack type of `Element`s.
|
||||
///
|
||||
pub fn Fixed(comptime Element: type) type {
|
||||
return struct {
|
||||
filled: usize = 0,
|
||||
buffer: []Element,
|
||||
|
||||
///
|
||||
/// Stack type.
|
||||
///
|
||||
const Self = @This();
|
||||
|
||||
///
|
||||
/// Resets the number of filled items to `0`, otherwise leaving the actual memory contents
|
||||
/// of the buffer untouched until it is later overwritten by following operations on it.
|
||||
///
|
||||
pub fn clear(self: *Self) void {
|
||||
self.filled = 0;
|
||||
}
|
||||
|
||||
///
|
||||
/// Returns `true` if `self` has filled its buffer to maximum capacity, otherwise `false`.
|
||||
///
|
||||
pub fn isFull(self: Self) bool {
|
||||
return (self.filled == self.buffer.len);
|
||||
}
|
||||
|
||||
///
|
||||
/// If `self` is filled with at least `1` value, it is decremented by `1`, otherwise leaving
|
||||
/// the actual memory contents of the buffer untouched until it is later overwritten by
|
||||
/// following operations on it.
|
||||
///
|
||||
/// The value of the element removed from the list is returned if something existed to be
|
||||
/// popped, otherwise `null` if it contained no elements.
|
||||
///
|
||||
pub fn pop(self: *Self) ?Element {
|
||||
if (self.filled == 0) return null;
|
||||
|
||||
self.filled -= 1;
|
||||
|
||||
return self.buffer[self.filled];
|
||||
}
|
||||
|
||||
///
|
||||
/// Attempts to push `element` into `self`, returning a [FixedPushError] if it failed.
|
||||
///
|
||||
pub fn push(self: *Self, element: Element) FixedPushError!void {
|
||||
if (self.isFull()) return error.BufferOverflow;
|
||||
|
||||
self.buffer[self.filled] = element;
|
||||
self.filled += 1;
|
||||
}
|
||||
|
||||
///
|
||||
/// Attempts to push all of `elements` into `self`, returning a [FixedPushError] if it
|
||||
/// failed.
|
||||
///
|
||||
pub fn pushAll(self: *Self, elements: []const Element) FixedPushError!void {
|
||||
const filled = (self.filled + elements.len);
|
||||
|
||||
if (filled > self.buffer.len) return error.BufferOverflow;
|
||||
|
||||
io.copy(Element, self.buffer[self.filled ..], elements);
|
||||
|
||||
self.filled = filled;
|
||||
}
|
||||
|
||||
///
|
||||
/// Attempts to push `count` instances of `element` into `self`, returning a
|
||||
/// [FixedPushError] if it failed.
|
||||
///
|
||||
pub fn pushMany(self: *Self, element: Element, count: usize) FixedPushError!void {
|
||||
const filled = (self.filled + count);
|
||||
|
||||
if (filled > self.buffer.len) return error.BufferOverflow;
|
||||
|
||||
io.fill(Element, self.buffer[self.filled ..], element);
|
||||
|
||||
self.filled = filled;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
test "Fixed([]const u8)" {
|
||||
const default_value = "";
|
||||
var buffer = [_][]const u8{default_value} ** 4;
|
||||
var shopping_list = Fixed([]const u8){.buffer = &buffer};
|
||||
|
||||
// Pop empty stack.
|
||||
{
|
||||
try testing.expect(shopping_list.pop() == null);
|
||||
try testing.expect(shopping_list.filled == 0);
|
||||
try testing.expect(shopping_list.buffer.ptr == &buffer);
|
||||
try testing.expect(shopping_list.buffer.len == buffer.len);
|
||||
|
||||
for (shopping_list.buffer) |item|
|
||||
try testing.expect(io.equals(u8, item, default_value));
|
||||
}
|
||||
|
||||
// Push single element.
|
||||
{
|
||||
try shopping_list.push("milk");
|
||||
try testing.expect(shopping_list.filled == 1);
|
||||
try testing.expect(shopping_list.buffer.ptr == &buffer);
|
||||
try testing.expect(shopping_list.buffer.len == buffer.len);
|
||||
try testing.expect(io.equals(u8, shopping_list.buffer[0], "milk"));
|
||||
|
||||
for (shopping_list.buffer[1 ..]) |item|
|
||||
try testing.expect(io.equals(u8, item, default_value));
|
||||
|
||||
// TODO: Test stack overflow.
|
||||
}
|
||||
|
||||
// Pop single element.
|
||||
{
|
||||
try testing.expect(io.equals(u8, shopping_list.pop().?, "milk"));
|
||||
try testing.expect(shopping_list.filled == 0);
|
||||
try testing.expect(shopping_list.buffer.ptr == &buffer);
|
||||
try testing.expect(shopping_list.buffer.len == buffer.len);
|
||||
try testing.expect(io.equals(u8, shopping_list.buffer[0], "milk"));
|
||||
|
||||
for (shopping_list.buffer[1 ..]) |item|
|
||||
try testing.expect(io.equals(u8, item, default_value));
|
||||
}
|
||||
|
||||
// TODO: Multiple elements.
|
||||
// TODO: Clear elements.
|
||||
}
|
||||
|
||||
///
|
||||
/// Potential errors that may occur while trying to push one or more elements into a [Fixed] stack.
|
||||
///
|
||||
pub const FixedPushError = error {
|
||||
BufferOverflow,
|
||||
};
|
||||
|
||||
///
|
||||
/// Creates and returns a [io.Allocator] value wrapping `fixed_stack`.
|
||||
///
|
||||
/// The returned [io.Allocator] uses `fixed_stack` and its backing memory buffer as a fixed-length
|
||||
/// memory pool to linearly allocate memory from.
|
||||
///
|
||||
pub fn fixedAllocator(fixed_stack: *Fixed(u8)) io.Allocator {
|
||||
const FixedStack = @TypeOf(fixed_stack.*);
|
||||
|
||||
return io.Allocator.wrap(@ptrCast(*opaque {
|
||||
const Self = @This();
|
||||
|
||||
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, 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;
|
||||
}
|
||||
|
||||
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" {
|
||||
var buffer = [_]u8{0} ** 32;
|
||||
var stack = Fixed(u8){.buffer = &buffer};
|
||||
const allocator = fixedAllocator(&stack);
|
||||
|
||||
// Allocation
|
||||
var block_memory = try allocator.alloc(.{
|
||||
.alignment = @alignOf(u64),
|
||||
.length = @sizeOf(u64),
|
||||
});
|
||||
|
||||
const buffer_address_head = @ptrToInt(&buffer);
|
||||
const buffer_address_tail = @ptrToInt(&buffer) + buffer.len;
|
||||
|
||||
{
|
||||
const block_memory_address = @ptrToInt(block_memory);
|
||||
|
||||
try testing.expect(block_memory_address >= buffer_address_head and
|
||||
block_memory_address < buffer_address_tail);
|
||||
}
|
||||
|
||||
// Reallocation.
|
||||
block_memory = try allocator.realloc(block_memory, .{
|
||||
.alignment = @alignOf(u64),
|
||||
.length = @sizeOf(u64),
|
||||
});
|
||||
|
||||
{
|
||||
const block_memory_address = @ptrToInt(block_memory);
|
||||
|
||||
try testing.expect(block_memory_address >= buffer_address_head and
|
||||
block_memory_address < buffer_address_tail);
|
||||
}
|
||||
|
||||
// Deallocation.
|
||||
allocator.dealloc(block_memory);
|
||||
}
|
||||
|
||||
///
|
||||
/// Returns an [io.Writer] wrapping `fixed_stack`.
|
||||
///
|
||||
/// Writing to the returned [io.Writer] will push values to the underlying [Fixed] stack instance
|
||||
/// referenced by `fixed_stack` until it is full.
|
||||
///
|
||||
pub fn fixedWriter(fixed_stack: *Fixed(u8)) io.Writer {
|
||||
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;
|
||||
}
|
||||
}, fixed_stack));
|
||||
}
|
||||
|
||||
test "fixedWriter" {
|
||||
var buffer = [_]u8{0} ** 4;
|
||||
var sequence_stack = Fixed(u8){.buffer = &buffer};
|
||||
const sequence_data = [_]u8{8, 16, 32, 64};
|
||||
|
||||
try testing.expect((try fixedWriter(&sequence_stack).
|
||||
write(&sequence_data)) == sequence_data.len);
|
||||
|
||||
try testing.expect(io.equals(u8, sequence_stack.buffer, &sequence_data));
|
||||
}
|
|
@ -0,0 +1,221 @@
|
|||
const io = @import("./io.zig");
|
||||
const stack = @import("./stack.zig");
|
||||
const testing = @import("./testing.zig");
|
||||
|
||||
///
|
||||
/// Returns a hash-backed table type of `Value`s indexed by `Key` and using `key_context` as the key
|
||||
/// context.
|
||||
///
|
||||
pub fn Hashed(comptime Key: type, comptime Value: type,
|
||||
comptime key_context: KeyContext(Key)) type {
|
||||
|
||||
const Allocator = io.Allocator;
|
||||
|
||||
return struct {
|
||||
allocator: Allocator,
|
||||
load_limit: f32,
|
||||
buckets: []Bucket,
|
||||
filled: usize,
|
||||
|
||||
///
|
||||
/// A slot in the hash table.
|
||||
///
|
||||
const Bucket = struct {
|
||||
maybe_entry: ?struct {
|
||||
key: Key,
|
||||
value: Value,
|
||||
} = null,
|
||||
|
||||
maybe_next_index: ?usize = null,
|
||||
};
|
||||
|
||||
///
|
||||
/// Errors that may occur during initialization of a hash table.
|
||||
///
|
||||
pub const InitError = io.AllocationError;
|
||||
|
||||
///
|
||||
/// Hash table type.
|
||||
///
|
||||
const Self = @This();
|
||||
|
||||
///
|
||||
/// Deinitializes `self`, preventing any further use.
|
||||
///
|
||||
pub fn deinit(self: *Self) void {
|
||||
io.free(self.allocator, self.buckets);
|
||||
|
||||
self.buckets = &.{};
|
||||
}
|
||||
|
||||
///
|
||||
/// Initializes a [Self] using `allocator` as the memory allocation strategy.
|
||||
///
|
||||
/// Returns a new [Self] value or an [InitError] if initializing failed.
|
||||
///
|
||||
pub fn init(allocator: Allocator) InitError!Self {
|
||||
const capacity = 4;
|
||||
|
||||
return Self{
|
||||
.buckets = (try io.makeMany(Bucket, allocator, capacity))[0 .. capacity],
|
||||
.filled = 0,
|
||||
.allocator = allocator,
|
||||
.load_limit = 0.75,
|
||||
};
|
||||
}
|
||||
|
||||
///
|
||||
/// Searches for `key` and deletes it from `self.
|
||||
///
|
||||
/// The removed value is returned or `null` if no key matching `key` was found.
|
||||
///
|
||||
pub fn remove(self: *Self, key: Key) ?Value {
|
||||
var bucket = &(self.buckets[@mod(key_context.hash(key), self.buckets.len)]);
|
||||
|
||||
if (bucket.maybe_entry) |*entry| if (key_context.equals(entry.key, key)) {
|
||||
defer {
|
||||
bucket.maybe_entry = null;
|
||||
self.filled -= 1;
|
||||
}
|
||||
|
||||
return entry.value;
|
||||
};
|
||||
|
||||
while (bucket.maybe_next_index) |index| {
|
||||
bucket = &(self.buckets[index]);
|
||||
|
||||
if (bucket.maybe_entry) |*entry| if (key_context.equals(entry.key, key)) {
|
||||
defer {
|
||||
bucket.maybe_entry = null;
|
||||
self.filled -= 1;
|
||||
}
|
||||
|
||||
return entry.value;
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
///
|
||||
/// Attempts to insert the value at `key` to be `value` in `self`, returning an
|
||||
/// [InsertError] if it fails.
|
||||
///
|
||||
pub fn insert(self: *Self, key: Key, value: Value) InsertError!void {
|
||||
if (self.isOverloaded()) {
|
||||
const old_buckets = self.buckets;
|
||||
|
||||
defer io.free(self.allocator, old_buckets);
|
||||
|
||||
const bucket_count = old_buckets.len * 2;
|
||||
|
||||
self.buckets = (try io.makeMany(Bucket, self.allocator,
|
||||
bucket_count))[0 .. bucket_count];
|
||||
|
||||
for (old_buckets) |bucket, index| self.buckets[index] = bucket;
|
||||
}
|
||||
|
||||
var hash = @mod(key_context.hash(key), self.buckets.len);
|
||||
|
||||
while (true) {
|
||||
const bucket = &(self.buckets[hash]);
|
||||
|
||||
if (key_context.equals((bucket.maybe_entry orelse {
|
||||
bucket.maybe_entry = .{
|
||||
.key = key,
|
||||
.value = value
|
||||
};
|
||||
|
||||
self.filled += 1;
|
||||
|
||||
break;
|
||||
}).key, key)) return error.KeyExists;
|
||||
|
||||
hash = @mod(hash + 1, self.buckets.len);
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
/// Returns `true` if the current load factor, derived from the number of elements filling
|
||||
/// the bucket table, is greater than the current load limit.
|
||||
///
|
||||
pub fn isOverloaded(self: Self) bool {
|
||||
return (@intToFloat(f32, self.filled) /
|
||||
@intToFloat(f32, self.buckets.len)) >= self.load_limit;
|
||||
}
|
||||
|
||||
///
|
||||
/// Searches for a value indexed with `key` in `self`.
|
||||
///
|
||||
/// The found value is returned or `null` if any key matching `key` failed to be found.
|
||||
///
|
||||
pub fn lookup(self: Self, key: Key) ?Value {
|
||||
var bucket = &(self.buckets[@mod(key_context.hash(key), self.buckets.len)]);
|
||||
|
||||
if (bucket.maybe_entry) |entry|
|
||||
if (key_context.equals(entry.key, key)) return entry.value;
|
||||
|
||||
while (bucket.maybe_next_index) |index| {
|
||||
bucket = &(self.buckets[index]);
|
||||
|
||||
if (bucket.maybe_entry) |entry|
|
||||
if (key_context.equals(entry.key, key)) return entry.value;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
///
|
||||
/// [InsertError.KeyExists] occurs when an insertion was attempted on a table with a matching key
|
||||
/// already present.
|
||||
///
|
||||
pub const InsertError = io.AllocationError || error {
|
||||
KeyExists,
|
||||
};
|
||||
|
||||
///
|
||||
/// Returns a context type for handling `Key` as a key in a table, associating hashing and equality
|
||||
/// behaviors to it.
|
||||
///
|
||||
pub fn KeyContext(comptime Key: type) type {
|
||||
return struct {
|
||||
hash: fn (Key) usize,
|
||||
equals: fn (Key, Key) bool,
|
||||
};
|
||||
}
|
||||
|
||||
///
|
||||
/// A [KeyContext] for dealing with string literal (i.e. []const u8) values.
|
||||
///
|
||||
/// **Note** that, while lightweight, this context should only be considered safe to use with string
|
||||
/// literals or variables pointing to string literals - as the [KeyContext] does not take ownership
|
||||
/// of its keys beyond copying the reference.
|
||||
///
|
||||
pub const string_literal_context = KeyContext([]const u8){
|
||||
.hash = io.hashBytes,
|
||||
|
||||
.equals = struct {
|
||||
fn stringsEqual(this: []const u8, that: []const u8) bool {
|
||||
return io.equals(u8, this, that);
|
||||
}
|
||||
}.stringsEqual,
|
||||
};
|
||||
|
||||
test "Hashed([]const u8, u32, string_literal_context)" {
|
||||
var buffer = [_]u8{0} ** 4096;
|
||||
var memory = stack.Fixed(u8){.buffer = &buffer};
|
||||
|
||||
var table = try Hashed([]const u8, u32, string_literal_context).
|
||||
init(stack.fixedAllocator(&memory));
|
||||
|
||||
defer table.deinit();
|
||||
|
||||
const foo = 69;
|
||||
|
||||
try testing.expect(table.remove("foo") == null);
|
||||
try table.insert("foo", foo);
|
||||
try testing.expect(table.remove("foo").? == foo);
|
||||
try testing.expect(table.remove("foo") == null);
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
///
|
||||
/// [TestError.UnexpectedResult] occurs when a conditional that should have been `true` was actually
|
||||
/// `false`.
|
||||
///
|
||||
pub const TestError = error {
|
||||
UnexpectedResult,
|
||||
};
|
||||
|
||||
///
|
||||
/// Returns a [TestError] if `ok` is false.
|
||||
///
|
||||
pub fn expect(ok: bool) TestError!void {
|
||||
if (!ok) return error.UnexpectedResult;
|
||||
}
|
||||
|
||||
test "expect" {
|
||||
try expect(true);
|
||||
|
||||
expect(false) catch {};
|
||||
}
|
||||
|
||||
pub const expectError = @import("std").testing.expectError;
|
|
@ -0,0 +1,119 @@
|
|||
const io = @import("./io.zig");
|
||||
const math = @import("./math.zig");
|
||||
const stack = @import("./stack.zig");
|
||||
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 = io.AccessError || error {
|
||||
WriteFailure,
|
||||
};
|
||||
|
||||
///
|
||||
/// Named identifiers for number formats used in printing functions.
|
||||
///
|
||||
pub const Radix = enum {
|
||||
binary,
|
||||
tinary,
|
||||
quaternary,
|
||||
quinary,
|
||||
senary,
|
||||
septenary,
|
||||
octal,
|
||||
nonary,
|
||||
decimal,
|
||||
undecimal,
|
||||
duodecimal,
|
||||
tridecimal,
|
||||
tetradecimal,
|
||||
pentadecimal,
|
||||
hexadecimal,
|
||||
|
||||
///
|
||||
/// Returns the base number of `radix`.
|
||||
///
|
||||
pub fn base(radix: Radix) u8 {
|
||||
return switch (radix) {
|
||||
.binary => 2,
|
||||
.tinary => 3,
|
||||
.quaternary => 4,
|
||||
.quinary => 5,
|
||||
.senary => 6,
|
||||
.septenary => 7,
|
||||
.octal => 8,
|
||||
.nonary => 9,
|
||||
.decimal => 10,
|
||||
.undecimal => 11,
|
||||
.duodecimal => 12,
|
||||
.tridecimal => 13,
|
||||
.tetradecimal => 14,
|
||||
.pentadecimal => 15,
|
||||
.hexadecimal => 16,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
///
|
||||
/// Writes `value` as a ASCII / UTF-8 encoded integer to `writer`, returning `true` if the full
|
||||
/// sequence was successfully written, otherwise `false`.
|
||||
///
|
||||
/// The `radix` argument identifies which base system to format `value` as.
|
||||
///
|
||||
pub fn printInt(writer: io.Writer, radix: Radix, value: anytype) PrintError!void {
|
||||
const Int = @TypeOf(value);
|
||||
|
||||
switch (@typeInfo(Int)) {
|
||||
.Int => |info| {
|
||||
if (value == 0) {
|
||||
const zero = "0";
|
||||
|
||||
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].
|
||||
var buffer = [_]u8{0} ** (@sizeOf(Int) * (@bitSizeOf(u8) / 4));
|
||||
var buffer_count: usize = 0;
|
||||
var n1 = value;
|
||||
|
||||
if (info.signedness == .signed and value < 0) {
|
||||
// Negative value.
|
||||
n1 = -value;
|
||||
buffer[0] = '-';
|
||||
buffer_count += 1;
|
||||
}
|
||||
|
||||
while (n1 != 0) {
|
||||
const base = radix.base();
|
||||
|
||||
buffer[buffer_count] = @intCast(u8, (n1 % base) + '0');
|
||||
n1 = (n1 / base);
|
||||
buffer_count += 1;
|
||||
}
|
||||
|
||||
for (buffer[0 .. (buffer_count / 2)]) |_, i|
|
||||
io.swap(u8, &buffer[i], &buffer[buffer_count - i - 1]);
|
||||
|
||||
if ((try writer.write(buffer[0 .. buffer_count])) != buffer_count)
|
||||
return error.WriteFailure;
|
||||
}
|
||||
},
|
||||
|
||||
// Cast comptime int into known-size integer and try again.
|
||||
.ComptimeInt => return printInt(writer, radix,
|
||||
@intCast(math.IntFittingRange(value, value), value)),
|
||||
|
||||
else => @compileError("`value` must be of type int or comptime_int"),
|
||||
}
|
||||
}
|
||||
|
||||
test "printInt" {
|
||||
// Max digits to represent a decimal u8 is 3 (i.e. 127 / 255).
|
||||
var decimal_buffer = [_]u8{0} ** 3;
|
||||
var decimal_stack = stack.Fixed(u8){.buffer = &decimal_buffer};
|
||||
var decimal_writer = stack.fixedWriter(&decimal_stack);
|
||||
|
||||
try printInt(decimal_writer, .decimal, 365);
|
||||
try testing.expect(decimal_stack.isFull());
|
||||
}
|
148
src/io.zig
148
src/io.zig
|
@ -1,148 +0,0 @@
|
|||
const stack = @import("./stack.zig");
|
||||
const std = @import("std");
|
||||
|
||||
///
|
||||
/// Opaque interface to a "writable" resource, such as a block device, memory buffer, or network
|
||||
/// socket.
|
||||
///
|
||||
pub const Writer = struct {
|
||||
context: *anyopaque,
|
||||
writeContext: fn (*anyopaque, []const u8) usize,
|
||||
|
||||
///
|
||||
/// Radices supported by [writeInt].
|
||||
///
|
||||
pub const Radix = enum {
|
||||
binary,
|
||||
tinary,
|
||||
quaternary,
|
||||
quinary,
|
||||
senary,
|
||||
septenary,
|
||||
octal,
|
||||
nonary,
|
||||
decimal,
|
||||
undecimal,
|
||||
duodecimal,
|
||||
tridecimal,
|
||||
tetradecimal,
|
||||
pentadecimal,
|
||||
hexadecimal,
|
||||
};
|
||||
|
||||
///
|
||||
/// Wraps and returns a reference to `write_context` of type `WriteContext` and its associated
|
||||
/// `writeContext` writing operation in a [Writer].
|
||||
///
|
||||
pub fn wrap(
|
||||
comptime WriteContext: type,
|
||||
write_context: *WriteContext,
|
||||
comptime writeContext: fn (*WriteContext, []const u8) usize
|
||||
) Writer {
|
||||
return .{
|
||||
.context = write_context,
|
||||
|
||||
.writeContext = struct {
|
||||
fn write(context: *anyopaque, buffer: []const u8) usize {
|
||||
return writeContext(@ptrCast(*WriteContext,
|
||||
@alignCast(@alignOf(WriteContext), context)), buffer);
|
||||
}
|
||||
}.write,
|
||||
};
|
||||
}
|
||||
|
||||
///
|
||||
/// Attempts to write `buffer` to `writer`, returning the number of bytes from `buffer` that
|
||||
/// were successfully written.
|
||||
///
|
||||
pub fn write(writer: Writer, buffer: []const u8) usize {
|
||||
return writer.writeContext(writer.context, buffer);
|
||||
}
|
||||
|
||||
///
|
||||
/// Writes the singular `byte` to `writer`, returning `true` if it was successfully written,
|
||||
/// otherwise `false`.
|
||||
///
|
||||
pub fn writeByte(writer: Writer, byte: u8) bool {
|
||||
return (writer.writeContext(writer.context,
|
||||
@ptrCast([*]const u8, &byte)[0 .. 1]) != 0);
|
||||
}
|
||||
|
||||
///
|
||||
/// Writes `value` as a ASCII / UTF-8 encoded integer to `writer`, returning `true` if the full
|
||||
/// sequence was successfully written, otherwise `false`.
|
||||
///
|
||||
/// The `radix` argument identifies which base system to encode `value` as, with `10` being
|
||||
/// decimal, `16` being hexadecimal, `8` being octal`, so on and so forth.
|
||||
///
|
||||
pub fn writeInt(writer: Writer, radix: Radix, value: anytype) bool {
|
||||
const Int = @TypeOf(value);
|
||||
const type_info = @typeInfo(Int);
|
||||
|
||||
switch (type_info) {
|
||||
.Int => {
|
||||
if (value == 0) return writer.writeByte('0');
|
||||
|
||||
// TODO: Unhardcode this as it will break with large ints.
|
||||
var buffer = std.mem.zeroes([28]u8);
|
||||
var buffer_count = @as(usize, 0);
|
||||
var n1 = value;
|
||||
|
||||
if ((type_info.Int.signedness == .signed) and (value < 0)) {
|
||||
// Negative value.
|
||||
n1 = -value;
|
||||
buffer[0] = '-';
|
||||
buffer_count += 1;
|
||||
}
|
||||
|
||||
while (n1 != 0) {
|
||||
const base = @enumToInt(radix);
|
||||
|
||||
buffer[buffer_count] = @intCast(u8, (n1 % base) + '0');
|
||||
n1 = (n1 / base);
|
||||
buffer_count += 1;
|
||||
}
|
||||
|
||||
for (buffer[0 .. (buffer_count / 2)]) |_, i|
|
||||
std.mem.swap(u8, &buffer[i], &buffer[buffer_count - i - 1]);
|
||||
|
||||
return (writer.write(buffer[0 .. buffer_count]) == buffer_count);
|
||||
},
|
||||
|
||||
// Cast comptime int into known-size integer and try again.
|
||||
.ComptimeInt => return writer.
|
||||
writeInt(radix, @intCast(std.math.IntFittingRange(value, value), value)),
|
||||
|
||||
else => @compileError("value must be of type int"),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
///
|
||||
/// Writer that silently throws consumed data away and never fails.
|
||||
///
|
||||
/// This is commonly used for testing or redirected otherwise unwanted output data that can't not be
|
||||
/// sent somewhere for whatever reason.
|
||||
///
|
||||
pub const null_writer = Writer{
|
||||
.context = undefined,
|
||||
|
||||
.writeContext = struct {
|
||||
fn write(_: *anyopaque, buffer: []const u8) usize {
|
||||
return buffer.len;
|
||||
}
|
||||
}.write,
|
||||
};
|
||||
|
||||
test {
|
||||
const testing = std.testing;
|
||||
|
||||
{
|
||||
const sequence = "foo";
|
||||
|
||||
try testing.expectEqual(null_writer.write(sequence), sequence.len);
|
||||
}
|
||||
|
||||
try testing.expect(null_writer.writeByte(0));
|
||||
try testing.expect(null_writer.writeInt(.decimal, 420));
|
||||
}
|
50
src/main.zig
50
src/main.zig
|
@ -1,50 +0,0 @@
|
|||
const ext = @cImport({
|
||||
@cInclude("SDL2/SDL.h");
|
||||
});
|
||||
|
||||
const io = @import("./io.zig");
|
||||
const stack = @import("./stack.zig");
|
||||
const std = @import("std");
|
||||
const sys = @import("./sys.zig");
|
||||
|
||||
///
|
||||
/// Entry point.
|
||||
///
|
||||
pub fn main() anyerror!void {
|
||||
return sys.runGraphics(anyerror, run);
|
||||
}
|
||||
|
||||
test {
|
||||
_ = io;
|
||||
_ = stack;
|
||||
_ = std;
|
||||
_ = sys;
|
||||
}
|
||||
|
||||
fn run(event_loop: *sys.EventLoop, graphics: *sys.GraphicsContext) anyerror!void {
|
||||
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
||||
|
||||
defer _ = gpa.deinit();
|
||||
|
||||
{
|
||||
const file_access = try event_loop.open(.readonly,
|
||||
try sys.FileSystem.data.joinedPath(&.{"data", "ona.lua"}));
|
||||
|
||||
defer event_loop.close(file_access);
|
||||
|
||||
const file_size = try file_access.size(event_loop);
|
||||
const allocator = gpa.allocator();
|
||||
const buffer = try allocator.alloc(u8, file_size);
|
||||
|
||||
defer allocator.free(buffer);
|
||||
|
||||
if ((try event_loop.readFile(file_access, buffer)) != file_size)
|
||||
return error.ScriptLoadFailure;
|
||||
|
||||
event_loop.log(.debug, buffer);
|
||||
}
|
||||
|
||||
while (graphics.poll()) |_| {
|
||||
graphics.present();
|
||||
}
|
||||
}
|
87
src/mem.zig
87
src/mem.zig
|
@ -1,87 +0,0 @@
|
|||
const std = @import("std");
|
||||
|
||||
///
|
||||
/// State machine for lazily computing all components of [Spliterator.source] that match the pattern
|
||||
/// in [Spliterator.delimiter].
|
||||
///
|
||||
pub fn Spliterator(comptime Element: type) type {
|
||||
return struct {
|
||||
source: []const Element,
|
||||
delimiter: []const Element,
|
||||
|
||||
const Self = @This();
|
||||
|
||||
///
|
||||
/// Returns `true` if there is more data to be processed, otherwise `false`.
|
||||
///
|
||||
pub fn hasNext(self: Self) bool {
|
||||
return (self.source.len != 0);
|
||||
}
|
||||
|
||||
///
|
||||
/// Iterates on `self` and returns the next view of [Spliterator.source] that matches
|
||||
/// [Spliterator.delimiter], or `null` if there is no more data to be processed.
|
||||
///
|
||||
pub fn next(self: *Self) ?[]const Element {
|
||||
if (!self.hasNext()) return null;
|
||||
|
||||
if (std.mem.indexOfPos(Element, self.source, 0, self.delimiter)) |index| {
|
||||
defer self.source = self.source[(index + self.delimiter.len) .. self.source.len];
|
||||
|
||||
return self.source[0 .. index];
|
||||
}
|
||||
|
||||
defer self.source = self.source[self.source.len .. self.source.len];
|
||||
|
||||
return self.source;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
test {
|
||||
const testing = std.testing;
|
||||
|
||||
// Single-character delimiter.
|
||||
{
|
||||
var spliterator = Spliterator(u8){
|
||||
.source = "single.character.separated.hello.world",
|
||||
.delimiter = ".",
|
||||
};
|
||||
|
||||
const components = [_][]const u8{"single", "character", "separated", "hello", "world"};
|
||||
var index = @as(usize, 0);
|
||||
|
||||
while (spliterator.next()) |split| : (index += 1) {
|
||||
try testing.expect(std.mem.eql(u8, split, components[index]));
|
||||
}
|
||||
}
|
||||
|
||||
// Multi-character delimiter.
|
||||
{
|
||||
var spliterator = Spliterator(u8){
|
||||
.source = "finding a needle in a needle stack",
|
||||
.delimiter = "needle",
|
||||
};
|
||||
|
||||
const components = [_][]const u8{"finding a ", " in a ", " stack"};
|
||||
var index = @as(usize, 0);
|
||||
|
||||
while (spliterator.next()) |split| : (index += 1) {
|
||||
try testing.expect(std.mem.eql(u8, split, components[index]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
/// Searches the slice of `Data` referenced by `data` for the first instance of `sought_datum`,
|
||||
/// returning its index or `null` if it could not be found.
|
||||
///
|
||||
pub fn findFirst(comptime Data: type, data: []const Data, sought_datum: Data) ?usize {
|
||||
for (data) |datum, index| if (datum == sought_datum) return index;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
test {
|
||||
try std.testing.expectEqual(findFirst(u8, "1234567890", '7'), 6);
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
const core = @import("core");
|
||||
const std = @import("std");
|
||||
const sys = @import("./sys.zig");
|
||||
|
||||
///
|
||||
/// Application entry-point.
|
||||
///
|
||||
pub fn main() anyerror!void {
|
||||
return nosuspend await async sys.display(anyerror, runEngine);
|
||||
}
|
||||
|
||||
///
|
||||
/// Runs the game engine.
|
||||
///
|
||||
fn runEngine(app: *sys.App, graphics: *sys.Graphics) anyerror!void {
|
||||
{
|
||||
const path = try sys.Path.from(&.{"ona.lua"});
|
||||
var file_reader = try app.data.openFileReader(path);
|
||||
|
||||
defer file_reader.close();
|
||||
|
||||
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 core.io.free(allocator, buffer);
|
||||
|
||||
if ((try file_reader.read(buffer)) != file_size) return error.ScriptLoadFailure;
|
||||
|
||||
app.log(.debug, buffer);
|
||||
}
|
||||
|
||||
while (graphics.poll()) |_| {
|
||||
graphics.present();
|
||||
}
|
||||
}
|
||||
|
||||
test {
|
||||
_ = sys;
|
||||
}
|
|
@ -0,0 +1,103 @@
|
|||
const core = @import("core");
|
||||
const sys = @import("./sys.zig");
|
||||
|
||||
///
|
||||
/// Metadata of an Oar archive file entry.
|
||||
///
|
||||
const Entry = extern struct {
|
||||
signature: [signature_magic.len]u8 = signature_magic,
|
||||
path: sys.Path = sys.Path.empty,
|
||||
data_offset: u64 = 0,
|
||||
data_length: u64 = 0,
|
||||
padding: [232]u8 = [_]u8{0} ** 232,
|
||||
|
||||
comptime {
|
||||
const entry_size = @sizeOf(@This());
|
||||
|
||||
if (entry_size != 512) @compileError("EntryBlock is not 512 bytes");
|
||||
}
|
||||
};
|
||||
|
||||
///
|
||||
/// [FindError.ArchiveUnsupported] occurs when trying to read a file that does not follow an Oar
|
||||
/// archive format considered valid by this implemenatation.
|
||||
///
|
||||
/// [FindError.EntryNotFound] occurs when the queried entry was not found in the archive file.
|
||||
///
|
||||
pub const FindError = core.io.AccessError || error {
|
||||
ArchiveUnsupported,
|
||||
EntryNotFound,
|
||||
};
|
||||
|
||||
///
|
||||
/// 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,
|
||||
padding: [502]u8 = [_]u8{0} ** 502,
|
||||
|
||||
comptime {
|
||||
const size = @sizeOf(@This());
|
||||
|
||||
if (size != 512) @compileError("Header is not 512 bytes");
|
||||
}
|
||||
};
|
||||
|
||||
///
|
||||
/// 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;
|
||||
|
||||
///
|
||||
/// Magic identifier used to validate [Header] and [Block] data.
|
||||
///
|
||||
const signature_magic = [3]u8{'o', 'a', 'r'};
|
|
@ -0,0 +1,781 @@
|
|||
const ext = @cImport({
|
||||
@cInclude("SDL2/SDL.h");
|
||||
});
|
||||
|
||||
const core = @import("core");
|
||||
const oar = @import("./oar.zig");
|
||||
const std = @import("std");
|
||||
|
||||
///
|
||||
/// Thread-safe platform abstraction over multiplexing system I/O processing and event handling.
|
||||
///
|
||||
pub const App = struct {
|
||||
message_chain: ?*Message = null,
|
||||
message_semaphore: *ext.SDL_sem,
|
||||
message_mutex: *ext.SDL_mutex,
|
||||
data: FileSystem,
|
||||
user: FileSystem,
|
||||
|
||||
///
|
||||
/// Enqueues `message` to the message chain in `app`.
|
||||
///
|
||||
fn enqueue(app: *App, message: *Message) void {
|
||||
{
|
||||
// TODO: Error check these.
|
||||
_ = ext.SDL_LockMutex(app.message_mutex);
|
||||
|
||||
defer _ = ext.SDL_UnlockMutex(app.message_mutex);
|
||||
|
||||
if (app.message_chain) |message_chain| {
|
||||
message_chain.next = message;
|
||||
} else {
|
||||
app.message_chain = message;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Error check this.
|
||||
_ = ext.SDL_SemPost(app.message_semaphore);
|
||||
}
|
||||
|
||||
///
|
||||
/// Asynchronously executes `procedure` with `arguments` as an anonymous struct of its arguments
|
||||
/// and `app` as its execution context.
|
||||
///
|
||||
/// Once the execution frame resumes, the value returned by executing `procedure` is returned.
|
||||
///
|
||||
pub fn schedule(app: *App, procedure: anytype,
|
||||
arguments: anytype) core.meta.FnReturn(@TypeOf(procedure)) {
|
||||
|
||||
const Task = struct {
|
||||
procedure: @TypeOf(procedure),
|
||||
arguments: *@TypeOf(arguments),
|
||||
result: core.meta.FnReturn(@TypeOf(procedure)),
|
||||
|
||||
const Task = @This();
|
||||
|
||||
fn process(userdata: *anyopaque) void {
|
||||
const task = @ptrCast(*Task, @alignCast(@alignOf(Task), userdata));
|
||||
|
||||
task.result = @call(.{}, task.procedure, task.arguments.*);
|
||||
}
|
||||
};
|
||||
|
||||
var task = Task{
|
||||
.procedure = procedure,
|
||||
.arguments = &arguments,
|
||||
};
|
||||
|
||||
var message = Message{
|
||||
.kind = .{.task = .{
|
||||
.data = &task,
|
||||
.action = Task.process,
|
||||
.frame = @frame(),
|
||||
}},
|
||||
};
|
||||
|
||||
suspend app.enqueue(&message);
|
||||
}
|
||||
|
||||
///
|
||||
/// Asynchronously logs `info` with `logger` as the logging method and `app` as the execution
|
||||
/// context.
|
||||
///
|
||||
pub fn log(app: *App, logger: Logger, info: []const u8) void {
|
||||
var message = Message{
|
||||
.kind = .{.log = .{
|
||||
.logger = logger,
|
||||
.info = info,
|
||||
}},
|
||||
};
|
||||
|
||||
app.enqueue(&message);
|
||||
}
|
||||
};
|
||||
|
||||
///
|
||||
/// Snapshotted information about the status of a file.
|
||||
///
|
||||
pub const FileStatus = struct {
|
||||
length: u64,
|
||||
};
|
||||
|
||||
///
|
||||
kayomn marked this conversation as resolved
|
||||
/// Interface for working with bi-directional, streamable resources accessed through a file-system.
|
||||
///
|
||||
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,
|
||||
},
|
||||
|
||||
///
|
||||
/// 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"),
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
///
|
||||
/// Platform-agnostic mechanism for working with an abstraction of the underlying file-system(s)
|
||||
/// available to the application in a sandboxed environment.
|
||||
///
|
||||
pub const FileSystem = union(enum) {
|
||||
native: []const u8,
|
||||
|
||||
archive: struct {
|
||||
file_system: *const FileSystem,
|
||||
path: Path,
|
||||
},
|
||||
|
||||
///
|
||||
/// [AccessError.FileNotFound] occurs when a queried file could not be found on the file-system
|
||||
/// 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.
|
||||
///
|
||||
/// [AccessError.FileSystemfailure] denotes a file-system implementation-specific failure to
|
||||
/// access resources has occured and therefore cannot proceed to access the file.
|
||||
///
|
||||
pub const AccessError = error {
|
||||
FileNotFound,
|
||||
FileSystemFailure,
|
||||
};
|
||||
|
||||
///
|
||||
/// 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.
|
||||
///
|
||||
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);
|
||||
|
||||
errdefer 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,
|
||||
};
|
||||
|
||||
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| {
|
||||
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.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),
|
||||
};
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
///
|
||||
///
|
||||
///
|
||||
pub const Graphics = opaque {
|
||||
///
|
||||
///
|
||||
///
|
||||
pub const Event = struct {
|
||||
keys_up: Keys = std.mem.zeroes(Keys),
|
||||
keys_down: Keys = std.mem.zeroes(Keys),
|
||||
keys_held: Keys = std.mem.zeroes(Keys),
|
||||
|
||||
const Keys = [256]bool;
|
||||
};
|
||||
|
||||
///
|
||||
///
|
||||
///
|
||||
const Implementation = struct {
|
||||
event: Event,
|
||||
};
|
||||
|
||||
///
|
||||
///
|
||||
///
|
||||
pub fn poll(graphics: *Graphics) ?*const Event {
|
||||
_ = graphics;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
///
|
||||
///
|
||||
///
|
||||
pub fn present(graphics: *Graphics) void {
|
||||
// TODO: Implement;
|
||||
_ = graphics;
|
||||
}
|
||||
};
|
||||
|
||||
///
|
||||
/// [Logger.info] logs information that isn't necessarily an error but indicates something useful to
|
||||
/// be logged.
|
||||
///
|
||||
/// [Logger.debug] logs information only when the engine is in debug mode.
|
||||
///
|
||||
/// [Logger.warning] logs information to indicate a non-critical error has occured.
|
||||
///
|
||||
pub const Logger = enum(u32) {
|
||||
info = ext.SDL_LOG_PRIORITY_INFO,
|
||||
debug = ext.SDL_LOG_PRIORITY_DEBUG,
|
||||
warning = ext.SDL_LOG_PRIORITY_WARN,
|
||||
};
|
||||
|
||||
///
|
||||
/// Linked list of asynchronous messages chained together to be processed by the work processor.
|
||||
///
|
||||
pub const Message = struct {
|
||||
next: ?*Message = null,
|
||||
|
||||
kind: union(enum) {
|
||||
quit,
|
||||
|
||||
log: struct {
|
||||
logger: Logger,
|
||||
info: []const u8,
|
||||
},
|
||||
|
||||
task: struct {
|
||||
data: *anyopaque,
|
||||
action: fn (*anyopaque) void,
|
||||
frame: anyframe,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
///
|
||||
/// Path to a file on a [FileSystem].
|
||||
///
|
||||
pub const Path = extern struct {
|
||||
buffer: [255]u8,
|
||||
length: u8,
|
||||
|
||||
///
|
||||
/// [Error.TooLong] occurs when creating a path that is greater than the maximum path size **in
|
||||
/// bytes**.
|
||||
///
|
||||
pub const FromError = error {
|
||||
TooLong,
|
||||
};
|
||||
|
||||
///
|
||||
/// An empty [Path] with a length of `0`.
|
||||
///
|
||||
pub const empty = Path{
|
||||
.buffer = [_]u8{0} ** 255,
|
||||
.length = 0,
|
||||
};
|
||||
|
||||
///
|
||||
/// 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]);
|
||||
}
|
||||
|
||||
///
|
||||
/// Returns `true` if `this` is equal to `that`, otherwise `false`.
|
||||
///
|
||||
pub fn equals(this: Path, that: Path) bool {
|
||||
return core.io.equals(u8, this.buffer[0 ..this.length], that.buffer[0 .. that.length]);
|
||||
}
|
||||
|
||||
///
|
||||
/// Attempts to create a [Path] with the path components in `sequences` as a fully qualified
|
||||
/// path from root.
|
||||
///
|
||||
/// A [Path] value is returned containing the fully qualified path from the file-system root or
|
||||
/// a [FromError] if it could not be created.
|
||||
///
|
||||
pub fn from(sequences: []const []const u8) FromError!Path {
|
||||
var path = empty;
|
||||
|
||||
if (sequences.len != 0) {
|
||||
const last_sequence_index = sequences.len - 1;
|
||||
|
||||
for (sequences) |sequence, index| if (sequence.len != 0) {
|
||||
var components = core.io.Spliterator(u8){
|
||||
.source = sequence,
|
||||
.delimiter = "/",
|
||||
};
|
||||
|
||||
while (components.next()) |component| if (component.len != 0) {
|
||||
for (component) |byte| {
|
||||
if (path.length == max) return error.TooLong;
|
||||
|
||||
path.buffer[path.length] = byte;
|
||||
path.length += 1;
|
||||
}
|
||||
|
||||
if (components.hasNext()) {
|
||||
if (path.length == max) return error.TooLong;
|
||||
|
||||
path.buffer[path.length] = '/';
|
||||
path.length += 1;
|
||||
}
|
||||
};
|
||||
|
||||
if (index < last_sequence_index) {
|
||||
if (path.length == max) return error.TooLong;
|
||||
|
||||
path.buffer[path.length] = '/';
|
||||
path.length += 1;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
///
|
||||
/// Returns the hash of the text in `path`.
|
||||
///
|
||||
pub fn hash(path: Path) usize {
|
||||
return core.io.hashBytes(path.buffer[0 .. path.length]);
|
||||
}
|
||||
|
||||
///
|
||||
/// Maximum number of **bytes** in a [Path].
|
||||
///
|
||||
pub const max = 255;
|
||||
|
||||
///
|
||||
/// Textual separator between components of a [Path].
|
||||
///
|
||||
pub const seperator = '/';
|
||||
};
|
||||
|
||||
///
|
||||
/// [RunError.InitFailure] occurs when the runtime fails to initialize.
|
||||
///
|
||||
pub const RunError = error {
|
||||
InitFailure,
|
||||
};
|
||||
|
||||
///
|
||||
/// Returns a thread-safe [core.io.Allocator] value based on the default system allocation strategy.
|
||||
///
|
||||
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));
|
||||
}
|
||||
|
||||
///
|
||||
/// Runs a graphical application referenced by `run` with `error` as its error set.
|
||||
///
|
||||
/// Should an error from `run` occur, an `Error` is returned, otherwise a [RunError] is returned if
|
||||
/// the underlying runtime fails and is logged.
|
||||
///
|
||||
pub fn display(comptime Error: anytype,
|
||||
comptime run: fn (*App, *Graphics) callconv(.Async) Error!void) (RunError || Error)!void {
|
||||
|
||||
const cwd = FileSystem{.native = "./"};
|
||||
const user_prefix = ext.SDL_GetPrefPath("ona", "ona") orelse return error.InitFailure;
|
||||
|
||||
defer ext.SDL_free(user_prefix);
|
||||
|
||||
var app = App{
|
||||
.user = .{.native = std.mem.sliceTo(user_prefix, 0)},
|
||||
|
||||
.message_semaphore = ext.SDL_CreateSemaphore(0) orelse {
|
||||
ext.SDL_LogCritical(ext.SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Failed to create message semaphore");
|
||||
|
||||
return error.InitFailure;
|
||||
},
|
||||
|
||||
.message_mutex = ext.SDL_CreateMutex() orelse {
|
||||
ext.SDL_LogCritical(ext.SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Failed to create message mutex");
|
||||
|
||||
return error.InitFailure;
|
||||
},
|
||||
|
||||
.data = .{.archive = .{
|
||||
.file_system = &cwd,
|
||||
.path = try Path.from(&.{"./data.oar"}),
|
||||
}},
|
||||
};
|
||||
|
||||
defer {
|
||||
ext.SDL_DestroySemaphore(app.message_semaphore);
|
||||
ext.SDL_DestroyMutex(app.message_mutex);
|
||||
}
|
||||
|
||||
const message_thread = ext.SDL_CreateThread(processMessages, "Message Processor", &app) orelse {
|
||||
ext.SDL_LogCritical(ext.SDL_LOG_CATEGORY_APPLICATION, "Failed to create message processor");
|
||||
|
||||
return error.InitFailure;
|
||||
};
|
||||
|
||||
defer {
|
||||
var message = Message{.kind = .quit};
|
||||
|
||||
app.enqueue(&message);
|
||||
|
||||
{
|
||||
var status: c_int = 0;
|
||||
|
||||
// SDL2 defines waiting on a null thread reference as a no-op. See
|
||||
// https://wiki.libsdl.org/SDL_WaitThread for more information
|
||||
ext.SDL_WaitThread(message_thread, &status);
|
||||
|
||||
if (status != 0) {
|
||||
// TODO: Error check this.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (ext.SDL_Init(ext.SDL_INIT_EVERYTHING) != 0) {
|
||||
ext.SDL_LogCritical(ext.SDL_LOG_CATEGORY_APPLICATION, "Failed to initialize runtime");
|
||||
|
||||
return error.InitFailure;
|
||||
}
|
||||
|
||||
defer ext.SDL_Quit();
|
||||
|
||||
const window = create_window: {
|
||||
const pos = ext.SDL_WINDOWPOS_UNDEFINED;
|
||||
var flags = @as(u32, 0);
|
||||
|
||||
break: create_window ext.SDL_CreateWindow("Ona", pos, pos, 640, 480, flags) orelse {
|
||||
ext.SDL_LogCritical(ext.SDL_LOG_CATEGORY_APPLICATION, "Failed to create window");
|
||||
|
||||
return error.InitFailure;
|
||||
};
|
||||
};
|
||||
|
||||
defer ext.SDL_DestroyWindow(window);
|
||||
|
||||
const renderer = create_renderer: {
|
||||
var flags = @as(u32, 0);
|
||||
|
||||
break: create_renderer ext.SDL_CreateRenderer(window, -1, flags) orelse {
|
||||
ext.SDL_LogCritical(ext.SDL_LOG_CATEGORY_APPLICATION, "Failed to create renderer");
|
||||
|
||||
return error.InitFailure;
|
||||
};
|
||||
};
|
||||
|
||||
defer ext.SDL_DestroyRenderer(renderer);
|
||||
|
||||
var graphics = Graphics.Implementation{
|
||||
.event = .{
|
||||
|
||||
},
|
||||
};
|
||||
|
||||
return run(@ptrCast(*App, &app), @ptrCast(*Graphics, &graphics));
|
||||
}
|
||||
|
||||
///
|
||||
/// [FileSystemMessage] processing function used by a dedicated worker thread, where `data`
|
||||
/// is a type-erased reference to a [EventLoop].
|
||||
///
|
||||
/// The processor returns `0` if it exited normally or any other value if an erroneous exit
|
||||
/// occured.
|
||||
///
|
||||
pub fn processMessages(userdata: ?*anyopaque) callconv(.C) c_int {
|
||||
const app = @ptrCast(*App, @alignCast(@alignOf(App), userdata orelse unreachable));
|
||||
|
||||
while (true) {
|
||||
// TODO: Error check these.
|
||||
_ = ext.SDL_SemWait(app.message_semaphore);
|
||||
_ = ext.SDL_LockMutex(app.message_mutex);
|
||||
|
||||
defer _ = ext.SDL_UnlockMutex(app.message_mutex);
|
||||
|
||||
while (app.message_chain) |message| {
|
||||
switch (message.kind) {
|
||||
.quit => return 0,
|
||||
|
||||
.log => |log| ext.SDL_LogMessage(ext.SDL_LOG_CATEGORY_APPLICATION,
|
||||
@enumToInt(log.logger), "%.*s", log.info.len, log.info.ptr),
|
||||
|
||||
.task => |task| {
|
||||
task.action(task.data);
|
||||
|
||||
resume task.frame;
|
||||
},
|
||||
}
|
||||
|
||||
app.message_chain = message.next;
|
||||
}
|
||||
}
|
||||
}
|
117
src/stack.zig
117
src/stack.zig
|
@ -1,117 +0,0 @@
|
|||
const io = @import("./io.zig");
|
||||
const std = @import("std");
|
||||
|
||||
pub fn Fixed(comptime Element: type) type {
|
||||
return struct {
|
||||
filled: usize = 0,
|
||||
buffer: []Element,
|
||||
|
||||
const Self = @This();
|
||||
|
||||
///
|
||||
/// Wraps `self` and returns it in a [io.Writer] value.
|
||||
///
|
||||
/// Note that this will raise a compilation error if [Element] is not `u8`.
|
||||
///
|
||||
pub fn writer(self: *Self) io.Writer {
|
||||
if (Element != u8) @compileError("Cannot coerce fixed stack of type " ++
|
||||
@typeName(Element) ++ " into a Writer");
|
||||
|
||||
return io.Writer.wrap(Self, self, struct {
|
||||
fn write(stack: *Self, buffer: []const u8) usize {
|
||||
stack.pushAll(buffer) catch |err| switch (err) {
|
||||
error.Overflow => return 0,
|
||||
};
|
||||
|
||||
return buffer.len;
|
||||
}
|
||||
}.write);
|
||||
}
|
||||
|
||||
///
|
||||
/// Clears all elements from `self`.
|
||||
///
|
||||
pub fn clear(self: *Self) void {
|
||||
self.filled = 0;
|
||||
}
|
||||
|
||||
///
|
||||
/// Counts and returns the number of pushed elements in `self`.
|
||||
///
|
||||
pub fn count(self: Self) usize {
|
||||
return self.filled;
|
||||
}
|
||||
|
||||
///
|
||||
/// Attempts to pop the tail-end of `self`, returning the element value or `null` if the
|
||||
/// stack is empty.
|
||||
///
|
||||
pub fn pop(self: *Self) ?Element {
|
||||
if (self.filled == 0) return null;
|
||||
|
||||
self.filled -= 1;
|
||||
|
||||
return self.buffer[self.filled];
|
||||
}
|
||||
|
||||
///
|
||||
/// Attempts to push `element` into `self`, returning a [FixedPushError] if it failed.
|
||||
///
|
||||
pub fn push(self: *Self, element: Element) FixedPushError!void {
|
||||
if (self.filled == self.buffer.len) return error.Overflow;
|
||||
|
||||
self.buffer[self.filled] = element;
|
||||
self.filled += 1;
|
||||
}
|
||||
|
||||
///
|
||||
/// Attempts to push all of `elements` into `self`, returning a [FixedPushError] if it
|
||||
/// failed.
|
||||
///
|
||||
pub fn pushAll(self: *Self, elements: []const u8) FixedPushError!void {
|
||||
const filled = (self.filled + elements.len);
|
||||
|
||||
if (filled > self.buffer.len) return error.Overflow;
|
||||
|
||||
std.mem.copy(u8, self.buffer[self.filled ..], elements);
|
||||
|
||||
self.filled = filled;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
///
|
||||
/// Potential errors that may occur while trying to push one or more elements into a stack of a
|
||||
/// known maximum size.
|
||||
///
|
||||
/// [FinitePushError.Overflow] is returned if the stack does not have sufficient capacity to hold a
|
||||
/// given set of elements.
|
||||
///
|
||||
pub const FixedPushError = error {
|
||||
Overflow,
|
||||
};
|
||||
|
||||
test {
|
||||
const testing = std.testing;
|
||||
var buffer = std.mem.zeroes([4]u8);
|
||||
var stack = Fixed(u8){.buffer = &buffer};
|
||||
|
||||
try testing.expectEqual(stack.count(), 0);
|
||||
try testing.expectEqual(stack.pop(), null);
|
||||
try stack.push(69);
|
||||
try testing.expectEqual(stack.count(), 1);
|
||||
try testing.expectEqual(stack.pop(), 69);
|
||||
try stack.pushAll(&.{42, 10, 95, 0});
|
||||
try testing.expectEqual(stack.count(), 4);
|
||||
try testing.expectError(FixedPushError.Overflow, stack.push(1));
|
||||
try testing.expectError(FixedPushError.Overflow, stack.pushAll(&.{1, 11, 11}));
|
||||
|
||||
stack.clear();
|
||||
|
||||
try testing.expectEqual(stack.count(), 0);
|
||||
|
||||
const writer = stack.writer();
|
||||
|
||||
try testing.expectEqual(writer.write(&.{0, 0, 0, 0}), 4);
|
||||
try testing.expectEqual(writer.writeByte(0), false);
|
||||
}
|
642
src/sys.zig
642
src/sys.zig
|
@ -1,642 +0,0 @@
|
|||
const ext = @cImport({
|
||||
@cInclude("SDL2/SDL.h");
|
||||
});
|
||||
|
||||
const io = @import("./io.zig");
|
||||
const mem = @import("./mem.zig");
|
||||
const stack = @import("./stack.zig");
|
||||
const std = @import("std");
|
||||
|
||||
///
|
||||
/// A thread-safe platform abstraction over multiplexing system I/O processing and event handling.
|
||||
///
|
||||
pub const EventLoop = opaque {
|
||||
///
|
||||
/// Linked list of messages chained together to be processed by the internal file system message
|
||||
/// processor of an [EventLoop].
|
||||
///
|
||||
const FileSystemMessage = struct {
|
||||
next: ?*FileSystemMessage = null,
|
||||
frame: anyframe,
|
||||
|
||||
request: union(enum) {
|
||||
exit,
|
||||
|
||||
close: struct {
|
||||
file_access: *FileAccess,
|
||||
},
|
||||
|
||||
log: struct {
|
||||
message: []const u8,
|
||||
kind: LogKind,
|
||||
},
|
||||
|
||||
open: struct {
|
||||
mode: OpenMode,
|
||||
file_system_path: *const FileSystem.Path,
|
||||
result: OpenError!*FileAccess = error.NotFound,
|
||||
},
|
||||
|
||||
read_file: struct {
|
||||
file_access: *FileAccess,
|
||||
buffer: []const u8,
|
||||
result: FileError!usize = error.Inaccessible,
|
||||
},
|
||||
|
||||
seek_file: struct {
|
||||
file_access: *FileAccess,
|
||||
origin: SeekOrigin,
|
||||
offset: usize,
|
||||
result: FileError!void = error.Inaccessible,
|
||||
},
|
||||
|
||||
tell_file: struct {
|
||||
file_access: *FileAccess,
|
||||
result: FileError!usize = error.Inaccessible,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
///
|
||||
/// Internal state of the event loop hidden from the API consumer.
|
||||
///
|
||||
const Implementation = struct {
|
||||
user_prefix: []const u8,
|
||||
file_system_semaphore: *ext.SDL_sem,
|
||||
file_system_mutex: *ext.SDL_mutex,
|
||||
file_system_thread: *ext.SDL_Thread,
|
||||
file_system_messages: ?*FileSystemMessage = null,
|
||||
|
||||
///
|
||||
/// Casts `event_loop` to a [Implementation] reference.
|
||||
///
|
||||
/// *Note* that if `event_loop` does not have the same alignment as [Implementation],
|
||||
/// safety-checked undefined behavior will occur.
|
||||
///
|
||||
fn cast(event_loop: *EventLoop) *Implementation {
|
||||
return @ptrCast(*Implementation, @alignCast(@alignOf(Implementation), event_loop));
|
||||
}
|
||||
};
|
||||
|
||||
///
|
||||
/// [LogKind.info] represents a log message which is purely informative and does not indicate
|
||||
/// any kind of issue.
|
||||
///
|
||||
/// [LogKind.debug] represents a log message which is purely for debugging purposes and will
|
||||
/// only occurs in debug builds.
|
||||
///
|
||||
/// [LogKind.warning] represents a log message which is a warning about a issue that does not
|
||||
/// break anything important but is not ideal.
|
||||
///
|
||||
pub const LogKind = enum(c_int) {
|
||||
info = ext.SDL_LOG_PRIORITY_INFO,
|
||||
debug = ext.SDL_LOG_PRIORITY_DEBUG,
|
||||
warning = ext.SDL_LOG_PRIORITY_WARN,
|
||||
};
|
||||
|
||||
///
|
||||
/// [OpenError.NotFound] is a catch-all for when a file could not be located to be opened. This
|
||||
/// may be as simple as it doesn't exist or the because the underlying file-system will not /
|
||||
/// cannot give access to it at this time.
|
||||
///
|
||||
pub const OpenError = error {
|
||||
NotFound,
|
||||
};
|
||||
|
||||
///
|
||||
/// [OpenMode.readonly] indicates that an existing file is opened in a read-only state,
|
||||
/// disallowing write access.
|
||||
///
|
||||
/// [OpenMode.overwrite] indicates that an empty file has been created or an existing file has
|
||||
/// been completely overwritten into.
|
||||
///
|
||||
/// [OpenMode.append] indicates that an existing file that has been opened for reading from and
|
||||
/// writing to on the end of existing data.
|
||||
///
|
||||
pub const OpenMode = enum {
|
||||
readonly,
|
||||
overwrite,
|
||||
append,
|
||||
};
|
||||
|
||||
///
|
||||
/// [SeekOrigin.head] indicates that a seek operation will seek from the offset origin of the
|
||||
/// file beginning, or "head".
|
||||
///
|
||||
/// [SeekOrigin.tail] indicates that a seek operation will seek from the offset origin of the
|
||||
/// file end, or "tail".
|
||||
///
|
||||
/// [SeekOrigin.cursor] indicates that a seek operation will seek from the current position of
|
||||
/// the file cursor.
|
||||
///
|
||||
pub const SeekOrigin = enum {
|
||||
head,
|
||||
tail,
|
||||
cursor,
|
||||
};
|
||||
|
||||
///
|
||||
/// Closes access to the file referenced by `file_access` via `event_loop`.
|
||||
///
|
||||
/// *Note* that nothing happens to `file_access` if it is already closed.
|
||||
///
|
||||
pub fn close(event_loop: *EventLoop, file_access: *FileAccess) void {
|
||||
var file_system_message = FileSystemMessage{
|
||||
.frame = @frame(),
|
||||
.request = .{.close = .{.file_access = file_access}},
|
||||
};
|
||||
|
||||
suspend event_loop.enqueueFileSystemMessage(&file_system_message);
|
||||
}
|
||||
|
||||
///
|
||||
/// Enqueues `message` to the file system message processor to be processed at a later, non-
|
||||
/// deterministic point.
|
||||
///
|
||||
fn enqueueFileSystemMessage(event_loop: *EventLoop, message: *FileSystemMessage) void {
|
||||
const implementation = Implementation.cast(event_loop);
|
||||
|
||||
// TODO: Error check this.
|
||||
_ = ext.SDL_LockMutex(implementation.file_system_mutex);
|
||||
|
||||
if (implementation.file_system_messages) |messages| {
|
||||
messages.next = message;
|
||||
} else {
|
||||
implementation.file_system_messages = message;
|
||||
}
|
||||
|
||||
// TODO: Error check these.
|
||||
_ = ext.SDL_UnlockMutex(implementation.file_system_mutex);
|
||||
_ = ext.SDL_SemPost(implementation.file_system_semaphore);
|
||||
}
|
||||
|
||||
///
|
||||
/// Writes `message` to the application log with `kind` via `event_loop`.
|
||||
///
|
||||
/// *Note* that `message` is not guaranteed to be partly, wholely, or at all written.
|
||||
///
|
||||
pub fn log(event_loop: *EventLoop, kind: LogKind, message: []const u8) void {
|
||||
var file_system_message = FileSystemMessage{
|
||||
.frame = @frame(),
|
||||
|
||||
.request = .{.log = .{
|
||||
.message = message,
|
||||
.kind = kind,
|
||||
}},
|
||||
};
|
||||
|
||||
suspend event_loop.enqueueFileSystemMessage(&file_system_message);
|
||||
}
|
||||
|
||||
///
|
||||
/// Attempts to open access to a file referenced at `file_system_path` using `mode` as the way
|
||||
/// to open it via `event_loop`.
|
||||
///
|
||||
/// A [FileAccess] pointer is returned referencing the opened file or a [OpenError] if the file
|
||||
/// could not be opened.
|
||||
///
|
||||
/// *Note* that all files are opened in "binary-mode", or Unix-mode. There are no conversions
|
||||
/// applied when data is accessed from a file.
|
||||
///
|
||||
pub fn open(event_loop: *EventLoop, mode: OpenMode,
|
||||
file_system_path: FileSystem.Path) OpenError!*FileAccess {
|
||||
|
||||
var file_system_message = FileSystemMessage{
|
||||
.frame = @frame(),
|
||||
|
||||
.request = .{.open = .{
|
||||
.mode = mode,
|
||||
.file_system_path = &file_system_path,
|
||||
}},
|
||||
};
|
||||
|
||||
suspend event_loop.enqueueFileSystemMessage(&file_system_message);
|
||||
|
||||
return file_system_message.request.open.result;
|
||||
}
|
||||
|
||||
///
|
||||
/// [FileSystemMessage] processing function used by a dedicated worker thread, where `data` is
|
||||
/// a type-erased reference to a [EventLoop].
|
||||
///
|
||||
/// The processor returns `0` if it exited normally or any other value if an erroneous exit
|
||||
/// occured.
|
||||
///
|
||||
fn processFileSystemMessages(data: ?*anyopaque) callconv(.C) c_int {
|
||||
const implementation = Implementation.cast(@ptrCast(*EventLoop, data orelse unreachable));
|
||||
|
||||
while (true) {
|
||||
while (implementation.file_system_messages) |messages| {
|
||||
switch (messages.request) {
|
||||
.exit => return 0,
|
||||
|
||||
.log => |*log_request| ext.SDL_LogMessage(ext.SDL_LOG_CATEGORY_APPLICATION,
|
||||
@enumToInt(log_request.priority), log_request.message),
|
||||
|
||||
.open => |*open_request| {
|
||||
switch (open_request.path.file_system) {
|
||||
.data => {
|
||||
// TODO: Implement
|
||||
open_request.result = error.NotFound;
|
||||
},
|
||||
|
||||
.user => {
|
||||
var path_buffer = std.mem.zeroes([4096]u8);
|
||||
var path = stack.Fixed(u8){.buffer = path_buffer[0 .. ]};
|
||||
|
||||
path.pushAll(implementation.user_prefix) catch {
|
||||
open_request.result = error.BadFileSystem;
|
||||
|
||||
continue;
|
||||
};
|
||||
|
||||
if (!open_request.path.write(path.writer())) {
|
||||
open_request.result = error.NotFound;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ext.SDL_RWFromFile(&path_buffer, switch (open_request.mode) {
|
||||
.readonly => "rb",
|
||||
.overwrite => "wb",
|
||||
.append => "ab",
|
||||
})) |rw_ops| {
|
||||
open_request.result = @ptrCast(*FileAccess, rw_ops);
|
||||
} else {
|
||||
open_request.result = error.NotFound;
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
.close => |*close_request| {
|
||||
// TODO: Use this result somehow.
|
||||
_ = ext.SDL_RWclose(@ptrCast(*ext.SDL_RWops, @alignCast(
|
||||
@alignOf(ext.SDL_RWops), close_request.file_access)));
|
||||
},
|
||||
|
||||
.read_file => |read_request| {
|
||||
// TODO: Implement.
|
||||
_ = read_request;
|
||||
},
|
||||
|
||||
.seek_file => |seek_request| {
|
||||
// TODO: Implement.
|
||||
_ = seek_request;
|
||||
},
|
||||
|
||||
.tell_file => |tell_request| {
|
||||
// TODO: Implement.
|
||||
_ = tell_request;
|
||||
},
|
||||
}
|
||||
|
||||
resume messages.frame;
|
||||
|
||||
implementation.file_system_messages = messages.next;
|
||||
}
|
||||
|
||||
// TODO: Error check this.
|
||||
_ = ext.SDL_SemWait(implementation.file_system_semaphore);
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
/// Attempts to read the contents of the file referenced by `file_access` at the current file
|
||||
/// cursor position into `buffer`.
|
||||
///
|
||||
/// The number of bytes that could be read / fitted into `buffer` is returned or a [FileError]
|
||||
/// if the file failed to be read.
|
||||
///
|
||||
pub fn readFile(event_loop: *EventLoop, file_access: *FileAccess,
|
||||
buffer: []const u8) FileError!usize {
|
||||
|
||||
var file_system_message = FileSystemMessage{
|
||||
.frame = @frame(),
|
||||
|
||||
.request = .{.read_file = .{
|
||||
.file_access = file_access,
|
||||
.buffer = buffer,
|
||||
}},
|
||||
};
|
||||
|
||||
suspend event_loop.enqueueFileSystemMessage(&file_system_message);
|
||||
|
||||
return file_system_message.request.read_file.result;
|
||||
}
|
||||
|
||||
///
|
||||
/// Attempts to tell the current file cursor position for the file referenced by `file_access`.
|
||||
///
|
||||
/// Returns the number of bytes into the file that the cursor is relative to its beginning or a
|
||||
/// [FileError] if the file failed to be queried.
|
||||
///
|
||||
pub fn queryFile(event_loop: *EventLoop, file_access: *FileAccess) FileError!usize {
|
||||
var file_system_message = FileSystemMessage{
|
||||
.frame = @frame(),
|
||||
.request = .{.tell_file = .{.file_access = file_access}},
|
||||
};
|
||||
|
||||
suspend event_loop.enqueueFileSystemMessage(&file_system_message);
|
||||
|
||||
return file_system_message.request.tell_file.result;
|
||||
}
|
||||
|
||||
///
|
||||
/// Attempts to seek the file cursor through the file referenced by `file_access` from `origin`
|
||||
/// to `offset` via `event_loop`, returning a [FileError] if the file failed to be sought.
|
||||
///
|
||||
pub fn seekFile(event_loop: *EventLoop, file_access: *FileAccess,
|
||||
origin: SeekOrigin, offset: usize) FileError!void {
|
||||
|
||||
var file_system_message = FileSystemMessage{
|
||||
.frame = @frame(),
|
||||
|
||||
.request = .{
|
||||
.seek_file = .{
|
||||
.file_access = file_access,
|
||||
.origin = origin,
|
||||
.offset = offset,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
suspend event_loop.enqueueFileSystemMessage(&file_system_message);
|
||||
|
||||
return file_system_message.request.seek_file.result;
|
||||
}
|
||||
};
|
||||
|
||||
///
|
||||
/// File-system agnostic abstraction for manipulating a file.
|
||||
///
|
||||
pub const FileAccess = opaque {
|
||||
///
|
||||
/// Scans the number of bytes in the file referenced by `file_access` via `event_loop`, returing
|
||||
/// its byte size or a [FileError] if it failed.
|
||||
///
|
||||
pub fn size(file_access: *FileAccess, event_loop: *EventLoop) FileError!usize {
|
||||
// Save cursor to return to it later.
|
||||
const origin_cursor = try event_loop.queryFile(file_access);
|
||||
|
||||
try event_loop.seekFile(file_access, .tail, 0);
|
||||
|
||||
const ending_cursor = try event_loop.queryFile(file_access);
|
||||
|
||||
// Return to original cursor.
|
||||
try event_loop.seekFile(file_access, .head, origin_cursor);
|
||||
|
||||
return ending_cursor;
|
||||
}
|
||||
};
|
||||
|
||||
///
|
||||
/// 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
|
||||
/// [Error.Inaccessible] errors.
|
||||
///
|
||||
pub const FileError = error {
|
||||
Inaccessible,
|
||||
};
|
||||
|
||||
///
|
||||
/// Platform-agnostic mechanism for working with an abstraction of the underlying file-system(s)
|
||||
/// available to the application in a sandboxed environment.
|
||||
///
|
||||
pub const FileSystem = enum {
|
||||
data,
|
||||
user,
|
||||
|
||||
///
|
||||
/// Platform-agnostic mechanism for referencing files and directories on a [FileSystem].
|
||||
///
|
||||
pub const Path = struct {
|
||||
file_system: FileSystem,
|
||||
length: u16,
|
||||
buffer: [max]u8,
|
||||
|
||||
///
|
||||
/// Returns `true` if the length of `path` is empty, otherwise `false`.
|
||||
///
|
||||
pub fn isEmpty(path: Path) bool {
|
||||
return (path.length == 0);
|
||||
}
|
||||
|
||||
///
|
||||
/// Returns `true` if `this` is equal to `that`, otherwise `false`.
|
||||
///
|
||||
pub fn equals(this: Path, that: Path) bool {
|
||||
return std.mem.eql(u8, this.buffer[0 .. this.length], that.buffer[0 .. that.length]);
|
||||
}
|
||||
|
||||
///
|
||||
/// The maximum possible byte-length of a [Path].
|
||||
///
|
||||
/// Note that paths are encoded using UTF-8, meaning that a character may be bigger than one
|
||||
/// byte. Because of this, it is not safe to asume that a path may hold [max] individual
|
||||
/// characters.
|
||||
///
|
||||
pub const max = 1000;
|
||||
|
||||
///
|
||||
///
|
||||
///
|
||||
pub fn write(path: Path, writer: io.Writer) bool {
|
||||
return (writer.write(path.buffer[0 .. path.length]) == path.length);
|
||||
}
|
||||
};
|
||||
|
||||
///
|
||||
/// [PathError.TooLong] occurs when creating a path that is greater than the maximum size **in
|
||||
/// bytes**.
|
||||
///
|
||||
pub const PathError = error {
|
||||
TooLong,
|
||||
};
|
||||
|
||||
///
|
||||
/// Creates and returns a [Path] value in the file system to the location specified by the
|
||||
/// joining of the `sequences` path values.
|
||||
///
|
||||
pub fn joinedPath(file_system: FileSystem, sequences: []const []const u8) PathError!Path {
|
||||
var path = Path{
|
||||
.file_system = file_system,
|
||||
.buffer = std.mem.zeroes([Path.max]u8),
|
||||
.length = 0,
|
||||
};
|
||||
|
||||
for (sequences) |sequence| if (sequence.len != 0) {
|
||||
var components = mem.Spliterator(u8){
|
||||
.source = sequence,
|
||||
.delimiter = "/",
|
||||
};
|
||||
|
||||
while (components.next()) |component| if (component.len != 0) {
|
||||
for (component) |byte| {
|
||||
if (path.length == Path.max) return error.TooLong;
|
||||
|
||||
path.buffer[path.length] = byte;
|
||||
path.length += 1;
|
||||
}
|
||||
|
||||
if (path.length == Path.max) return error.TooLong;
|
||||
|
||||
path.buffer[path.length] = '/';
|
||||
path.length += 1;
|
||||
};
|
||||
};
|
||||
|
||||
return path;
|
||||
}
|
||||
};
|
||||
|
||||
///
|
||||
///
|
||||
///
|
||||
pub const GraphicsContext = opaque {
|
||||
///
|
||||
///
|
||||
///
|
||||
pub const Event = struct {
|
||||
keys_up: Keys = std.mem.zeroes(Keys),
|
||||
keys_down: Keys = std.mem.zeroes(Keys),
|
||||
keys_held: Keys = std.mem.zeroes(Keys),
|
||||
|
||||
const Keys = [256]bool;
|
||||
};
|
||||
|
||||
const Implementation = struct {
|
||||
event: Event,
|
||||
};
|
||||
|
||||
///
|
||||
///
|
||||
///
|
||||
pub fn poll(graphics_context: *GraphicsContext) ?*const Event {
|
||||
_ = graphics_context;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
///
|
||||
///
|
||||
///
|
||||
pub fn present(graphics_context: *GraphicsContext) void {
|
||||
// TODO: Implement;
|
||||
_ = graphics_context;
|
||||
}
|
||||
};
|
||||
|
||||
///
|
||||
///
|
||||
///
|
||||
pub fn GraphicsRunner(comptime Errors: type) type {
|
||||
return fn (*EventLoop, *GraphicsContext) Errors!void;
|
||||
}
|
||||
|
||||
///
|
||||
///
|
||||
///
|
||||
pub fn runGraphics(comptime Errors: anytype, run: GraphicsRunner(Errors)) Errors!void {
|
||||
if (ext.SDL_Init(ext.SDL_INIT_EVERYTHING) != 0) {
|
||||
ext.SDL_LogCritical(ext.SDL_LOG_CATEGORY_APPLICATION, "Failed to initialize runtime");
|
||||
|
||||
return error.InitFailure;
|
||||
}
|
||||
|
||||
defer ext.SDL_Quit();
|
||||
|
||||
const pref_path = create_pref_path: {
|
||||
const path = ext.SDL_GetPrefPath("ona", "ona") orelse {
|
||||
ext.SDL_LogCritical(ext.SDL_LOG_CATEGORY_APPLICATION, "Failed to load user path");
|
||||
|
||||
return error.InitFailure;
|
||||
};
|
||||
|
||||
break: create_pref_path path[0 .. std.mem.len(path)];
|
||||
};
|
||||
|
||||
defer ext.SDL_free(pref_path.ptr);
|
||||
|
||||
const window = create_window: {
|
||||
const pos = ext.SDL_WINDOWPOS_UNDEFINED;
|
||||
var flags = @as(u32, 0);
|
||||
|
||||
break: create_window ext.SDL_CreateWindow("Ona", pos, pos, 640, 480, flags) orelse {
|
||||
ext.SDL_LogCritical(ext.SDL_LOG_CATEGORY_APPLICATION, "Failed to create window");
|
||||
|
||||
return error.InitFailure;
|
||||
};
|
||||
};
|
||||
|
||||
defer ext.SDL_DestroyWindow(window);
|
||||
|
||||
const renderer = create_renderer: {
|
||||
var flags = @as(u32, 0);
|
||||
|
||||
break: create_renderer ext.SDL_CreateRenderer(window, -1, flags) orelse {
|
||||
ext.SDL_LogCritical(ext.SDL_LOG_CATEGORY_APPLICATION, "Failed to create renderer");
|
||||
|
||||
return error.InitFailure;
|
||||
};
|
||||
};
|
||||
|
||||
defer ext.SDL_DestroyRenderer(renderer);
|
||||
|
||||
var event_loop = EventLoop.Implementation{
|
||||
.file_system_semaphore = ext.SDL_CreateSemaphore(0) orelse {
|
||||
ext.SDL_LogCritical(ext.SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Failed to create file-system work scheduler");
|
||||
|
||||
return error.InitFailure;
|
||||
},
|
||||
|
||||
.file_system_mutex = ext.SDL_CreateMutex() orelse {
|
||||
ext.SDL_LogCritical(ext.SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Failed to create file-system work lock");
|
||||
|
||||
return error.InitFailure;
|
||||
},
|
||||
|
||||
.file_system_thread = unreachable,
|
||||
.user_prefix = pref_path,
|
||||
};
|
||||
|
||||
event_loop.file_system_thread = ext.SDL_CreateThread(
|
||||
EventLoop.processFileSystemMessages, "File System Worker", &event_loop) orelse {
|
||||
|
||||
ext.SDL_LogCritical(ext.SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Failed to create file-system work processor");
|
||||
|
||||
return error.InitFailure;
|
||||
};
|
||||
|
||||
defer {
|
||||
ext.SDL_DestroyThread(event_loop.file_system_thread);
|
||||
ext.SDL_DestroySemaphore(event_loop.file_system_mutex);
|
||||
ext.SDL_DestroySemaphore(event_loop.file_system_semaphore);
|
||||
}
|
||||
|
||||
var graphics_context = GraphicsContext.Implementation{
|
||||
.event = .{
|
||||
|
||||
},
|
||||
};
|
||||
|
||||
var message = EventLoop.FileSystemMessage{
|
||||
.frame = @frame(),
|
||||
.request = .exit,
|
||||
};
|
||||
|
||||
@ptrCast(*EventLoop, event_loop).enqueueFileSystemMessage(&message);
|
||||
|
||||
var status = @as(c_int, 0);
|
||||
|
||||
ext.SDL_WaitThread(event_loop.file_system_thread, &status);
|
||||
|
||||
if (status != 0) {
|
||||
// TODO: Error check this.
|
||||
}
|
||||
|
||||
return run(@ptrCast(*EventLoop, &event_loop), @ptrCast(*GraphicsContext, &graphics_context));
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
test {
|
||||
_ = @import("./core/main.zig");
|
||||
_ = @import("./ona/main.zig");
|
||||
}
|
Loading…
Reference in New Issue
Missing documentation comment.