Heap allocation without a main function

I am just exploring Zig, so perhaps this is an obvious newbie mistake. I couldn’t find any questions resembling it, so here I am to ask:

is it possible?

As part of learning the language I thought I would test the C ABI by writing a small Zig library and calling it from Raku. It works perfectly with the basic add from the default zig init-lib!

But if I want to pass a struct, I’ve got issues. Raku’s FFI allows for definitions of classes with native representations. It expects the resultant objects to be referenced by pointer in the signatures of called C functions. So I can’t conform to the pattern of returning a struct directly, which is how I saw some Zig code doing it. And obviously we can’t hand it back a pointer to something just created on the stack.

Trying to allocate leads to the following situation:

src/main.zig:21:12: error: expected type '*main.Reality', found 'error{OutOfMemory}'
    return try allocator.create(Reality);
           ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~
src/main.zig:20:25: note: function cannot return an error
export fn get_reality() *Reality {
                        ^~~~~~~~

Here is the code:

const std = @import("std");
const testing = std.testing;

var gpa = std.heap.GeneralPurposeAllocator(.{}){};
var allocator = gpa.allocator();
const Allocator = std.mem.Allocator;

export fn add(a: i32, b: i32) i32 {
    return a + b;
}

pub const Reality = extern struct {
    verses: u32 = 128,
    fn snap(self: *Reality) void {
        self.verses = self.verses / 2;
        std.debug.print("SNAP!\n{} verses left in the multiverse!\n", .{ self.verses });
    }
};

export fn get_reality() *Reality {
     const result = try allocator.create(Reality);
     result.* = .{ verses = 128 };
     return result;
}

export fn do_snap(reality: *Reality) void {
    reality.snap();
}

export fn destroy_reality(reality: *Reality) void {
    allocator.destroy(reality);
}

test "basic add functionality" {
    try testing.expect(add(3, 7) == 10);

    const reality = get_reality();
    defer destroy_reality(reality);
    try testing.expect(reality.verses == 128);
    
    do_snap(reality);
    try testing.expect(reality.verses == 64);
}

I’ve tried a ton of variations on the above code but nothing has worked for me yet…

Finally, I just want to start by saying how impressed I am with Zig. I’ve only been playing around for a few days but it is really striking how quickly the basics can be learned. The depths of systems programming lay in wait, of course… but it’s great to finally feel a bit equipped to handle them.

Thanks in advance for your help!

2 Likes

Hi! Welcome to the community!

You can try printing or ignoring the error, instead of returning it. C ABI-compatible functions don’t allow returning Zig errors.

const result = allocator.create(Reality) catch unreachable;
3 Likes

If you can’t return the error to the caller, you have to handle the error yourself:

export fn get_reality() *Reality {
    const result = allocator.create(Reality) catch blk: {
        // Handle error. 
        // In this case, we just say that an error is impossible.
        break :blk unreachable;
    };
    result.* = .{ verses = 128 };
    return result;
}
2 Likes

Ah!! I was trying

const result = try allocator.create(Reality) catch unreachable;

Thanks for your help!

1 Like

unreachable means that the error cannot happen, which you can’t guarantee with heap allocation - if you do get an error (which is out of your control) then you will have undefined behaviour.

If you want to handle the error, either:

  • Signify the error somehow - C functions usually do this by returning NULL
  • Panic using the @panic builtin, which will guarantee a runtime crash in all build modes

Also you don’t have to yield unreachable from a block like @LucasSantos91 said. If you have an unconditional return, @panic, or unreachable in a block, the block will have the type noreturn which coerces to any other type and thus avoids type errors.

3 Likes

Thanks for this explanation, it was already obvious to me that it could not stay with unreachable beyond testing/getting things working.

Your comment makes crystal clear what is to be the next step. Running out of allocatable memory sounds like an excellent scenario to @panic in. Much appreciated.

3 Likes