⚡ Zig Guide LiveUnofficial
✓ Zig 0.17.0-dev.1454+5faa79730On an older Zig?

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().