lib: allocator interface based on Zig allocators
parent
32bf37e5e4
commit
969fcfaec3
|
|
@ -7,6 +7,127 @@
|
|||
extern "C" {
|
||||
#endif
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
// Types
|
||||
|
||||
typedef struct GhosttyOscParser GhosttyOscParser;
|
||||
|
||||
typedef enum {
|
||||
GHOSTTY_VT_SUCCESS = 0,
|
||||
GHOSTTY_VT_OUT_OF_MEMORY = -1,
|
||||
} GhosttyVtResult;
|
||||
|
||||
typedef struct {
|
||||
/**
|
||||
* Return a pointer to `len` bytes with specified `alignment`, or return
|
||||
* `NULL` indicating the allocation failed.
|
||||
*
|
||||
* @param ctx The allocator context
|
||||
* @param len Number of bytes to allocate
|
||||
* @param alignment Required alignment for the allocation. Guaranteed to
|
||||
* be a power of two between 1 and 16 inclusive.
|
||||
* @param ret_addr First return address of the allocation call stack (0 if not provided)
|
||||
* @return Pointer to allocated memory, or NULL if allocation failed
|
||||
*/
|
||||
void* (*alloc)(void *ctx, size_t len, uint8_t alignment, uintptr_t ret_addr);
|
||||
|
||||
/**
|
||||
* Attempt to expand or shrink memory in place.
|
||||
*
|
||||
* `memory_len` must equal the length requested from the most recent
|
||||
* successful call to `alloc`, `resize`, or `remap`. `alignment` must
|
||||
* equal the same value that was passed as the `alignment` parameter to
|
||||
* the original `alloc` call.
|
||||
*
|
||||
* `new_len` must be greater than zero.
|
||||
*
|
||||
* @param ctx The allocator context
|
||||
* @param memory Pointer to the memory block to resize
|
||||
* @param memory_len Current size of the memory block
|
||||
* @param alignment Alignment (must match original allocation)
|
||||
* @param new_len New requested size
|
||||
* @param ret_addr First return address of the allocation call stack (0 if not provided)
|
||||
* @return true if resize was successful in-place, false if relocation would be required
|
||||
*/
|
||||
bool (*resize)(void *ctx, void *memory, size_t memory_len, uint8_t alignment, size_t new_len, uintptr_t ret_addr);
|
||||
|
||||
/**
|
||||
* Attempt to expand or shrink memory, allowing relocation.
|
||||
*
|
||||
* `memory_len` must equal the length requested from the most recent
|
||||
* successful call to `alloc`, `resize`, or `remap`. `alignment` must
|
||||
* equal the same value that was passed as the `alignment` parameter to
|
||||
* the original `alloc` call.
|
||||
*
|
||||
* A non-`NULL` return value indicates the resize was successful. The
|
||||
* allocation may have same address, or may have been relocated. In either
|
||||
* case, the allocation now has size of `new_len`. A `NULL` return value
|
||||
* indicates that the resize would be equivalent to allocating new memory,
|
||||
* copying the bytes from the old memory, and then freeing the old memory.
|
||||
* In such case, it is more efficient for the caller to perform the copy.
|
||||
*
|
||||
* `new_len` must be greater than zero.
|
||||
*
|
||||
* @param ctx The allocator context
|
||||
* @param memory Pointer to the memory block to remap
|
||||
* @param memory_len Current size of the memory block
|
||||
* @param alignment Alignment (must match original allocation)
|
||||
* @param new_len New requested size
|
||||
* @param ret_addr First return address of the allocation call stack (0 if not provided)
|
||||
* @return Pointer to resized memory (may be relocated), or NULL if manual copy is needed
|
||||
*/
|
||||
void* (*remap)(void *ctx, void *memory, size_t memory_len, uint8_t alignment, size_t new_len, uintptr_t ret_addr);
|
||||
|
||||
/**
|
||||
* Free and invalidate a region of memory.
|
||||
*
|
||||
* `memory_len` must equal the length requested from the most recent
|
||||
* successful call to `alloc`, `resize`, or `remap`. `alignment` must
|
||||
* equal the same value that was passed as the `alignment` parameter to
|
||||
* the original `alloc` call.
|
||||
*
|
||||
* @param ctx The allocator context
|
||||
* @param memory Pointer to the memory block to free
|
||||
* @param memory_len Size of the memory block
|
||||
* @param alignment Alignment (must match original allocation)
|
||||
* @param ret_addr First return address of the allocation call stack (0 if not provided)
|
||||
*/
|
||||
void (*free)(void *ctx, void *memory, size_t memory_len, uint8_t alignment, uintptr_t ret_addr);
|
||||
} GhosttyVtAllocatorVtable;
|
||||
|
||||
/**
|
||||
* Custom memory allocator.
|
||||
*
|
||||
* Usage example:
|
||||
* @code
|
||||
* GhosttyVtAllocator allocator = {
|
||||
* .vtable = &my_allocator_vtable,
|
||||
* .ctx = my_allocator_state
|
||||
* };
|
||||
* @endcode
|
||||
*/
|
||||
typedef struct {
|
||||
/**
|
||||
* Opaque context pointer passed to all vtable functions.
|
||||
* This allows the allocator implementation to maintain state
|
||||
* or reference external resources needed for memory management.
|
||||
*/
|
||||
void *ctx;
|
||||
|
||||
/**
|
||||
* Pointer to the allocator's vtable containing function pointers
|
||||
* for memory operations (alloc, resize, remap, free).
|
||||
*/
|
||||
const GhosttyVtAllocatorVtable *vtable;
|
||||
} GhosttyVtAllocator;
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
// Functions
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue