mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-04-19 14:00:29 +00:00
libghostty: add ghostty_free for cross-runtime memory safety (#11785)
## What
On Windows, calling `free()` on memory allocated by libghostty crashes
because Zig and MSVC use separate heaps.
This adds `ghostty_free()` so consumers can free library-allocated
memory safely on all platforms.
## Why
When Zig builds a DLL on Windows with `link_libc = true`, it does not
link the Windows C runtime (`ucrtbase.dll`). Instead it uses its own
libc built on top of `KERNEL32.dll`. So `builtin.link_libc` is true and
`c_allocator` is selected, but Zig's `malloc` and MSVC's `malloc` are
different implementations with different heaps. 💥
On Linux/macOS this is not a problem because Zig links the system libc
and everyone shares the same heap. On Windows, `free(buf)` from MSVC
tries to free memory from Zig's heap and you get a debug assertion
failure or undefined behavior.
The `format_alloc` docs said "the buffer can be freed with `free()`" but
that is only true when the library and consumer share the same C
runtime, which is not the case on Windows.
## How
- Add `ghostty_free(allocator, ptr, len)` that frees through the same
allocator that did the allocation
- Update `format_alloc` docs to point to `ghostty_free()` instead of
`free()`
- Update all 3 examples to use `ghostty_free(NULL, buf, len)`
The signature takes an allocator because raw buffers (unlike objects
like terminals or formatters) do not store their allocator internally.
The caller already has all three values: the allocator they passed, the
pointer, and the length they got back.
I went back and forth on the naming. Other options I considered:
`ghostty_alloc_free(allocator, ptr, len)` or returning a `GhosttyBuffer`
wrapper with its own `_free`. Happy to change the naming if there is a
preference.
No impact on Linux/macOS. `ghostty_free()` works correctly there too, it
just happens to call the same `free()` the consumer would have called
anyway.
## Verified
- `zig build test-lib-vt` passes on Windows, macOS arm64, Linux x86_64
(exit 0)
- `zig build test` passes on Windows (2575/2619 passed, 1 pre-existing
font sprite failure) and macOS (exit 0)
- cmake shared example builds, links, and runs correctly on Windows with
`ghostty_free()` (no more heap crash)
## What I Learnt
- What I wrote in Why
- Zig allocators require the length to free (no hidden metadata headers
like C's malloc). This is a deliberate design choice for explicit
control.
- The standard pattern for C libraries on Windows is "whoever allocates,
frees" (like `curl_free()`, `SDL_free()`). This avoids cross-runtime
heap issues entirely.
This commit is contained in:
@@ -45,7 +45,7 @@ int main() {
|
||||
fwrite(buf, 1, len, stdout);
|
||||
printf("\n");
|
||||
|
||||
free(buf);
|
||||
ghostty_free(NULL, buf, len);
|
||||
ghostty_formatter_free(formatter);
|
||||
ghostty_terminal_free(terminal);
|
||||
return 0;
|
||||
|
||||
@@ -45,7 +45,7 @@ int main() {
|
||||
fwrite(buf, 1, len, stdout);
|
||||
printf("\n");
|
||||
|
||||
free(buf);
|
||||
ghostty_free(NULL, buf, len);
|
||||
ghostty_formatter_free(formatter);
|
||||
ghostty_terminal_free(terminal);
|
||||
return 0;
|
||||
|
||||
@@ -56,7 +56,7 @@ int main() {
|
||||
printf("\n");
|
||||
|
||||
// Clean up
|
||||
free(buf);
|
||||
ghostty_free(NULL, buf, len);
|
||||
ghostty_formatter_free(formatter);
|
||||
ghostty_terminal_free(terminal);
|
||||
return 0;
|
||||
|
||||
@@ -44,6 +44,24 @@
|
||||
* 2. Create a GhosttyAllocator struct with your vtable and context
|
||||
* 3. Pass the allocator to functions that accept one
|
||||
*
|
||||
* ## Alloc/Free Helpers
|
||||
*
|
||||
* ghostty_alloc() and ghostty_free() provide a simple malloc/free-style
|
||||
* interface for allocating and freeing byte buffers through the library's
|
||||
* allocator. These are useful when:
|
||||
*
|
||||
* - You need to allocate a buffer to pass into a libghostty-vt function
|
||||
* (e.g. preparing input data for ghostty_terminal_vt_write()).
|
||||
* - You need to free a buffer returned by a libghostty-vt function
|
||||
* (e.g. the output of ghostty_formatter_format_alloc()).
|
||||
* - You are on a platform where the library's internal allocator differs
|
||||
* from the consumer's C runtime (e.g. Windows, where Zig's libc and
|
||||
* MSVC's CRT maintain separate heaps), so calling the standard C
|
||||
* free() on library-allocated memory would be undefined behavior.
|
||||
*
|
||||
* Always use the same allocator (or NULL) for both the allocation and
|
||||
* the corresponding free.
|
||||
*
|
||||
* @{
|
||||
*/
|
||||
|
||||
@@ -191,6 +209,46 @@ typedef struct GhosttyAllocator {
|
||||
const GhosttyAllocatorVtable *vtable;
|
||||
} GhosttyAllocator;
|
||||
|
||||
/**
|
||||
* Allocate a buffer of `len` bytes.
|
||||
*
|
||||
* Uses the provided allocator, or the default allocator if NULL is passed.
|
||||
* The returned buffer must be freed with ghostty_free() using the same
|
||||
* allocator.
|
||||
*
|
||||
* @param allocator Pointer to the allocator to use, or NULL for the default
|
||||
* @param len Number of bytes to allocate
|
||||
* @return Pointer to the allocated buffer, or NULL if allocation failed
|
||||
*
|
||||
* @ingroup allocator
|
||||
*/
|
||||
uint8_t* ghostty_alloc(const GhosttyAllocator* allocator, size_t len);
|
||||
|
||||
/**
|
||||
* Free memory that was allocated by a libghostty-vt function.
|
||||
*
|
||||
* Use this to free buffers returned by functions such as
|
||||
* ghostty_formatter_format_alloc(). Pass the same allocator that was
|
||||
* used for the allocation, or NULL if the default allocator was used.
|
||||
*
|
||||
* On platforms where the library's internal allocator differs from the
|
||||
* consumer's C runtime (e.g. Windows, where Zig's libc and MSVC's CRT
|
||||
* maintain separate heaps), calling the standard C free() on memory
|
||||
* allocated by the library causes undefined behavior. This function
|
||||
* guarantees the correct allocator is used regardless of platform.
|
||||
*
|
||||
* It is safe to pass a NULL pointer; the call is a no-op in that case.
|
||||
*
|
||||
* @param allocator Pointer to the allocator that was used to allocate the
|
||||
* memory, or NULL if the default allocator was used
|
||||
* @param ptr Pointer to the memory to free (may be NULL)
|
||||
* @param len Length of the allocation in bytes (must match the original
|
||||
* allocation size)
|
||||
*
|
||||
* @ingroup allocator
|
||||
*/
|
||||
void ghostty_free(const GhosttyAllocator* allocator, uint8_t* ptr, size_t len);
|
||||
|
||||
/** @} */
|
||||
|
||||
#endif /* GHOSTTY_VT_ALLOCATOR_H */
|
||||
|
||||
@@ -186,10 +186,9 @@ GhosttyResult ghostty_formatter_format_buf(GhosttyFormatter formatter,
|
||||
*
|
||||
* Each call formats the current terminal state. The buffer is allocated
|
||||
* using the provided allocator (or the default allocator if NULL).
|
||||
* The caller is responsible for freeing the returned buffer. When using
|
||||
* the default allocator (NULL), the buffer can be freed with `free()`.
|
||||
* When using a custom allocator, the buffer must be freed using the
|
||||
* same allocator.
|
||||
* The caller is responsible for freeing the returned buffer with
|
||||
* ghostty_free(), passing the same allocator (or NULL for the default)
|
||||
* that was used for the allocation.
|
||||
*
|
||||
* @param formatter The formatter handle (must not be NULL)
|
||||
* @param allocator Pointer to allocator, or NULL to use the default allocator
|
||||
|
||||
@@ -217,6 +217,8 @@ comptime {
|
||||
@export(&c.grid_ref_graphemes, .{ .name = "ghostty_grid_ref_graphemes" });
|
||||
@export(&c.grid_ref_style, .{ .name = "ghostty_grid_ref_style" });
|
||||
@export(&c.build_info, .{ .name = "ghostty_build_info" });
|
||||
@export(&c.alloc_alloc, .{ .name = "ghostty_alloc" });
|
||||
@export(&c.alloc_free, .{ .name = "ghostty_free" });
|
||||
|
||||
// On Wasm we need to export our allocator convenience functions.
|
||||
if (builtin.target.cpu.arch.isWasm()) {
|
||||
|
||||
67
src/terminal/c/allocator.zig
Normal file
67
src/terminal/c/allocator.zig
Normal file
@@ -0,0 +1,67 @@
|
||||
const std = @import("std");
|
||||
const testing = std.testing;
|
||||
const lib_alloc = @import("../../lib/allocator.zig");
|
||||
const CAllocator = lib_alloc.Allocator;
|
||||
|
||||
/// Allocate a buffer of `len` bytes using the given allocator
|
||||
/// (or the default allocator if NULL).
|
||||
///
|
||||
/// Returns a pointer to the allocated buffer, or NULL if the
|
||||
/// allocation failed.
|
||||
pub fn alloc(
|
||||
alloc_: ?*const CAllocator,
|
||||
len: usize,
|
||||
) callconv(.c) ?[*]u8 {
|
||||
const allocator = lib_alloc.default(alloc_);
|
||||
const buf = allocator.alloc(u8, len) catch return null;
|
||||
return buf.ptr;
|
||||
}
|
||||
|
||||
/// Free memory that was allocated by a libghostty-vt function.
|
||||
///
|
||||
/// This must be used to free buffers returned by functions like
|
||||
/// `format_alloc`. Pass the same allocator (or NULL for the default)
|
||||
/// that was used for the allocation.
|
||||
pub fn free(
|
||||
alloc_: ?*const CAllocator,
|
||||
ptr: ?[*]u8,
|
||||
len: usize,
|
||||
) callconv(.c) void {
|
||||
const mem = ptr orelse return;
|
||||
const allocator = lib_alloc.default(alloc_);
|
||||
allocator.free(mem[0..len]);
|
||||
}
|
||||
|
||||
test "alloc returns non-null" {
|
||||
const ptr = alloc(&lib_alloc.test_allocator, 16);
|
||||
try testing.expect(ptr != null);
|
||||
free(&lib_alloc.test_allocator, ptr, 16);
|
||||
}
|
||||
|
||||
test "alloc with null allocator" {
|
||||
const ptr = alloc(null, 8);
|
||||
try testing.expect(ptr != null);
|
||||
free(null, ptr, 8);
|
||||
}
|
||||
|
||||
test "alloc zero length" {
|
||||
const ptr = alloc(&lib_alloc.test_allocator, 0);
|
||||
defer free(&lib_alloc.test_allocator, ptr, 0);
|
||||
}
|
||||
|
||||
test "free null pointer" {
|
||||
free(&lib_alloc.test_allocator, null, 0);
|
||||
}
|
||||
|
||||
test "free allocated memory" {
|
||||
const allocator = lib_alloc.default(&lib_alloc.test_allocator);
|
||||
const mem = try allocator.alloc(u8, 16);
|
||||
free(&lib_alloc.test_allocator, mem.ptr, mem.len);
|
||||
}
|
||||
|
||||
test "free with null allocator" {
|
||||
// null allocator falls back to the default (test allocator in tests)
|
||||
const allocator = lib_alloc.default(null);
|
||||
const mem = try allocator.alloc(u8, 8);
|
||||
free(null, mem.ptr, mem.len);
|
||||
}
|
||||
@@ -1,4 +1,8 @@
|
||||
const lib_alloc = @import("../../lib/allocator.zig");
|
||||
const CAllocator = lib_alloc.Allocator;
|
||||
|
||||
const buildpkg = @import("build_info.zig");
|
||||
pub const allocator = @import("allocator.zig");
|
||||
pub const cell = @import("cell.zig");
|
||||
pub const color = @import("color.zig");
|
||||
pub const focus = @import("focus.zig");
|
||||
@@ -112,6 +116,9 @@ pub const mouse_encoder_encode = mouse_encode.encode;
|
||||
|
||||
pub const paste_is_safe = paste.is_safe;
|
||||
|
||||
pub const alloc_alloc = allocator.alloc;
|
||||
pub const alloc_free = allocator.free;
|
||||
|
||||
pub const size_report_encode = size_report.encode;
|
||||
|
||||
pub const cell_get = cell.get;
|
||||
@@ -139,6 +146,7 @@ pub const grid_ref_graphemes = grid_ref.grid_ref_graphemes;
|
||||
pub const grid_ref_style = grid_ref.grid_ref_style;
|
||||
|
||||
test {
|
||||
_ = allocator;
|
||||
_ = buildpkg;
|
||||
_ = cell;
|
||||
_ = color;
|
||||
|
||||
Reference in New Issue
Block a user