Skip to content
Chung Leong edited this page Apr 24, 2024 · 4 revisions

An opaque is a notional type in the Zig language. It's used to create a pointer that points to "mystery" data, whose meaning is only known to the code that created it:

const std = @import("std");

const Context = struct {
    number1: i32,
    number2: i32,
    number3: i32,
};
const OpaquePtr = *align(@alignOf(Context)) opaque {};

pub fn startContext(allocator: std.mem.Allocator) !OpaquePtr {
    const ctx = try allocator.create(Context);
    ctx.* = .{ .number1 = 10, .number2 = 20, .number3 = 30 };
    return @ptrCast(ctx);
}

pub fn showContext(opaque_ptr: OpaquePtr) void {
    const ctx: *Context = @ptrCast(opaque_ptr);
    std.debug.print("{any}\n", .{ctx.*});
}
import { showContext, startContext } from './opaque-example-1.zig';

const ctx = startContext();
console.log(ctx.valueOf());
showContext(ctx);
{}
opaque-example-1.Context{ .number1 = 10, .number2 = 20, .number3 = 30 }

Casting to an opaque pointer is a way of hiding implementation details.

Potential pitfall

The example above uses an allocator provided by Zigar, which obtains memory from the JavaScript engine. This memory is garbage-collected. It also gets moved around (for instance, when a buffer graduates from V8's young-generation heap to its old-generation heap). You cannot place pointers to relocatable memory into a struct then hide its implementation behind an opaque pointer. Zigar must know of their existance in order to update them.

The following code demonstrates the problem:

const std = @import("std");

const Context = struct {
    string: []const u8,
};
const OpaquePtr = *align(@alignOf(Context)) opaque {};

pub fn startContext(allocator: std.mem.Allocator) !OpaquePtr {
    const ctx = try allocator.create(Context);
    ctx.string = try allocator.dupe(u8, "This is a test");
    return @ptrCast(ctx);
}

pub fn showContext(opaque_ptr: OpaquePtr) void {
    const ctx: *Context = @ptrCast(opaque_ptr);
    std.debug.print("{s}\n", .{ctx.string});
}
import { showContext, startContext } from './opaque-example-2.zig';

const ctx = startContext();
gc(); // force immediate garbage collection; requires --expose-gc
showContext(ctx);
�B,��Ʊ/4

Without the call to gc, the code would actually produce the expected output. As garbage collection happens at unpredictable intervals, this could lead to the most insideous of bugs--ones that only pop up on rare occasions.

You need to use your own allocator if you're going to keep pointers hidden behind an opaque pointer. You'll also need to provide a clean-up function.