From 8de5bf6fecc19cf2be6093b4338691fe4af84815 Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Thu, 24 Sep 2026 12:52:49 -0300 Subject: Hand every C library's allocator hook the same C heap C's malloc, calloc, realloc and free over a Zig allocator were written three times: syntax.zig's for tree-sitter, pdf.zig's for MuPDF, and gui.zig's for FreeType and HarfBuzz. They are now one, src/c_heap.zig's Heap. Each hook names the allocator variable its heap reads; gui.zig exports its heap under the ui_malloc names font.c and HarfBuzz call. It is a module of its own because pdf.zig is the MuPDF module, and a file cannot belong to that module and the core's at once. A block starts with a 16-byte header holding its size and the allocator that made it, so it goes back where it came from even after the heap is repointed, as the fonts' copy did. The fonts' copy carried the 16-byte Allocator itself in a 32-byte header; here the header holds the allocator's index in a table of every allocator a heap has used (entries are published once and never change, and a heap remembers where its current one was last found). The difference is measurable: with the 32-byte header, pardes-pdf-bench's filtered page render at 96 dpi took 1.1% longer than before this change, slower in 15 of 16 pinned rounds in two separate runs, and highlighting all of src/pardes.zig (305k tree-sitter blocks) up to 0.6% longer, against an A/A pair within 0.7%. With the 16-byte header both are back within the A/A pair's noise. Two other things change. For tree-sitter and MuPDF, a block made before the allocator was repointed now goes back to the one that made it rather than through the current one. For the fonts, realloc(block, 0) now frees and returns null, as glibc's does and as the other two copies already did; neither FreeType nor HarfBuzz asks for it. The heap's test replaces the tests of the old copies. Co-Authored-By: Claude Opus 5.5 (1M context) --- src/pdf.zig | 87 +++++++++---------------------------------------------------- 1 file changed, 13 insertions(+), 74 deletions(-) (limited to 'src/pdf.zig') diff --git a/src/pdf.zig b/src/pdf.zig index de5e6d86..8ddca497 100644 --- a/src/pdf.zig +++ b/src/pdf.zig @@ -2,66 +2,30 @@ //! setjmp/longjmp; pdf_bridge.c catches every such throw before control returns //! to Zig. This module is present in native builds unless `-Dmupdf=false`. const std = @import("std"); +const c_heap = @import("c_heap"); const c = @cImport({ @cInclude("pdf_bridge.h"); }); -const allocation_header_size = 16; +/// What MuPDF allocates from, itself and the libraries it bundles (they all +/// allocate through its context): `start`'s allocator, through the shared C +/// heap. MuPDF's allocator functions carry a user pointer the heap does not +/// need. var bridge_allocator: std.mem.Allocator = undefined; +const bridge_heap = c_heap.Heap(&bridge_allocator); var bridge_started = false; -comptime { - if (allocation_header_size < @sizeOf(usize)) - @compileError("PDF allocation header is too small"); +fn bridgeAlloc(_: ?*anyopaque, size: usize) callconv(.c) ?*anyopaque { + return bridge_heap.malloc(size); } -fn bridgeAlloc(user: ?*anyopaque, size_arg: usize) callconv(.c) ?*anyopaque { - const user_ptr = user orelse return null; - const allocator: *std.mem.Allocator = @ptrCast(@alignCast(user_ptr)); - const size = @max(size_arg, 1); - const total = std.math.add(usize, allocation_header_size, size) catch - return null; - const bytes = allocator.alignedAlloc(u8, .@"16", total) catch return null; - const header: *align(16) usize = @ptrCast(bytes.ptr); - header.* = total; - return @ptrCast(bytes.ptr + allocation_header_size); +fn bridgeRealloc(_: ?*anyopaque, block: ?*anyopaque, size: usize) callconv(.c) ?*anyopaque { + return bridge_heap.realloc(block, size); } -fn bridgeFree(user: ?*anyopaque, pointer: ?*anyopaque) callconv(.c) void { - const data = pointer orelse return; - const user_ptr = user orelse @panic("PDF allocator callback has no allocator"); - const allocator: *std.mem.Allocator = @ptrCast(@alignCast(user_ptr)); - const data_bytes: [*]u8 = @ptrCast(data); - const base: [*]align(16) u8 = - @ptrCast(@alignCast(data_bytes - allocation_header_size)); - const header: *align(16) usize = @ptrCast(base); - allocator.free(base[0..header.*]); -} - -fn bridgeRealloc( - user: ?*anyopaque, - pointer: ?*anyopaque, - size_arg: usize, -) callconv(.c) ?*anyopaque { - if (pointer == null) return bridgeAlloc(user, size_arg); - if (size_arg == 0) { - bridgeFree(user, pointer); - return null; - } - - const user_ptr = user orelse return null; - const allocator: *std.mem.Allocator = @ptrCast(@alignCast(user_ptr)); - const total = std.math.add(usize, allocation_header_size, size_arg) catch - return null; - const data_bytes: [*]u8 = @ptrCast(pointer.?); - const base: [*]align(16) u8 = - @ptrCast(@alignCast(data_bytes - allocation_header_size)); - const header: *align(16) usize = @ptrCast(base); - const resized = allocator.realloc(base[0..header.*], total) catch return null; - const resized_header: *align(16) usize = @ptrCast(resized.ptr); - resized_header.* = total; - return @ptrCast(resized.ptr + allocation_header_size); +fn bridgeFree(_: ?*anyopaque, block: ?*anyopaque) callconv(.c) void { + bridge_heap.free(block); } /// Installs the allocator used by all subsequently opened PDF documents and @@ -70,7 +34,7 @@ pub fn start(allocator: std.mem.Allocator) void { if (bridge_started) @panic("PDF allocator is already started"); bridge_allocator = allocator; const descriptor = c.pardes_pdf_allocator{ - .user = &bridge_allocator, + .user = null, .alloc = bridgeAlloc, .realloc = bridgeRealloc, .free = bridgeFree, @@ -90,31 +54,6 @@ pub fn stop() void { bridge_allocator = undefined; } -test "PDF C allocator callbacks preserve and free exact allocations" { - var allocator = std.testing.allocator; - - var live: ?*anyopaque = bridgeAlloc(&allocator, 4) orelse - return error.OutOfMemory; - defer if (live) |pointer| bridgeFree(&allocator, pointer); - const original: [*]u8 = @ptrCast(live.?); - @memcpy(original[0..4], "data"); - - live = bridgeRealloc(&allocator, live, 32) orelse - return error.OutOfMemory; - const grown: [*]u8 = @ptrCast(live.?); - try std.testing.expectEqualSlices(u8, "data", grown[0..4]); - - try std.testing.expect(bridgeAlloc( - &allocator, - std.math.maxInt(usize), - ) == null); - - const zero = bridgeAlloc(&allocator, 0) orelse return error.OutOfMemory; - bridgeFree(&allocator, zero); - try std.testing.expect(bridgeRealloc(&allocator, live, 0) == null); - live = null; -} - test "custom allocator owns a complete PDF document lifecycle" { start(std.testing.allocator); defer stop(); -- cgit v1.3