The title is awkward, but here goes…
Say, you want to write a function that (1) opens a file, (2) allocates a buffer large enough to read the whole file, (3) reads the file into the buffer (4) closes the file, (5) works with the buffer, and (6) frees the buffer.
If in the process an error is caught, the file has to be closed (if it’s open) and the buffer has to be freed (if it’s allocated).
Also, the file has to be closed as soon as possible, which is a good policy to follow.
How would you do this?
My code is below, but it has a bug -
fn readFile(fname: []const u8, alloc8r: std.mem.Allocator) !void
{
const f = try std.fs.cwd().openFile(fname, .{});
errdefer f.close(); // -- But what if an error is returned AFTER the file is closed?
const f_len = try f.getEndPos();
var buf = try alloc8r.alloc(u8, f_len);
defer alloc8r.free(buf); // This is fine
const read_bytes = try f.readAll(buf);
f.close(); // We want to close the file here, but `errdefer f.close();` is going to fire if we return an error somewhere below this line
std.debug.print("Read {} bytes\n", .{read_bytes});
// Work with `buf`, but if we return an error, the file is going to be closed again, which means a core dump
}
The problem with the code above is that errdefer f.close();
can be triggered even after the file is closed.
Another idea is to use a block { }
around the file handling part. This, however, would mean that buf
would be deallocated at the end of this block:
fn readFile(fname: []const u8, alloc8r: std.mem.Allocator) !void
{
{
const f = try std.fs.cwd().openFile(fname, .{});
defer f.close(); // The file will be closed when we leave the block, which is great
const f_len = try f.getEndPos();
var buf = try alloc8r.alloc(u8, f_len);
defer alloc8r.free(buf); // -- `buf` will be freed when we leave the block :(
const read_bytes = try f.readAll(buf);
std.debug.print("Read {} bytes\n", .{read_bytes});
}
// Can't work with `buf`, it's not defined in this scope :(
}
So, to avoid this, defer alloc8r.free(buf);
would have to be placed AFTER the block where file handling takes place.
However, if try f.readAll(buf);
returns an error, buf
is not going to be freed, so we have to add errdefer
immediately after allocating buf
:
fn readFile(fname: []const u8, alloc8r: std.mem.Allocator) !void
{
var buf: []u8 = undefined; // Don't like this...
{
const f = try std.fs.cwd().openFile(fname, .{});
defer f.close(); // The file will be closed when we leave the block, which is great
const f_len = try f.getEndPos();
buf = try alloc8r.alloc(u8, f_len);
errdefer alloc8r.free(buf); // Will be active until the block ends
const read_bytes = try f.readAll(buf);
std.debug.print("Read {} bytes\n", .{read_bytes});
}
defer alloc8r.free(buf);
// Work with `buf`, it will be freed when we leave the function, whether normally, or by returning an error
}
This could actually work, if I’m not missing anything, but… It’s far from simple and elegant.
I wonder if there are better ideas.