Application Context Implementation #4

Closed
kayomn wants to merge 93 commits from event-loop-dev into main
4 changed files with 102 additions and 44 deletions
Showing only changes of commit 14b3921001 - Show all commits

View File

@ -4,11 +4,12 @@ const stack = @import("./stack.zig");
const testing = @import("./testing.zig"); const testing = @import("./testing.zig");
/// ///
/// /// Allocation options for an [Allocator].
/// ///
pub const Allocation = struct { pub const Allocation = struct {
existing: ?[*]u8, existing: ?[*]u8,
size: usize alignment: u29,
size: usize,
}; };
/// ///
@ -17,29 +18,7 @@ pub const Allocation = struct {
pub const Allocator = meta.Function(Allocation, ?[*]u8); pub const Allocator = meta.Function(Allocation, ?[*]u8);
/// ///
/// /// [MakeError.OutOfMemory] if the requested amount of memory could not be allocated.
///
pub const ArenaAllocator = struct {
region: []u8,
cursor: usize = 0,
///
///
///
pub fn allocator(arena_allocator: *ArenaAllocator) Allocator {
return Allocator.fromClosure(arena_allocator, struct {
fn call(context: *ArenaAllocator, allocation: Allocation) ?[*]u8 {
_ = allocation;
_ = context;
return null;
}
}.call);
}
};
///
///
/// ///
pub const MakeError = error { pub const MakeError = error {
OutOfMemory, OutOfMemory,
@ -259,6 +238,21 @@ test "Check memory is equal" {
try testing.expect(!equals(u8, bytes_sequence, &.{69, 42})); 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 data" {
var buffer = [_]u32{0} ** 8;
fill(u32, &buffer, 1);
for (buffer) |element| try testing.expect(element == 1);
}
/// ///
/// Searches for the first instance of an `Element` equal to `needle` in `haystack`, returning its /// Searches for the first instance of an `Element` equal to `needle` in `haystack`, returning its
kayomn marked this conversation as resolved Outdated

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

Worth mentioning that it performs linear time, making it O(n) time complexity?
/// index or `null` if nothing was found. /// index or `null` if nothing was found.
@ -318,7 +312,10 @@ test "Find first of sequence" {
} }
/// ///
/// 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 { pub fn free(allocator: Allocator, allocated_memory: anytype) void {
if (allocator.call(.{ if (allocator.call(.{
@ -332,6 +329,7 @@ pub fn free(allocator: Allocator, allocated_memory: anytype) void {
}), }),
.size = 0, .size = 0,
.alignment = 0,
}) != null) unreachable; }) != null) unreachable;
} }
@ -356,13 +354,21 @@ test "Hashing bytes" {
} }
/// ///
/// /// Attempts to allocate a buffer of `size` `Element`s using `allocator`, returning it or a
/// [MakeError] if it failed.
/// ///
pub fn makeMany(comptime Element: type, allocator: Allocator, size: usize) MakeError![*]Element { pub fn makeMany(comptime Element: type, allocator: Allocator, size: usize) MakeError![*]Element {
return @ptrCast([*]Element, @alignCast(@alignOf(Element), allocator.call(.{ const alignment = @alignOf(Element);
if (allocator.call(.{
.existing = null, .existing = null,
.size = size, .size = @sizeOf(Element) * size,
}) orelse return error.OutOfMemory)); .alignment = alignment,
})) |buffer| {
return @ptrCast([*]Element, @alignCast(alignment, buffer));
}
return error.OutOfMemory;
} }
/// ///
@ -385,7 +391,8 @@ test "Data swapping" {
} }
/// ///
/// [Writer] that silently consumes all given data without failure and throws it away. /// 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 /// This is commonly used for testing or redirected otherwise unwanted output data that has to be
/// sent somewhere for whatever reason. /// sent somewhere for whatever reason.

View File

@ -69,18 +69,16 @@ pub fn Function(comptime In: type, comptime Out: type) type {
else => @compileError("`context` must be a pointer"), else => @compileError("`context` must be a pointer"),
} }
var function = Self{ return Self{
.context = @ptrCast(*anyopaque, context), .context = @ptrCast(*anyopaque, context),
.callErased = struct { .callErased = struct {
fn callErased(erased: *anyopaque, input: In) Out { fn callErased(erased: *anyopaque, input: In) Out {
return if (Context == void) invoke(input) else invoke(@ptrCast( return if (Context == void) invoke(input) else invoke(@ptrCast(
*Context, @alignCast(@alignOf(Context), erased)).*, input); Context, @alignCast(@alignOf(Context), erased)), input);
} }
}.callErased, }.callErased,
}; };
return function;
} }
}; };
} }

View File

@ -39,7 +39,7 @@ pub fn Fixed(comptime Element: type) type {
} }
/// ///
/// Attempts to push `element` into `self`, returning a [FixedPushError] if it failed. /// Attempts to push `element` into `self`, returning a [PushError] if it failed.
/// ///
pub fn push(self: *Self, element: Element) PushError!void { pub fn push(self: *Self, element: Element) PushError!void {
if (self.filled == self.buffer.len) return error.OutOfMemory; if (self.filled == self.buffer.len) return error.OutOfMemory;
@ -49,8 +49,7 @@ pub fn Fixed(comptime Element: type) type {
} }
/// ///
/// Attempts to push all of `elements` into `self`, returning a [FixedPushError] if it /// Attempts to push all of `elements` into `self`, returning a [PushError] if it failed.
/// failed.
/// ///
pub fn pushAll(self: *Self, elements: []const Element) PushError!void { pub fn pushAll(self: *Self, elements: []const Element) PushError!void {
const filled = (self.filled + elements.len); const filled = (self.filled + elements.len);
@ -61,6 +60,20 @@ pub fn Fixed(comptime Element: type) type {
self.filled = filled; self.filled = filled;
} }
///
/// Attempts to push `count` instances of `element` into `self`, returning a [PushError] if
/// it failed.
///
pub fn pushMany(self: *Self, element: Element, count: usize) PushError!void {
const filled = (self.filled + count);
if (filled > self.buffer.len) return error.OutOfMemory;
io.fill(Element, self.buffer[self.filled ..], element);
self.filled = filled;
}
}; };
} }
@ -115,6 +128,45 @@ test "Fixed stack of string literals" {
/// ///
pub const PushError = io.MakeError; pub const PushError = io.MakeError;
///
/// 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.
///
kayomn marked this conversation as resolved Outdated

Seeing as FixedStack does not depend on any kind of dynamic allocation directly, does it make sense to make PushError a direct alias of io.MakeError?

Would it make more sense to use BufferOverflow instead of OutOfMemory?

Seeing as `FixedStack` does not depend on any kind of dynamic allocation directly, does it make sense to make `PushError` a direct alias of `io.MakeError`? Would it make more sense to use `BufferOverflow` instead of `OutOfMemory`?
pub fn fixedAllocator(fixed_stack: *Fixed(u8)) io.Allocator {
return io.Allocator.fromClosure(fixed_stack, struct {
fn alloc(stack: *Fixed(u8), allocation: io.Allocation) ?[*]u8 {
if (allocation.existing) |buffer| if (allocation.size == 0) {
// Deallocate the memory.
const buffer_address = @ptrToInt(buffer);
const stack_buffer_address = @ptrToInt(stack.buffer.ptr);
// Check the buffer is within the address space of the stack buffer. If not, it
// should just be returned to let the caller know it cannot be freed.
if ((buffer_address < stack_buffer_address) or
(buffer_address >= (stack_buffer_address + stack.filled))) return buffer;
// TODO: Investigate ways of freeing if it is the last allocation.
return null;
};
// Reallocate / allocate the memory.
// TODO: Remove stdlib dependency.
const adjusted_offset = @import("std").mem.alignPointerOffset(stack.buffer.ptr +
stack.filled, allocation.alignment) orelse return null;
kayomn marked this conversation as resolved Outdated

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

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

Reallocation could benefit from the same kind of last-alloc check optimization as deallocation. May be worth clarifying that in the comment and code structure.
const head = stack.filled + adjusted_offset;
const tail = head + allocation.size;
stack.pushMany(0, tail) catch return null;
return stack.buffer[head .. tail].ptr;
}
}.alloc);
}
/// ///
/// Returns an [io.Writer] wrapping `fixed_stack`. /// Returns an [io.Writer] wrapping `fixed_stack`.
/// ///

View File

@ -1,4 +1,5 @@
const io = @import("./io.zig"); const io = @import("./io.zig");
const stack = @import("./stack.zig");
const testing = @import("./testing.zig"); const testing = @import("./testing.zig");
/// ///
@ -48,10 +49,10 @@ pub fn Hashed(comptime Key: type, comptime Value: type,
/// Returns a new [Self] value or an [io.MakeError] if initializing failed. /// Returns a new [Self] value or an [io.MakeError] if initializing failed.
/// ///
pub fn init(allocator: Allocator) io.MakeError!Self { pub fn init(allocator: Allocator) io.MakeError!Self {
const initial_capacity = 4; const capacity = 4;
return Self{ return Self{
.buckets = (try io.makeMany(Bucket, allocator, initial_capacity))[0 .. initial_capacity], .buckets = (try io.makeMany(Bucket, allocator, capacity))[0 .. capacity],
.filled = 0, .filled = 0,
.allocator = allocator, .allocator = allocator,
.load_limit = 0.75, .load_limit = 0.75,
@ -197,15 +198,15 @@ pub const string_literal_context = KeyContext([]const u8){
}; };
test "Hash table manipulation with string literal context" { test "Hash table manipulation with string literal context" {
var buffer = [_]u8{0} ** 1024; var buffer = [_]u8{0} ** 4096;
var arena_allocator = io.ArenaAllocator{.region = &buffer}; var fixed_stack = stack.Fixed(u8){.buffer = &buffer};
var table = var table = try Hashed([]const u8, u32, string_literal_context).
try Hashed([]const u8, u32, string_literal_context).init(arena_allocator.allocator()); init(stack.fixedAllocator(&fixed_stack));
defer table.deinit(); defer table.deinit();
const foo = @as(u32, 69); const foo = 69;
try testing.expect(table.remove("foo") == null); try testing.expect(table.remove("foo") == null);
try table.insert("foo", foo); try table.insert("foo", foo);