From c5786c5d38f1f7edd986ed4a4d5339e450511df3 Mon Sep 17 00:00:00 2001 From: CoderJoshDK <74162303+CoderJoshDK@users.noreply.github.com> Date: Wed, 24 Sep 2025 08:28:32 -0400 Subject: [PATCH 1/4] fix: alloc free off by one --- include/ghostty.h | 1 + src/config/CApi.zig | 3 ++- src/main_c.zig | 7 +++++-- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/include/ghostty.h b/include/ghostty.h index 7888b380c..a2964c227 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -353,6 +353,7 @@ typedef struct { typedef struct { const char* ptr; uintptr_t len; + uintptr_t cap; } ghostty_string_s; typedef struct { diff --git a/src/config/CApi.zig b/src/config/CApi.zig index bdc59797a..f90b0ca24 100644 --- a/src/config/CApi.zig +++ b/src/config/CApi.zig @@ -130,7 +130,8 @@ export fn ghostty_config_open_path() c.String { return .empty; }; - return .fromSlice(path); + // Capacity is len + 1 due to sentinel + return .fromSlice(path, path.len + 1); } /// Sync with ghostty_diagnostic_s diff --git a/src/main_c.zig b/src/main_c.zig index 9a9bcc6d2..1212e0b07 100644 --- a/src/main_c.zig +++ b/src/main_c.zig @@ -63,16 +63,19 @@ const Info = extern struct { pub const String = extern struct { ptr: ?[*]const u8, len: usize, + cap: usize, pub const empty: String = .{ .ptr = null, .len = 0, + .cap = 0, }; - pub fn fromSlice(slice: []const u8) String { + pub fn fromSlice(slice: []const u8, cap: usize) String { return .{ .ptr = slice.ptr, .len = slice.len, + .cap = cap, }; } }; @@ -129,5 +132,5 @@ pub export fn ghostty_translate(msgid: [*:0]const u8) [*:0]const u8 { /// Free a string allocated by Ghostty. pub export fn ghostty_string_free(str: String) void { - state.alloc.free(str.ptr.?[0..str.len]); + state.alloc.free(str.ptr.?[0..str.cap]); } From 79685f87c420ca7a9d73fee5865bb5046b7df74b Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Wed, 24 Sep 2025 16:33:18 -0500 Subject: [PATCH 2/4] use comptime to make C String interface nicer --- src/config/CApi.zig | 2 +- src/main_c.zig | 31 ++++++++++++++++++++++++++----- 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/src/config/CApi.zig b/src/config/CApi.zig index f90b0ca24..154cc0c9c 100644 --- a/src/config/CApi.zig +++ b/src/config/CApi.zig @@ -131,7 +131,7 @@ export fn ghostty_config_open_path() c.String { }; // Capacity is len + 1 due to sentinel - return .fromSlice(path, path.len + 1); + return .fromSlice(path); } /// Sync with ghostty_diagnostic_s diff --git a/src/main_c.zig b/src/main_c.zig index 1212e0b07..a72d82a3e 100644 --- a/src/main_c.zig +++ b/src/main_c.zig @@ -63,21 +63,42 @@ const Info = extern struct { pub const String = extern struct { ptr: ?[*]const u8, len: usize, - cap: usize, + sentinel: bool, pub const empty: String = .{ .ptr = null, .len = 0, - .cap = 0, + .sentinel = false, }; - pub fn fromSlice(slice: []const u8, cap: usize) String { + pub fn fromSlice(slice: anytype) String { return .{ .ptr = slice.ptr, .len = slice.len, - .cap = cap, + .sentinel = sentinel: { + const info = @typeInfo(@TypeOf(slice)); + switch (info) { + .pointer => |p| { + if (p.size != .slice) @compileError("only slices supported"); + if (p.child != u8) @compileError("only u8 slices supported"); + const sentinel_ = p.sentinel(); + if (sentinel_) |sentinel| if (sentinel != 0) @compileError("only 0 is supported for sentinels"); + break :sentinel sentinel_ != null; + }, + else => @compileError("only []const u8 and [:0]const u8"), + } + }, }; } + + pub fn deinit(self: *const String) void { + const ptr = self.ptr orelse return; + if (self.sentinel) { + state.alloc.free(ptr[0..self.len :0]); + } else { + state.alloc.free(ptr[0..self.len]); + } + } }; /// Initialize ghostty global state. @@ -132,5 +153,5 @@ pub export fn ghostty_translate(msgid: [*:0]const u8) [*:0]const u8 { /// Free a string allocated by Ghostty. pub export fn ghostty_string_free(str: String) void { - state.alloc.free(str.ptr.?[0..str.cap]); + str.deinit(); } From dc03a47558572dc66ae03350739cda70e1ea4cc5 Mon Sep 17 00:00:00 2001 From: CoderJoshDK <74162303+CoderJoshDK@users.noreply.github.com> Date: Wed, 24 Sep 2025 17:47:24 -0400 Subject: [PATCH 3/4] chore: sync changes with ghostty_string_s --- include/ghostty.h | 2 +- src/config/CApi.zig | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/include/ghostty.h b/include/ghostty.h index a2964c227..3f1e0c9d9 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -353,7 +353,7 @@ typedef struct { typedef struct { const char* ptr; uintptr_t len; - uintptr_t cap; + bool sentinel; } ghostty_string_s; typedef struct { diff --git a/src/config/CApi.zig b/src/config/CApi.zig index 154cc0c9c..bdc59797a 100644 --- a/src/config/CApi.zig +++ b/src/config/CApi.zig @@ -130,7 +130,6 @@ export fn ghostty_config_open_path() c.String { return .empty; }; - // Capacity is len + 1 due to sentinel return .fromSlice(path); } From d79441edd1ec28132f10e867040cd5f646196238 Mon Sep 17 00:00:00 2001 From: CoderJoshDK <74162303+CoderJoshDK@users.noreply.github.com> Date: Wed, 24 Sep 2025 21:53:02 -0400 Subject: [PATCH 4/4] test: valid string slices for ghostty_string_s --- src/main_c.zig | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/src/main_c.zig b/src/main_c.zig index a72d82a3e..d3fb753ef 100644 --- a/src/main_c.zig +++ b/src/main_c.zig @@ -155,3 +155,43 @@ pub export fn ghostty_translate(msgid: [*:0]const u8) [*:0]const u8 { pub export fn ghostty_string_free(str: String) void { str.deinit(); } + +test "ghostty_string_s empty string" { + const testing = std.testing; + const empty_string = String.empty; + defer empty_string.deinit(); + + try testing.expect(empty_string.len == 0); + try testing.expect(empty_string.sentinel == false); +} + +test "ghostty_string_s c string" { + const testing = std.testing; + state.alloc = testing.allocator; + + const slice: [:0]const u8 = "hello"; + const allocated_slice = try testing.allocator.dupeZ(u8, slice); + const c_null_string = String.fromSlice(allocated_slice); + defer c_null_string.deinit(); + + try testing.expect(allocated_slice[5] == 0); + try testing.expect(@TypeOf(slice) == [:0]const u8); + try testing.expect(@TypeOf(allocated_slice) == [:0]u8); + try testing.expect(c_null_string.len == 5); + try testing.expect(c_null_string.sentinel == true); +} + +test "ghostty_string_s zig string" { + const testing = std.testing; + state.alloc = testing.allocator; + + const slice: []const u8 = "hello"; + const allocated_slice = try testing.allocator.dupe(u8, slice); + const zig_string = String.fromSlice(allocated_slice); + defer zig_string.deinit(); + + try testing.expect(@TypeOf(slice) == []const u8); + try testing.expect(@TypeOf(allocated_slice) == []u8); + try testing.expect(zig_string.len == 5); + try testing.expect(zig_string.sentinel == false); +}