Memory leak when assigning in return vs setting variable then return

Hello!

Below I have a piece of code that duplicates memory using an arena and returns a MemObject type containing the arena and the duplicated value.

There are two functions mem_object and mem_object_leak the only difference between them is that mem_object_leak sets the value of MemObject in the return while mem_object first assigns to a variable and then returns.

Both arenas are deinitialized so why does the leaky one leak ?

Edit: compiler version is 0.14.0-dev.3222+8a3aebaee

const std = @import("std");

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{ .stack_trace_frames = 10 }){};
    defer _ = gpa.deinit();

    const string: []const u8 = "hello";
    var no_leak = try mem_object(gpa.allocator(), string);
    var leak = try mem_object_leaky(gpa.allocator(), string);

    no_leak.deinit();
    leak.deinit();
}

pub fn MemObject(comptime T: type) type {
    return struct {
        const Self = @This();

        arena: std.heap.ArenaAllocator,
        value: T,

        pub fn deinit(self: *Self) void {
            self.arena.deinit();
        }
    };
}

pub fn mem_object(allocator: std.mem.Allocator, value: []const u8) !MemObject([]const u8) {
    var arena = std.heap.ArenaAllocator.init(allocator);
    errdefer arena.deinit();
    const cloner = .{ .allocator = arena.allocator() };
    const cloned = try clone_anytype(cloner, value);
    return .{
        .arena = arena,
        // only difference
        .value = cloned,
    };
}

pub fn mem_object_leaky(allocator: std.mem.Allocator, value: []const u8) !MemObject([]const u8) {
    var arena = std.heap.ArenaAllocator.init(allocator);
    errdefer arena.deinit();
    const cloner = .{ .allocator = arena.allocator() };
    return .{
        .arena = arena,
        // only difference
        .value = try clone_anytype(cloner, value),
    };
}

pub fn clone_anytype(cloner: anytype, value: anytype) error{OutOfMemory}!@TypeOf(value) {
    return try cloner.allocator.dupe(u8, value);
}

When you copy an arena, you copy all of its internal state, so when you do an allocation after copying it, the copied arena will not know about it and therefore cannot free it on deinit.

In that case mem_object should leak as well but it’s doesn’t.

Also I’m not copying the arena until I’m done with the allocation and in clone_anytype the interface is passed rather than the whole arena.

In that case mem_object should leak as well but it’s doesn’t.

For mem_object you copy the arena after the allocation:

    const cloned = try clone_anytype(cloner, value); // Allocation
    return .{
        .arena = arena, // copy

Also I’m not copying the arena until I’m done with the allocation and in clone_anytype the interface is passed rather than the whole arena.

The interface contains a mutable pointer to the arena. And you are clearly copying the arena before the allocation in mem_object_leaky:

    return .{
        .arena = arena, // copy
        // only difference
        .value = try clone_anytype(cloner, value), // Allocation
    };

oooooh I see now. Thanks for the help! was really confused about this

To clarify, the order of copying in a return block is the order of assignments correct ?

Such copies problems can be avoided my storing a pointer to the arena instead, for example:

2 Likes

I tried to do what std.json.Parsed does but I failed to see that it’s a pointer to an arena. Thanks!