Skip to content

Opaque Types

An opaque type is a type whose internal structure is hidden.

An opaque type is a type whose internal structure is hidden.

In Zig, you write an opaque type with opaque {}.

const Handle = opaque {};

This defines a type named Handle, but it does not say what fields Handle has.

You cannot create a normal Handle value directly. You also cannot access fields inside it. The whole point is that the inside is unknown.

Opaque types are mostly used through pointers.

*Handle

This means:

a pointer to a Handle

You know that a Handle exists somewhere, but you do not know its layout.

Why Opaque Types Exist

Opaque types are useful when one part of a program should not know the details of another part.

This is common in C libraries.

A C library might give you a database handle, window handle, file handle, parser handle, or graphics context. You can pass the handle around, but you are not supposed to inspect its fields.

The library owns the real data. Your code only holds a pointer to it.

In Zig, an opaque type models that clearly.

const Database = opaque {};

extern fn db_open(path: [*:0]const u8) ?*Database;
extern fn db_close(db: *Database) void;

Here, Database is opaque.

The user of the API can open a database and receive *Database.

But the user cannot do this:

db.some_field // error

There are no visible fields.

Opaque Types Hide Layout

A normal struct exposes its layout:

const User = struct {
    id: u64,
    name: []const u8,
};

Code can access the fields:

const id = user.id;
const name = user.name;

An opaque type does not expose layout:

const UserHandle = opaque {};

There is no .id.

There is no .name.

There is no known size that ordinary Zig code can use to create the value directly.

Usually, you only use pointers to opaque values:

*UserHandle
?*UserHandle
*const UserHandle

This is the same idea as an incomplete struct type in C.

A Small C-Style API

Consider a tiny imaginary C library:

typedef struct App App;

App *app_create(void);
void app_destroy(App *app);
void app_run(App *app);

The C header says App exists, but it does not reveal the fields of struct App.

In Zig, you can represent that like this:

const App = opaque {};

extern fn app_create() ?*App;
extern fn app_destroy(app: *App) void;
extern fn app_run(app: *App) void;

Then you can use it:

pub fn main() void {
    const app = app_create() orelse return;
    defer app_destroy(app);

    app_run(app);
}

This code does not know how App is stored. It only knows how to ask the external library to create, run, and destroy it.

That is exactly what we want.

Opaque Types and Ownership

Opaque types often represent resources.

A pointer to an opaque value may mean:

an open database connection
a window created by a graphics library
a parser object
a compression stream
a network session
a handle owned by another library

Because the layout is hidden, the code that receives the pointer usually cannot free the memory directly. It must call the matching destroy or close function.

For example:

extern fn parser_create() ?*Parser;
extern fn parser_destroy(parser: *Parser) void;

Correct use:

const parser = parser_create() orelse return;
defer parser_destroy(parser);

This pattern is common:

create resource
defer cleanup
use resource

The opaque type helps enforce the boundary. Your code cannot accidentally modify the internals because the internals are not visible.

Opaque Types vs Structs

Use a struct when Zig code should know the fields.

const Point = struct {
    x: f32,
    y: f32,
};

This is good because a point is simple data. Code should be able to read x and y.

Use an opaque type when Zig code should not know the fields.

const Window = opaque {};

This is good when the real window data belongs to a library or module that controls it.

A struct says:

Here is the data layout.
You may access it.

An opaque type says:

This thing exists.
You may hold a pointer to it.
You may not inspect its internals.

Opaque Types vs anyopaque

Zig also has anyopaque.

These two are related, but they are not the same.

const FileHandle = opaque {};

This creates a specific hidden type.

A *FileHandle pointer is a pointer to that specific hidden type.

By contrast:

*anyopaque

means a pointer to some unknown data of no specific type.

You can think of *anyopaque as Zig’s version of C’s void *.

Use a named opaque type when the hidden thing has a specific identity.

const Database = opaque {};
const Window = opaque {};

Now these are different types:

*Database
*Window

The compiler will not confuse them.

That is useful. A database handle and a window handle are both hidden pointers, but they are not the same kind of handle.

Example: Safer Handles

Suppose two libraries expose two different resources:

const Database = opaque {};
const Window = opaque {};

extern fn db_close(db: *Database) void;
extern fn window_close(window: *Window) void;

This is safe:

db_close(database);
window_close(window);

This is a type error:

db_close(window); // error

Even though both are pointers to hidden things, their types are different.

Named opaque types give you type safety across API boundaries.

Opaque Types Are Usually Incomplete Alone

You usually do not write code like this:

var h: Handle = undefined; // not useful

Opaque values are not meant to be stored directly by normal Zig code.

You usually store pointers:

var h: ?*Handle = null;

or receive them from functions:

const h = createHandle() orelse return;

The actual storage is created somewhere else.

That “somewhere else” might be:

a C library
an operating system API
a Zig module that hides its implementation
a custom allocator inside another subsystem

Opaque Types in Your Own APIs

Opaque types are not only for C interop. You can also use them to design Zig APIs with hidden implementation details.

For example, a module might expose this:

pub const Engine = opaque {};

pub fn create() !*Engine {
    // implementation hidden
}

pub fn destroy(engine: *Engine) void {
    // implementation hidden
}

pub fn update(engine: *Engine) void {
    // implementation hidden
}

The user can call:

const engine = try engine_lib.create();
defer engine_lib.destroy(engine);

engine_lib.update(engine);

But the user cannot access the internal fields of Engine.

This gives you freedom to change the internal layout later without changing the public API.

What Beginners Should Remember

An opaque type is for hidden data.

You usually use it through a pointer.

const Thing = opaque {};

Common pointer forms are:

*Thing
?*Thing
*const Thing

Use opaque types when the caller should know that a type exists, but should not know how it is built inside.

They are especially common when wrapping C libraries or exposing stable APIs.

The main idea is simple:

A struct reveals its fields.
An opaque type hides them.