From 9ba7dbd9b7de6af7b279176ea392d5aeb61d8ff9 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Tue, 2 Jun 2026 20:17:30 +0800 Subject: [PATCH] Extract ZigToCurlAllocator into it's own file, only use in debug Extract ZigToCurlAllocator to its own file and rename it to CurlDebugAllocator to indicate that it is only used in debug. Also, only use it in debug. In release mode, this just adds 16 bytes of overhead per allocation that curl makes. In debug mode, it's the same overhead, but it at least hooks into the std's DebugAllocator which can detect misuse. --- src/network/CurlDebugAllocator.zig | 134 +++++++++++++++++++++++++++++ src/network/Network.zig | 122 +++----------------------- 2 files changed, 144 insertions(+), 112 deletions(-) create mode 100644 src/network/CurlDebugAllocator.zig diff --git a/src/network/CurlDebugAllocator.zig b/src/network/CurlDebugAllocator.zig new file mode 100644 index 00000000..7fc700e5 --- /dev/null +++ b/src/network/CurlDebugAllocator.zig @@ -0,0 +1,134 @@ +// Copyright (C) 2023-2026 Lightpanda (Selecy SAS) +// +// Francis Bouvier +// Pierre Tachoire +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +const std = @import("std"); +const lp = @import("lightpanda"); + +const libcurl = @import("../sys/libcurl.zig"); + +const Allocator = std.mem.Allocator; + +const CurlDebugAllocator = @This(); + +// C11 requires malloc to return memory aligned to max_align_t (16 bytes on x86_64). +// We match this guarantee since libcurl expects malloc-compatible alignment. +const alignment = 16; + +var instance: ?CurlDebugAllocator = null; + +allocator: Allocator, + +pub fn init(allocator: Allocator) void { + lp.assert(instance == null, "Initialization of curl must happen only once", .{}); + instance = .{ .allocator = allocator }; +} + +pub fn interface() libcurl.CurlAllocator { + return .{ + .free = free, + .strdup = strdup, + .malloc = malloc, + .calloc = calloc, + .realloc = realloc, + }; +} + +fn _allocBlock(size: usize) ?*Block { + const slice = instance.?.allocator.alignedAlloc(u8, .fromByteUnits(alignment), Block.fullsize(size)) catch return null; + const block: *Block = @ptrCast(@alignCast(slice.ptr)); + block.size = size; + return block; +} + +fn _freeBlock(header: *Block) void { + instance.?.allocator.free(header.slice()); +} + +fn malloc(size: usize) ?*anyopaque { + const block = _allocBlock(size) orelse return null; + return @ptrCast(block.data()); +} + +fn calloc(nmemb: usize, size: usize) ?*anyopaque { + const total = nmemb * size; + const block = _allocBlock(total) orelse return null; + const ptr = block.data(); + @memset(ptr[0..total], 0); // for historical reasons, calloc zeroes memory, but malloc does not. + return @ptrCast(ptr); +} + +fn realloc(ptr: ?*anyopaque, size: usize) ?*anyopaque { + const p = ptr orelse return malloc(size); + const block = Block.fromPtr(p); + + const old_size = block.size; + if (size == old_size) return ptr; + + if (instance.?.allocator.resize(block.slice(), alignment + size)) { + block.size = size; + return ptr; + } + + const copy_size = @min(old_size, size); + const new_block = _allocBlock(size) orelse return null; + @memcpy(new_block.data()[0..copy_size], block.data()[0..copy_size]); + _freeBlock(block); + return @ptrCast(new_block.data()); +} + +fn free(ptr: ?*anyopaque) void { + const p = ptr orelse return; + _freeBlock(Block.fromPtr(p)); +} + +fn strdup(str: [*:0]const u8) ?[*:0]u8 { + const len = std.mem.len(str); + const header = _allocBlock(len + 1) orelse return null; + const ptr = header.data(); + @memcpy(ptr[0..len], str[0..len]); + ptr[len] = 0; + return ptr[0..len :0]; +} + +const Block = extern struct { + size: usize = 0, + _padding: [alignment - @sizeOf(usize)]u8 = .{0} ** (alignment - @sizeOf(usize)), + + inline fn fullsize(bytes: usize) usize { + return alignment + bytes; + } + + inline fn fromPtr(ptr: *anyopaque) *Block { + const raw: [*]u8 = @ptrCast(ptr); + return @ptrCast(@alignCast(raw - @sizeOf(Block))); + } + + inline fn data(self: *Block) [*]u8 { + const ptr: [*]u8 = @ptrCast(self); + return ptr + @sizeOf(Block); + } + + inline fn slice(self: *Block) []align(alignment) u8 { + const base: [*]align(alignment) u8 = @ptrCast(@alignCast(self)); + return base[0 .. alignment + self.size]; + } +}; + +comptime { + std.debug.assert(@sizeOf(Block) == alignment); +} diff --git a/src/network/Network.zig b/src/network/Network.zig index c7591805..cbc1b454 100644 --- a/src/network/Network.zig +++ b/src/network/Network.zig @@ -30,6 +30,7 @@ const http = @import("http.zig"); const IpFilter = @import("IpFilter.zig"); const RobotStore = @import("Robots.zig").RobotStore; const WebBotAuth = @import("WebBotAuth.zig"); +const CurlDebugAllocator = @import("CurlDebugAllocator.zig"); const Cache = @import("cache/Cache.zig"); const FsCache = @import("cache/FsCache.zig"); @@ -39,6 +40,7 @@ const net = std.net; const posix = std.posix; const Allocator = std.mem.Allocator; const DoublyLinkedList = std.DoublyLinkedList; +const IS_DEBUG = builtin.mode == .Debug; const Network = @This(); @@ -152,120 +154,16 @@ const TickCallback = struct { fun: *const fn (*anyopaque) void, }; -const ZigToCurlAllocator = struct { - // C11 requires malloc to return memory aligned to max_align_t (16 bytes on x86_64). - // We match this guarantee since libcurl expects malloc-compatible alignment. - const alignment = 16; - - const Block = extern struct { - size: usize = 0, - _padding: [alignment - @sizeOf(usize)]u8 = .{0} ** (alignment - @sizeOf(usize)), - - inline fn fullsize(bytes: usize) usize { - return alignment + bytes; - } - - inline fn fromPtr(ptr: *anyopaque) *Block { - const raw: [*]u8 = @ptrCast(ptr); - return @ptrCast(@alignCast(raw - @sizeOf(Block))); - } - - inline fn data(self: *Block) [*]u8 { - const ptr: [*]u8 = @ptrCast(self); - return ptr + @sizeOf(Block); - } - - inline fn slice(self: *Block) []align(alignment) u8 { - const base: [*]align(alignment) u8 = @ptrCast(@alignCast(self)); - return base[0 .. alignment + self.size]; - } - }; - - comptime { - std.debug.assert(@sizeOf(Block) == alignment); - } - - var instance: ?ZigToCurlAllocator = null; - - allocator: Allocator, - - pub fn init(allocator: Allocator) void { - lp.assert(instance == null, "Initialization of curl must happen only once", .{}); - instance = .{ .allocator = allocator }; - } - - pub fn interface() libcurl.CurlAllocator { - return .{ - .free = free, - .strdup = strdup, - .malloc = malloc, - .calloc = calloc, - .realloc = realloc, - }; - } - - fn _allocBlock(size: usize) ?*Block { - const slice = instance.?.allocator.alignedAlloc(u8, .fromByteUnits(alignment), Block.fullsize(size)) catch return null; - const block: *Block = @ptrCast(@alignCast(slice.ptr)); - block.size = size; - return block; - } - - fn _freeBlock(header: *Block) void { - instance.?.allocator.free(header.slice()); - } - - fn malloc(size: usize) ?*anyopaque { - const block = _allocBlock(size) orelse return null; - return @ptrCast(block.data()); - } - - fn calloc(nmemb: usize, size: usize) ?*anyopaque { - const total = nmemb * size; - const block = _allocBlock(total) orelse return null; - const ptr = block.data(); - @memset(ptr[0..total], 0); // for historical reasons, calloc zeroes memory, but malloc does not. - return @ptrCast(ptr); - } - - fn realloc(ptr: ?*anyopaque, size: usize) ?*anyopaque { - const p = ptr orelse return malloc(size); - const block = Block.fromPtr(p); - - const old_size = block.size; - if (size == old_size) return ptr; - - if (instance.?.allocator.resize(block.slice(), alignment + size)) { - block.size = size; - return ptr; - } - - const copy_size = @min(old_size, size); - const new_block = _allocBlock(size) orelse return null; - @memcpy(new_block.data()[0..copy_size], block.data()[0..copy_size]); - _freeBlock(block); - return @ptrCast(new_block.data()); - } - - fn free(ptr: ?*anyopaque) void { - const p = ptr orelse return; - _freeBlock(Block.fromPtr(p)); - } - - fn strdup(str: [*:0]const u8) ?[*:0]u8 { - const len = std.mem.len(str); - const header = _allocBlock(len + 1) orelse return null; - const ptr = header.data(); - @memcpy(ptr[0..len], str[0..len]); - ptr[len] = 0; - return ptr[0..len :0]; - } -}; - fn globalInit(allocator: Allocator) void { - ZigToCurlAllocator.init(allocator); + // Only route curl's own allocations through our allocator in Debug, so the + // leak detector sees them. In Release it'd just wrap c_allocator (curl's + // default malloc anyway) at the cost of a per-allocation header. + const curl_allocator = comptime if (IS_DEBUG) CurlDebugAllocator.interface() else null; + if (comptime IS_DEBUG) { + CurlDebugAllocator.init(allocator); + } - libcurl.curl_global_init(.{ .ssl = true }, ZigToCurlAllocator.interface()) catch |err| { + libcurl.curl_global_init(.{ .ssl = true }, curl_allocator) catch |err| { lp.assert(false, "curl global init", .{ .err = err }); }; }