Hacker News new | ask | show | jobs
by yellowapple 2383 days ago
Also worth noting that this is one thing Zig seems to have gotten quite right already: every standard library function which might need to allocate memory requires the user of that function to pass in an allocator of one's choosing, whether provided with the standard library or provided from scratch.

For example, I've been (slowly, given limited free time) working on a SQLite binding for Zig. SQLite has its own allocator, which I've wrapped like so:

    const std = @import("std");
    const Allocator = std.mem.Allocator;
    const ArrayList = std.ArrayList;
    
    // This is all ripped from Zig's C allocator, with adaptations to point to
    // SQLite's allocator instead.
    pub const allocator = &allocator_state;
    var allocator_state = Allocator{
        .reallocFn = realloc,
        .shrinkFn = shrink,
    };
    
    fn realloc(self: *Allocator,
               old_mem: []u8,
               old_align: u29,
               new_size: usize,
               new_align: u29) ![]u8 {
        std.debug.assert(new_align <= @alignOf(c_longdouble));
        const old_ptr =
            if (old_mem.len == 0) null else @ptrCast(*c_void, old_mem.ptr);
        const buf = sqlite3_realloc64(old_ptr, @intCast(u64, new_size))
            orelse return error.OutOfMemory;
        return @ptrCast([*]u8, buf)[0..new_size];
    }
    
    fn shrink(self: *Allocator,
              old_mem: []u8,
              old_align: u29,
              new_size: usize,
              new_align: u29) []u8 {
        const old_ptr = @ptrCast(*c_void, old_mem.ptr);
        const buf = sqlite3_realloc64(old_ptr, @intCast(u64, new_size))
            orelse return old_mem[0..new_size];
        return @ptrCast([*]u8, buf)[0..new_size];
    }
And later, to actually open a database (where we need an allocator to add a null byte to a Zig string to satisfy SQLite's expectations for a database name, so we might as well use SQLite's)¹:

    pub fn open(name: []const u8) !Database {
        // "allocator" here being the const-defined one above
        var cstrName = try std.cstr.addNullByte(allocator, name);
        defer allocator.free(cstrName);
        var db: Database = undefined;
        var rc = sqlite3_open(cstrName.ptr, &db);
    
        if (rc == SQLITE_OK) {
            return db;
        } else {
            _ = sqlite3_close(db);
            return errorCode(rc);
        }
    }
Of course, it'd be even better if such a wrapper actually used SQLite's own support for custom allocators to follow the Zig stdlib convention of allowing the user to supply one's own allocator. Going the other way seemed like a reasonable short-term choice, though, and it was a good enough excuse for me to learn the ropes on custom allocators (even if most of the code ultimately came from Zig's own C allocator).

----

¹: It turns out that this specific example will apparently be entirely unnecessary in future versions of Zig; the master branch documentation seems to do away with "Zig strings" v. "C strings", instead just making strings null-terminated by default (using what looks to be new support for "sentinel-terminated" pointers/arrays/slices). Looks like I've got some work to do :)