Zig Function Pointers
A function pointer stores the address of a function. Instead of calling a function by name, you call it through a variable that holds a reference to that function. This lets you choose at runtime which function to call — the foundation of callbacks, plugin systems, dispatch tables, and event handlers.
The Concept
Normal call: add(3, 4) → compiler knows exactly which function to jump to Function pointer call: var op = add; // op holds the address of add op(3, 4) → follows the pointer, then calls add Change the pointer: op = subtract; op(3, 4) → now calls subtract instead
Function Pointer Type Syntax
*const fn(param_types) return_type Examples: *const fn(i32, i32) i32 → pointer to fn taking two i32, returning i32 *const fn([]const u8) void → pointer to fn taking a string, returning nothing *const fn() !void → pointer to fn that can fail
Basic Function Pointer
const std = @import("std");
fn add(a: i32, b: i32) i32 { return a + b; }
fn sub(a: i32, b: i32) i32 { return a - b; }
fn mul(a: i32, b: i32) i32 { return a * b; }
pub fn main() void {
const Operation = *const fn(i32, i32) i32;
var op: Operation = add;
std.debug.print("add: {d}\n", .{op(10, 3)}); // 13
op = sub;
std.debug.print("sub: {d}\n", .{op(10, 3)}); // 7
op = mul;
std.debug.print("mul: {d}\n", .{op(10, 3)}); // 30
}
op → [address of add] → call → 13 op → [address of sub] → call → 7 op → [address of mul] → call → 30
Function Pointers as Parameters — Callbacks
Passing a function pointer to another function creates a callback — the receiving function calls back into code you supply:
fn applyToAll(
data: []i32,
transform: *const fn(i32) i32,
) void {
for (data) |*item| {
item.* = transform(item.*);
}
}
fn doubleIt(x: i32) i32 { return x * 2; }
fn squareIt(x: i32) i32 { return x * x; }
pub fn main() void {
var nums = [_]i32{ 1, 2, 3, 4, 5 };
applyToAll(&nums, doubleIt);
// nums = [2, 4, 6, 8, 10]
applyToAll(&nums, squareIt);
// nums = [4, 16, 36, 64, 100]
for (nums) |n| std.debug.print("{d} ", .{n});
std.debug.print("\n", .{});
}
applyToAll with doubleIt: [1,2,3,4,5] → each item × 2 → [2,4,6,8,10] applyToAll with squareIt: [2,4,6,8,10] → each item² → [4,16,36,64,100]
Dispatch Table
An array of function pointers creates a dispatch table — a lookup structure that maps keys to behaviors without a long if-else chain:
const std = @import("std");
fn handleGet() void { std.debug.print("GET handler\n", .{}); }
fn handlePost() void { std.debug.print("POST handler\n", .{}); }
fn handleDelete() void { std.debug.print("DELETE handler\n", .{}); }
const Method = enum { GET, POST, DELETE };
const HandlerFn = *const fn() void;
const dispatch = std.EnumArray(Method, HandlerFn).init(.{
.GET = handleGet,
.POST = handlePost,
.DELETE = handleDelete,
});
pub fn main() void {
const req = Method.POST;
dispatch.get(req)(); // calls handlePost
}
dispatch table: ┌────────┬──────────────────┐ │ GET │ → handleGet() │ │ POST │ → handlePost() │ ← req=POST, calls this │ DELETE │ → handleDelete() │ └────────┴──────────────────┘
Optional Function Pointers
Function pointers can be optional — useful for optional callbacks that may or may not be registered:
const Logger = struct {
log_fn: ?*const fn([]const u8) void = null,
fn log(self: Logger, msg: []const u8) void {
if (self.log_fn) |f| f(msg);
// if no log_fn registered, silently do nothing
}
};
fn consoleLog(msg: []const u8) void {
std.debug.print("[LOG] {s}\n", .{msg});
}
pub fn main() void {
var logger = Logger{};
logger.log("This message is silently ignored.");
logger.log_fn = consoleLog;
logger.log("Now this appears."); // [LOG] Now this appears.
}
Function Pointers in Structs — Vtable Pattern
A struct of function pointers creates a vtable — the mechanism behind runtime polymorphism (similar to virtual functions in C++):
const Renderer = struct {
drawRect: *const fn(x: i32, y: i32, w: i32, h: i32) void,
drawCircle: *const fn(x: i32, y: i32, r: i32) void,
clear: *const fn() void,
};
fn terminalDrawRect(x: i32, y: i32, w: i32, h: i32) void {
std.debug.print("TERM rect({d},{d},{d},{d})\n", .{x,y,w,h});
}
fn terminalDrawCircle(x: i32, y: i32, r: i32) void {
std.debug.print("TERM circle({d},{d},{d})\n", .{x,y,r});
}
fn terminalClear() void {
std.debug.print("TERM clear\n", .{});
}
const terminal_renderer = Renderer{
.drawRect = terminalDrawRect,
.drawCircle = terminalDrawCircle,
.clear = terminalClear,
};
pub fn main() void {
const r = terminal_renderer;
r.clear();
r.drawRect(0, 0, 100, 50);
r.drawCircle(50, 25, 20);
}
anyopaque Context — Generic Callbacks
When a callback needs access to external state but the caller does not know the type of that state, pair the function pointer with a type-erased context pointer:
const Callback = struct {
ctx: *anyopaque, // type-erased context
fn_ptr: *const fn(*anyopaque, i32) void, // function pointer
fn call(self: Callback, value: i32) void {
self.fn_ptr(self.ctx, value);
}
};
This pattern — a function pointer plus a context pointer — is the basis for nearly every callback API in C, and Zig models it the same way with the added safety of explicit types.
