diff --git a/include/ghostty-vt.h b/include/ghostty-vt.h index 591b095a2..b61069058 100644 --- a/include/ghostty-vt.h +++ b/include/ghostty-vt.h @@ -7,6 +7,127 @@ extern "C" { #endif +#include +#include +#include + +//------------------------------------------------------------------- +// 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 diff --git a/src/lib/allocator.zig b/src/lib/allocator.zig new file mode 100644 index 000000000..ef296f23d --- /dev/null +++ b/src/lib/allocator.zig @@ -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); +} diff --git a/src/lib_vt.zig b/src/lib_vt.zig index 98242ce78..19849805c 100644 --- a/src/lib_vt.zig +++ b/src/lib_vt.zig @@ -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; } diff --git a/src/terminal/c_api.zig b/src/terminal/c_api.zig index 287711fec..079a16fb3 100644 --- a/src/terminal/c_api.zig +++ b/src/terminal/c_api.zig @@ -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); }