mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-06-04 10:54:48 +00:00
lib: allocator interface based on Zig allocators
This commit is contained in:
317
src/lib/allocator.zig
Normal file
317
src/lib/allocator.zig
Normal file
@@ -0,0 +1,317 @@
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
const testing = std.testing;
|
||||
|
||||
/// Useful alias since they're required to create Zig allocators
|
||||
pub const ZigVTable = std.mem.Allocator.VTable;
|
||||
|
||||
/// The VTable required by the C interface.
|
||||
pub const VTable = extern struct {
|
||||
alloc: *const fn (*anyopaque, len: usize, alignment: u8, ret_addr: usize) callconv(.c) ?[*]u8,
|
||||
resize: *const fn (*anyopaque, memory: [*]u8, memory_len: usize, alignment: u8, new_len: usize, ret_addr: usize) callconv(.c) bool,
|
||||
remap: *const fn (*anyopaque, memory: [*]u8, memory_len: usize, alignment: u8, new_len: usize, ret_addr: usize) callconv(.c) ?[*]u8,
|
||||
free: *const fn (*anyopaque, memory: [*]u8, memory_len: usize, alignment: u8, ret_addr: usize) callconv(.c) void,
|
||||
};
|
||||
|
||||
/// The Allocator interface for custom memory allocation strategies
|
||||
/// within C libghostty APIs.
|
||||
///
|
||||
/// This -- purposely -- matches the Zig allocator interface. We do this
|
||||
/// for two reasons: (1) Zig's allocator interface is well proven in
|
||||
/// the real world to be flexible and useful, and (2) it allows us to
|
||||
/// easily convert C allocators to Zig allocators and vice versa, since
|
||||
/// we're written in Zig.
|
||||
pub const Allocator = extern struct {
|
||||
ctx: *anyopaque,
|
||||
vtable: *const VTable,
|
||||
|
||||
/// vtable for the Zig allocator interface to map our extern
|
||||
/// allocator to Zig's allocator interface.
|
||||
pub const zig_vtable: ZigVTable = .{
|
||||
.alloc = alloc,
|
||||
.resize = resize,
|
||||
.remap = remap,
|
||||
.free = free,
|
||||
};
|
||||
|
||||
/// Create a C allocator from a Zig allocator. This requires that
|
||||
/// the Zig allocator be pointer-stable for the lifetime of the
|
||||
/// C allocator.
|
||||
pub fn fromZig(zig_alloc: *const std.mem.Allocator) Allocator {
|
||||
return .{
|
||||
.ctx = @ptrCast(@constCast(zig_alloc)),
|
||||
.vtable = &ZigAllocator.vtable,
|
||||
};
|
||||
}
|
||||
|
||||
/// Create a Zig allocator from this C allocator. This requires
|
||||
/// a pointer to a Zig allocator vtable that we can populate with
|
||||
/// our callbacks.
|
||||
pub fn zig(self: *const Allocator) std.mem.Allocator {
|
||||
return .{
|
||||
.ptr = @ptrCast(@constCast(self)),
|
||||
.vtable = &zig_vtable,
|
||||
};
|
||||
}
|
||||
|
||||
fn alloc(
|
||||
ctx: *anyopaque,
|
||||
len: usize,
|
||||
alignment: std.mem.Alignment,
|
||||
ra: usize,
|
||||
) ?[*]u8 {
|
||||
const self: *Allocator = @ptrCast(@alignCast(ctx));
|
||||
return self.vtable.alloc(
|
||||
self.ctx,
|
||||
len,
|
||||
@intFromEnum(alignment),
|
||||
ra,
|
||||
);
|
||||
}
|
||||
|
||||
fn resize(
|
||||
ctx: *anyopaque,
|
||||
old_mem: []u8,
|
||||
alignment: std.mem.Alignment,
|
||||
new_len: usize,
|
||||
ra: usize,
|
||||
) bool {
|
||||
const self: *Allocator = @ptrCast(@alignCast(ctx));
|
||||
return self.vtable.resize(
|
||||
self.ctx,
|
||||
old_mem.ptr,
|
||||
old_mem.len,
|
||||
@intFromEnum(alignment),
|
||||
new_len,
|
||||
ra,
|
||||
);
|
||||
}
|
||||
|
||||
fn remap(
|
||||
ctx: *anyopaque,
|
||||
old_mem: []u8,
|
||||
alignment: std.mem.Alignment,
|
||||
new_len: usize,
|
||||
ra: usize,
|
||||
) ?[*]u8 {
|
||||
const self: *Allocator = @ptrCast(@alignCast(ctx));
|
||||
return self.vtable.remap(
|
||||
self.ctx,
|
||||
old_mem.ptr,
|
||||
old_mem.len,
|
||||
@intFromEnum(alignment),
|
||||
new_len,
|
||||
ra,
|
||||
);
|
||||
}
|
||||
|
||||
fn free(
|
||||
ctx: *anyopaque,
|
||||
old_mem: []u8,
|
||||
alignment: std.mem.Alignment,
|
||||
ra: usize,
|
||||
) void {
|
||||
const self: *Allocator = @ptrCast(@alignCast(ctx));
|
||||
self.vtable.free(
|
||||
self.ctx,
|
||||
old_mem.ptr,
|
||||
old_mem.len,
|
||||
@intFromEnum(alignment),
|
||||
ra,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/// An allocator implementation that wraps a Zig allocator so that
|
||||
/// it can be exposed to C.
|
||||
const ZigAllocator = struct {
|
||||
const vtable: VTable = .{
|
||||
.alloc = alloc,
|
||||
.resize = resize,
|
||||
.remap = remap,
|
||||
.free = free,
|
||||
};
|
||||
|
||||
fn alloc(
|
||||
ctx: *anyopaque,
|
||||
len: usize,
|
||||
alignment: u8,
|
||||
ra: usize,
|
||||
) callconv(.c) ?[*]u8 {
|
||||
const zig_alloc: *const std.mem.Allocator = @ptrCast(@alignCast(ctx));
|
||||
return zig_alloc.vtable.alloc(
|
||||
zig_alloc.ptr,
|
||||
len,
|
||||
@enumFromInt(alignment),
|
||||
ra,
|
||||
);
|
||||
}
|
||||
|
||||
fn resize(
|
||||
ctx: *anyopaque,
|
||||
memory: [*]u8,
|
||||
memory_len: usize,
|
||||
alignment: u8,
|
||||
new_len: usize,
|
||||
ra: usize,
|
||||
) callconv(.c) bool {
|
||||
const zig_alloc: *const std.mem.Allocator = @ptrCast(@alignCast(ctx));
|
||||
return zig_alloc.vtable.resize(
|
||||
zig_alloc.ptr,
|
||||
memory[0..memory_len],
|
||||
@enumFromInt(alignment),
|
||||
new_len,
|
||||
ra,
|
||||
);
|
||||
}
|
||||
|
||||
fn remap(
|
||||
ctx: *anyopaque,
|
||||
memory: [*]u8,
|
||||
memory_len: usize,
|
||||
alignment: u8,
|
||||
new_len: usize,
|
||||
ra: usize,
|
||||
) callconv(.c) ?[*]u8 {
|
||||
const zig_alloc: *const std.mem.Allocator = @ptrCast(@alignCast(ctx));
|
||||
return zig_alloc.vtable.remap(
|
||||
zig_alloc.ptr,
|
||||
memory[0..memory_len],
|
||||
@enumFromInt(alignment),
|
||||
new_len,
|
||||
ra,
|
||||
);
|
||||
}
|
||||
|
||||
fn free(
|
||||
ctx: *anyopaque,
|
||||
memory: [*]u8,
|
||||
memory_len: usize,
|
||||
alignment: u8,
|
||||
ra: usize,
|
||||
) callconv(.c) void {
|
||||
const zig_alloc: *const std.mem.Allocator = @ptrCast(@alignCast(ctx));
|
||||
return zig_alloc.vtable.free(
|
||||
zig_alloc.ptr,
|
||||
memory[0..memory_len],
|
||||
@enumFromInt(alignment),
|
||||
ra,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/// C allocator (libc)
|
||||
pub const CAllocator = struct {
|
||||
comptime {
|
||||
if (!builtin.link_libc) {
|
||||
@compileError("C allocator is only available when linking against libc");
|
||||
}
|
||||
}
|
||||
|
||||
const vtable: VTable = .{
|
||||
.alloc = alloc,
|
||||
.resize = resize,
|
||||
.remap = remap,
|
||||
.free = free,
|
||||
};
|
||||
|
||||
fn alloc(
|
||||
ctx: *anyopaque,
|
||||
len: usize,
|
||||
alignment: u8,
|
||||
ra: usize,
|
||||
) callconv(.c) ?[*]u8 {
|
||||
return std.heap.c_allocator.vtable.alloc(
|
||||
ctx,
|
||||
len,
|
||||
@enumFromInt(alignment),
|
||||
ra,
|
||||
);
|
||||
}
|
||||
|
||||
fn resize(
|
||||
ctx: *anyopaque,
|
||||
memory: [*]u8,
|
||||
memory_len: usize,
|
||||
alignment: u8,
|
||||
new_len: usize,
|
||||
ra: usize,
|
||||
) callconv(.c) bool {
|
||||
return std.heap.c_allocator.vtable.resize(
|
||||
ctx,
|
||||
memory[0..memory_len],
|
||||
@enumFromInt(alignment),
|
||||
new_len,
|
||||
ra,
|
||||
);
|
||||
}
|
||||
|
||||
fn remap(
|
||||
ctx: *anyopaque,
|
||||
memory: [*]u8,
|
||||
memory_len: usize,
|
||||
alignment: u8,
|
||||
new_len: usize,
|
||||
ra: usize,
|
||||
) callconv(.c) ?[*]u8 {
|
||||
return std.heap.c_allocator.vtable.remap(
|
||||
ctx,
|
||||
memory[0..memory_len],
|
||||
@enumFromInt(alignment),
|
||||
new_len,
|
||||
ra,
|
||||
);
|
||||
}
|
||||
|
||||
fn free(
|
||||
ctx: *anyopaque,
|
||||
memory: [*]u8,
|
||||
memory_len: usize,
|
||||
alignment: u8,
|
||||
ra: usize,
|
||||
) callconv(.c) void {
|
||||
std.heap.c_allocator.vtable.free(
|
||||
ctx,
|
||||
memory[0..memory_len],
|
||||
@enumFromInt(alignment),
|
||||
ra,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
pub const c_allocator: Allocator = .{
|
||||
.ctx = undefined,
|
||||
.vtable = &CAllocator.vtable,
|
||||
};
|
||||
|
||||
/// Allocator that can be sent to the C API that does full
|
||||
/// leak checking within Zig tests. This should only be used from
|
||||
/// Zig tests.
|
||||
pub const test_allocator: Allocator = b: {
|
||||
if (!builtin.is_test) @compileError("test_allocator can only be used in tests");
|
||||
break :b .fromZig(&testing.allocator);
|
||||
};
|
||||
|
||||
test "c allocator" {
|
||||
if (!comptime builtin.link_libc) return error.SkipZigTest;
|
||||
|
||||
const alloc = c_allocator.zig();
|
||||
const str = try alloc.alloc(u8, 10);
|
||||
defer alloc.free(str);
|
||||
try testing.expectEqual(10, str.len);
|
||||
}
|
||||
|
||||
test "fba allocator" {
|
||||
var buf: [1024]u8 = undefined;
|
||||
var fba: std.heap.FixedBufferAllocator = .init(&buf);
|
||||
const zig_alloc = fba.allocator();
|
||||
|
||||
// Convert the Zig allocator to a C interface
|
||||
const c_alloc: Allocator = .fromZig(&zig_alloc);
|
||||
|
||||
// Convert back to Zig so we can test it.
|
||||
const alloc = c_alloc.zig();
|
||||
const str = try alloc.alloc(u8, 10);
|
||||
defer alloc.free(str);
|
||||
try testing.expectEqual(10, str.len);
|
||||
}
|
||||
@@ -68,9 +68,16 @@ pub const Attribute = terminal.Attribute;
|
||||
comptime {
|
||||
// If we're building the C library (vs. the Zig module) then
|
||||
// we want to reference the C API so that it gets exported.
|
||||
if (terminal.is_c_lib) _ = terminal.c_api;
|
||||
if (terminal.is_c_lib) {
|
||||
const c = terminal.c_api;
|
||||
@export(&c.ghostty_vt_osc_new, .{ .name = "ghostty_vt_osc_new" });
|
||||
@export(&c.ghostty_vt_osc_free, .{ .name = "ghostty_vt_osc_free" });
|
||||
}
|
||||
}
|
||||
|
||||
test {
|
||||
_ = terminal;
|
||||
|
||||
// Tests always test the C API
|
||||
_ = terminal.c_api;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,44 @@
|
||||
pub export fn ghostty_hi() void {
|
||||
// Does nothing, but you can see this symbol exists:
|
||||
// nm -D --defined-only zig-out/lib/libghostty-vt.so | rg ' T '
|
||||
// This is temporary as we figure out the API.
|
||||
const std = @import("std");
|
||||
const lib_alloc = @import("../lib/allocator.zig");
|
||||
const CAllocator = lib_alloc.Allocator;
|
||||
const osc = @import("osc.zig");
|
||||
|
||||
pub const GhosttyOscParser = extern struct {
|
||||
parser: *osc.Parser,
|
||||
};
|
||||
|
||||
pub const Result = enum(c_int) {
|
||||
success = 0,
|
||||
out_of_memory = -1,
|
||||
};
|
||||
|
||||
pub fn ghostty_vt_osc_new(
|
||||
c_alloc: *const CAllocator,
|
||||
result: *GhosttyOscParser,
|
||||
) callconv(.c) Result {
|
||||
const alloc = c_alloc.zig();
|
||||
const ptr = alloc.create(osc.Parser) catch return .out_of_memory;
|
||||
ptr.* = .initAlloc(alloc);
|
||||
result.* = .{ .parser = ptr };
|
||||
return .success;
|
||||
}
|
||||
|
||||
pub fn ghostty_vt_osc_free(parser: GhosttyOscParser) callconv(.c) void {
|
||||
const alloc = parser.parser.alloc.?;
|
||||
parser.parser.deinit();
|
||||
alloc.destroy(parser.parser);
|
||||
}
|
||||
|
||||
test {
|
||||
_ = lib_alloc;
|
||||
}
|
||||
|
||||
test "osc" {
|
||||
const testing = std.testing;
|
||||
var p: GhosttyOscParser = undefined;
|
||||
try testing.expectEqual(Result.success, ghostty_vt_osc_new(
|
||||
&lib_alloc.test_allocator,
|
||||
&p,
|
||||
));
|
||||
ghostty_vt_osc_free(p);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user