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!

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;

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;
}

Ah!! I was trying

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

Thanks for your help!

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.

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.