From c891eff664a1506cdab6fb93b74126775ecfc8f4 Mon Sep 17 00:00:00 2001 From: Nikolay Govorov Date: Wed, 11 Mar 2026 03:34:27 +0000 Subject: [PATCH] Use zig allocator for libcurl --- src/network/Runtime.zig | 118 +++++++++++++++++++++++++++++++++++++++- src/sys/libcurl.zig | 51 ++++++++++++++++- 2 files changed, 164 insertions(+), 5 deletions(-) diff --git a/src/network/Runtime.zig b/src/network/Runtime.zig index 0112dc18..0e6cc398 100644 --- a/src/network/Runtime.zig +++ b/src/network/Runtime.zig @@ -51,8 +51,120 @@ wakeup_pipe: [2]posix.fd_t = .{ -1, -1 }, shutdown: std.atomic.Value(bool) = .init(false), -fn globalInit() void { - libcurl.curl_global_init(.{ .ssl = true }) catch |err| { +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); + + libcurl.curl_global_init(.{ .ssl = true }, ZigToCurlAllocator.interface()) catch |err| { lp.assert(false, "curl global init", .{ .err = err }); }; } @@ -62,7 +174,7 @@ fn globalDeinit() void { } pub fn init(allocator: Allocator, config: *const Config) !Runtime { - globalInit(); + globalInit(allocator); errdefer globalDeinit(); const pipe = try posix.pipe2(.{ .NONBLOCK = true, .CLOEXEC = true }); diff --git a/src/sys/libcurl.zig b/src/sys/libcurl.zig index 759cba25..f13e999a 100644 --- a/src/sys/libcurl.zig +++ b/src/sys/libcurl.zig @@ -41,6 +41,20 @@ pub const CurlHeaderFunction = fn ([*]const u8, usize, usize, *anyopaque) usize; pub const CurlWriteFunction = fn ([*]const u8, usize, usize, *anyopaque) usize; pub const curl_writefunc_error: usize = c.CURL_WRITEFUNC_ERROR; +pub const FreeCallback = fn (ptr: ?*anyopaque) void; +pub const StrdupCallback = fn (str: [*:0]const u8) ?[*:0]u8; +pub const MallocCallback = fn (size: usize) ?*anyopaque; +pub const CallocCallback = fn (nmemb: usize, size: usize) ?*anyopaque; +pub const ReallocCallback = fn (ptr: ?*anyopaque, size: usize) ?*anyopaque; + +pub const CurlAllocator = struct { + free: FreeCallback, + strdup: StrdupCallback, + malloc: MallocCallback, + calloc: CallocCallback, + realloc: ReallocCallback, +}; + pub const CurlGlobalFlags = packed struct(u8) { ssl: bool = false, _reserved: u7 = 0, @@ -449,8 +463,41 @@ pub const CurlMsg = struct { data: CurlMsgData, }; -pub fn curl_global_init(flags: CurlGlobalFlags) Error!void { - try errorCheck(c.curl_global_init(flags.to_c())); +pub fn curl_global_init(flags: CurlGlobalFlags, comptime curl_allocator: ?CurlAllocator) Error!void { + const alloc = curl_allocator orelse { + return errorCheck(c.curl_global_init(flags.to_c())); + }; + + // The purpose of these wrappers is to hide callconv + // and provide an easy place to add logging when debugging. + const free = struct { + fn cb(ptr: ?*anyopaque) callconv(.c) void { + alloc.free(ptr); + } + }.cb; + const strdup = struct { + fn cb(str: [*c]const u8) callconv(.c) [*c]u8 { + const s: [*:0]const u8 = @ptrCast(str orelse return null); + return @ptrCast(alloc.strdup(s)); + } + }.cb; + const malloc = struct { + fn cb(size: usize) callconv(.c) ?*anyopaque { + return alloc.malloc(size); + } + }.cb; + const calloc = struct { + fn cb(nmemb: usize, size: usize) callconv(.c) ?*anyopaque { + return alloc.calloc(nmemb, size); + } + }.cb; + const realloc = struct { + fn cb(ptr: ?*anyopaque, size: usize) callconv(.c) ?*anyopaque { + return alloc.realloc(ptr, size); + } + }.cb; + + try errorCheck(c.curl_global_init_mem(flags.to_c(), malloc, free, realloc, strdup, calloc)); } pub fn curl_global_cleanup() void {