Opaque
const std = @import("std");
const expect = std.testing.expect;
// Typically this stands in for a C type you were never given a definition
// for: `typedef struct Window Window;`
const Window = opaque {
// Opaque types can still have methods.
pub fn close(self: *Window) void {
_ = self;
}
};
const Handle = opaque {};
test "opaque types are only usable behind a pointer" {
// `var w: Window = ...` is impossible: the size is unknown. Only
// pointers to it exist, which is exactly how C handles are used.
var storage: u32 = 0;
const handle: *Handle = @ptrCast(&storage);
try expect(@TypeOf(handle) == *Handle);
}
test "distinct opaque types do not mix" {
// *Window and *Handle are unrelated types, so the compiler prevents
// passing one where the other is expected, unlike C's void*.
try expect(*Window != *Handle);
}An opaque {} type has unknown size and layout. You can never have one by
value, only a pointer to one.
Why you would want that
This is the Zig spelling of C’s incomplete type:
typedef struct Window Window; /* definition not provided */
Window *create_window(void);
The library hands you a *Window and you may only pass it back. You cannot
inspect it, copy it, or put it on the stack, because its size is genuinely not
part of the public interface.
Better than void*
C’s usual alternative is void*, which erases the type entirely. Nothing stops
you passing a *File where a *Window was expected. Distinct opaque types stay
distinct:
*Window != *Handle // different types, checked by the compiler
You keep the “you may not look inside” property without giving up type safety
at the boundary. Opaque types can also have methods, so the handle still reads
like an object: window.close().