diff --git a/.drone.yml b/.drone.yml index f9941e1..d5426c7 100644 --- a/.drone.yml +++ b/.drone.yml @@ -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 diff --git a/.vscode/launch.json b/.vscode/launch.json index a37980f..fec89bf 100755 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -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", }, ] } diff --git a/.vscode/settings.json b/.vscode/settings.json index 4beb35f..33c8d57 100755 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -14,4 +14,5 @@ "git.detectSubmodulesLimit": 0, "git.ignoreSubmodules": true, + "debug.onTaskErrors": "showErrors", } diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 016ab6a..f2f1dbc 100755 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -1,60 +1,40 @@ { "version": "2.0.0", + "problemMatcher": { + "source": "zig", + "owner": "cpptools", + + "fileLocation": [ + "autoDetect", + "${cwd}", + ], + + "pattern": { + "regexp": "^(.*?):(\\d+):(\\d*):?\\s+(?:fatal\\s+)?(warning|error):\\s+(.*)$", + "file": 1, + "line": 2, + "column": 3, + "severity": 4, + "message": 5, + } + }, + "tasks": [ { - "label": "Build", + "label": "Build Debug", "type": "shell", "command": "zig build", - - "group": { - "kind": "build", - "isDefault": true - }, - + "group": "build", "presentation": { "echo": true, - "reveal": "always", + "reveal": "silent", "focus": true, "panel": "shared", "showReuseMessage": true, "clear": true, "revealProblems": "onProblem", }, - - "problemMatcher": { - "source": "gcc", - "owner": "cpptools", - - "fileLocation": [ - "autoDetect", - "${cwd}", - ], - - "pattern": { - "regexp": "^(.*?):(\\d+):(\\d*):?\\s+(?:fatal\\s+)?(warning|error):\\s+(.*)$", - "file": 1, - "line": 2, - "column": 3, - "severity": 4, - "message": 5, - } - } - }, - { - "label": "Test", - "type": "shell", - "command": "$(find zig-cache -name test) src/main.zig", - "group": { - "kind": "test", - "isDefault": true - }, - }, - { - "label": "Build Test", - "type": "shell", - "command": "zig build test", - "group": "test" }, ], } diff --git a/build.zig b/build.zig index 7eef4a8..6dd3828 100644 --- a/build.zig +++ b/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, + }; +} diff --git a/src/core/io.zig b/src/core/io.zig new file mode 100644 index 0000000..bf794b8 --- /dev/null +++ b/src/core/io.zig @@ -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); +} diff --git a/src/core/main.zig b/src/core/main.zig new file mode 100644 index 0000000..785b8e5 --- /dev/null +++ b/src/core/main.zig @@ -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; +} diff --git a/src/core/math.zig b/src/core/math.zig new file mode 100644 index 0000000..733a9dc --- /dev/null +++ b/src/core/math.zig @@ -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); +} diff --git a/src/core/meta.zig b/src/core/meta.zig new file mode 100644 index 0000000..9fa2c48 --- /dev/null +++ b/src/core/meta.zig @@ -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; +} diff --git a/src/core/stack.zig b/src/core/stack.zig new file mode 100755 index 0000000..7cec654 --- /dev/null +++ b/src/core/stack.zig @@ -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)); +} diff --git a/src/core/table.zig b/src/core/table.zig new file mode 100644 index 0000000..174449a --- /dev/null +++ b/src/core/table.zig @@ -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); +} diff --git a/src/core/testing.zig b/src/core/testing.zig new file mode 100644 index 0000000..c2b88e9 --- /dev/null +++ b/src/core/testing.zig @@ -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; diff --git a/src/core/unicode.zig b/src/core/unicode.zig new file mode 100644 index 0000000..13e9a21 --- /dev/null +++ b/src/core/unicode.zig @@ -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()); +} diff --git a/src/io.zig b/src/io.zig deleted file mode 100644 index 58a3c38..0000000 --- a/src/io.zig +++ /dev/null @@ -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)); -} diff --git a/src/main.zig b/src/main.zig deleted file mode 100644 index 4108ff8..0000000 --- a/src/main.zig +++ /dev/null @@ -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(); - } -} diff --git a/src/mem.zig b/src/mem.zig deleted file mode 100644 index 3df40b9..0000000 --- a/src/mem.zig +++ /dev/null @@ -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); -} diff --git a/src/ona/main.zig b/src/ona/main.zig new file mode 100644 index 0000000..6bf9f79 --- /dev/null +++ b/src/ona/main.zig @@ -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; +} diff --git a/src/ona/oar.zig b/src/ona/oar.zig new file mode 100644 index 0000000..d22b7ae --- /dev/null +++ b/src/ona/oar.zig @@ -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'}; diff --git a/src/ona/sys.zig b/src/ona/sys.zig new file mode 100644 index 0000000..c5b66a0 --- /dev/null +++ b/src/ona/sys.zig @@ -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, +}; + +/// +/// 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; + } + } +} diff --git a/src/stack.zig b/src/stack.zig deleted file mode 100755 index 3f8284a..0000000 --- a/src/stack.zig +++ /dev/null @@ -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); -} diff --git a/src/sys.zig b/src/sys.zig deleted file mode 100644 index b880fef..0000000 --- a/src/sys.zig +++ /dev/null @@ -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)); -} diff --git a/src/tests.zig b/src/tests.zig new file mode 100644 index 0000000..f2feb81 --- /dev/null +++ b/src/tests.zig @@ -0,0 +1,4 @@ +test { + _ = @import("./core/main.zig"); + _ = @import("./ona/main.zig"); +}