Confusion about releasing a resource from a future

I’m trying to understand how to correctly manage resources when using futures. The following example does not compile:

const std = @import("std");
const Io = std.Io;
const Allocator = std.mem.Allocator;
const ArrayList = std.ArrayList;

pub fn main(init: std.process.Init) !void {
    const gpa = init.gpa;
    const io = init.io;

    var numbers_future = io.async(createNumbers, .{gpa});
    defer if (numbers_future.cancel(io)) |numbers| numbers.deinit(gpa) else |_| {};

    const numbers = try numbers_future.await(io);
    for (numbers.items) |number| {
        std.debug.print("Number: {}\n", .{number});
    }
}

fn createNumbers(gpa: Allocator) !std.ArrayList(i32) {
    var numbers: std.ArrayList(i32) = .empty;
    errdefer numbers.deinit(gpa);

    try numbers.append(gpa, 1);
    try numbers.append(gpa, 2);

    return numbers;
}
src/main.zig:11:59: error: expected type '*T', found '*const T'
    defer if (numbers_future.cancel(io)) |numbers| numbers.deinit(gpa) else |_| {};
                                                   ~~~~~~~^~~~~~~
src/main.zig:11:59: note: T = array_list.Aligned(i32,null)
src/main.zig:11:59: note: cast discards const qualifier

If I understand the Zig 0.16.0 changelog correctly this is the recommended pattern to handle resources.

Use this pattern to avoid resource leaks and handle Cancelation gracefully:

But it fails because the std.ArrayList.deinit function takes the ArrayList by a mutable pointer which seems to be very common in Zig for deinit functions.

Am I missing something? How to correctly release resources in this case?

returned temporaries, even if captured in this way, are const. you must open a block, assign to a variable and then deinit. assuming the object is an ArrayList, this is safe to do, since the reason the function accepts *T is to do list.* = undefined, which is desirable in general to help catch use after free.

My way be like: when in doubt, abuse arena…

const std = @import("std");
const Io = std.Io;
const Allocator = std.mem.Allocator;
const ArrayList = std.ArrayList;

pub fn main(init: std.process.Init) !void {
    const gpa = init.gpa;
    const io = init.io;

    var future_aa = std.heap.ArenaAllocator.init(gpa);

    var numbers_future = io.async(createNumbers, .{future_aa.allocator()});
    defer if (numbers_future.cancel(io)) |_| future_aa.deinit() else |_| {};

    const numbers = try numbers_future.await(io);
    for (numbers) |number| {
        std.debug.print("Number: {}\n", .{number});
    }
}

fn createNumbers(gpa: Allocator) !std.ArrayList(i32) {
    var numbers: std.ArrayList(i32) = .empty;
    errdefer {numbers.deinit(gpa); 
                    gpa.deinit();
    }
    

    try numbers.append(gpa, 1);
    try numbers.append(gpa, 2);

    return numbers;
}

That makes a lot of sense. Thanks!

For future reference, this code works:

    var numbers_future = io.async(createNumbers, .{gpa});
    defer if (numbers_future.cancel(io)) |numbers| {
        var n = numbers;
        n.deinit(gpa);
    } else |_| {};

Temporary var is a good solution in general. In this case however, I’d probably just use toOwnedSlice() in the return from create numbers, and just have the slice in main, which doesn’t have this problem. In general prefer slices over arraylists if I’m done adding or removing values from it.

2 Likes