Ziguanic way of converting C types to Zig types?

Hi everyone, I’m currently switching one of my library’s build system from make to Zig. So far so good, everything work as expected, now since the library is building fine, I figure it’s time to use Zig unit testing framework for making sure everything work as expected. The only problem I’ve found is that, the process of converting C types to Zig types is pretty verbose, and hinders a lot of the readability of my test case, so I’ve decided to build two simple functions to make the conversion between ?*anyopaque amd slice :

const clib = @cImport({
    @cInclude("clib.h");
});

pub fn sliceFromAnyopaque(c_ptr: ?*anyopaque, comptime child_type: type) [:0]child_type {
    const slc: [*c]child_type = @as([*c]child_type, @alignCast(@ptrCast(c_ptr.?)));
    const sp = std.mem.span(slc);
    return (sp);
}

pub fn anyopaqueFromSlice(slice: anytype) ?*anyopaque {
    return (@as(?*anyopaque, @alignCast(@ptrCast(slice.ptr))));
}

test "heap_allocator : test1" {
    const heap: *HeapAllocator = clib.heap_init();
    defer _ = clib.heap_deinit(heap);
    const ptr = sliceFromAnyopaque(heap.alloc.?(heap, 32), u8);
    defer _ = heap.dealloc.?(heap, anyopaqueFromSlice(ptr));
}

This is what I came up with, and I was wondering if there was a better or cleaner way to do that, and if the logic make sense ?

Are heap.alloc and heap.dealloc optional function pointers?
I think you could consider defining those as non optional pointers that are initialized with undefined, then have some field in heap that says if heap was fully initialized and only access those pointers if that is true.

So overall I would imagine something more like this:

test "heap_allocator : test1" {
    const heap: *HeapAllocator = clib.heap_init();
    defer _ = clib.heap_deinit(heap);
    heap.setup(); // unknown args, only needed if this can't be done in heap_init
    const ptr = sliceFromAnyopaque(heap.alloc(heap, 32), u8);
    defer _ = heap.dealloc(heap, anyopaqueFromSlice(ptr));
}

Another thing you could consider is defining HeapAllocator yourself (or wrapping it) and adding methods to it, so that you can for example use heap.alloc(u8, 32) and get your desired result type directly.

pub const WrapperAllocator = struct {
     heap:*HeapAllocator,

     pub fn init() WrapperAllocator {
         return .{ .heap = clib.heap_init() };
     }
     pub fn deinit(self:WrapperAllocator) void {
         _ = clib.heap_deinit(self.heap);
     }
     pub fn alloc(self:WrapperAllocator, comptime T:type, size:usize) ![:0]T {
         // I probably would change this from size to len, so I would use something like
         // (len+1) * sizeOf(T) to calculate size,
         // however you still need to deal with alignment if your T has any alignment 
         // greater than 1, for that it can be valuable to look at existing allocators and
         // functions like `std.mem.alignForward`
         const maybe_c_ptr = self.heap.alloc.?(self.heap, size); 
         if(maybe_c_ptr == null) return error.OutOfMemory;
         const slc: [*:0]T = @alignCast(@ptrCast(maybe_c_ptr.?));
         return std.mem.span(slc);
     }
     pub fn free(self:WrapperAllocator, slice:anytype) void {
         _ = self.heap.dealloc.?(self.heap, @alignCast(@ptrCast(slice.ptr)));
     }
};

test "heap_allocator : test1" {
    const heap = WrapperAllocator.init();
    defer heap.deinit();
    const ptr = try heap.alloc(u8, 32);
    defer heap.free(ptr);
}

Also where is HeapAllocator defined, do you define it yourself? Then you probably could turn WrapperAllocator into a direct replacement of HeapAllocator.

Another question you should ask yourself is whether you could just use std.heap.c_allocator directly, or even just normal zig allocators and using functions like alloc.dupeZ, std.fmt.bufPrintZ or std.fmt.allocPrintZ to get zero terminated slices, you then can pass slice.ptr directly to c functions.

1 Like

To give some more context, this library of mine, is for a school’s projects. In those assignments I’m not allowed to use anything more than :

  • malloc
  • free
  • open
  • read
  • write

so basically not a lot, that library of mine is really a re implementation of the C std.
But since there is no need to “match” the actual implementation, I’m free of implementing it the way I want, since I love Zig’s allocator as first class citizen, I’ve decided to implement this myself for my library. so I have a struct s_allocator, which is unfortunately a bulky struct containing everything each allocator needs, so I have a

  • heap_allocator (just a wrapper around malloc/free)
  • arena_allocator (a simple arena implementation)
  • logging_allocator(as in zig just a front end allocator for debug purpose)

Those allocators are used everywhere throughout my library, so even if I know they are not as good as the current existing allocators, I have no choice but to use them, as the project code must be exclusively in C, and rely on nothing but handwritten code. So while I can have build.zig for the testing suite, I also have to maintain a makefile for the project to be reviewed, and of course I can’t use any external code from zig apart for testing.

But thank you for the wrapping strategy I think I’m going to go with that.

1 Like