Function Declaration
Functions in Zig are declared with the fn keyword:
const std = @import("std");
const expect = std.testing.expect;
// Functions are declared like this
fn add(a: i8, b: i8) i8 {
if (a == 0) {
return b;
}
return a + b;
}
test "function" {
try expect(add(5, 6) == 11);
}
Function parameters are immutable by default. To modify a parameter, pass it as a pointer.
Parameters and Return Values
Basic Parameters
fn multiply(a: i32, b: i32) i32 {
return a * b;
}
Multiple Return Values
Zig doesn’t have built-in tuple returns, but you can return structs:
const Result = struct {
quotient: i32,
remainder: i32,
};
fn divMod(a: i32, b: i32) Result {
return .{
.quotient = @divTrunc(a, b),
.remainder = @rem(a, b),
};
}
Void Returns
Functions that don’t return a value use void:
fn printMessage(msg: []const u8) void {
std.debug.print("{s}\n", .{msg});
}
Function Visibility
Public Functions
Use pub to make functions visible when importing:
// The pub specifier allows the function to be visible when importing.
// Another file can use @import and call sub2
pub fn sub2(a: i8, b: i8) i8 {
return a - b;
}
Private Functions
Functions without pub are private to the file:
fn internalHelper() void {
// Only visible within this file
}
pub fn publicFunction() void {
// Accessible from other files
}
fn privateFunction() void {
// Only accessible within this file
}
Calling Conventions
Export Functions
The export specifier makes a function externally visible with C ABI:
// The export specifier makes a function externally visible in the generated
// object file, and makes it use the C ABI.
export fn sub(a: i8, b: i8) i8 {
return a - b;
}
Extern Functions
The extern specifier declares functions resolved at link time:
// The extern specifier is used to declare a function that will be resolved
// at link time, when linking statically, or at runtime, when linking
// dynamically.
extern "kernel32" fn ExitProcess(exit_code: u32) callconv(.winapi) noreturn;
extern "c" fn atan2(a: f64, b: f64) f64;
Inline Functions
// The inline calling convention forces a function to be inlined at all call sites.
// If the function cannot be inlined, it is a compile-time error.
inline fn shiftLeftOne(a: u32) u32 {
return a << 1;
}
Naked Functions
// The naked calling convention makes a function not have any function
// prologue or epilogue. This can be useful when integrating with assembly.
fn _start() callconv(.naked) noreturn {
abort();
}
Naked functions should only be used for low-level programming and assembly integration.
Function Pointers
Function pointers are prefixed with *const:
const Call2Op = *const fn (a: i8, b: i8) i8;
fn doOp(fnCall: Call2Op, op1: i8, op2: i8) i8 {
return fnCall(op1, op2);
}
fn add(a: i8, b: i8) i8 {
return a + b;
}
fn sub(a: i8, b: i8) i8 {
return a - b;
}
test "function pointers" {
try expect(doOp(add, 5, 6) == 11);
try expect(doOp(sub, 5, 6) == -1);
}
Function pointers enable callbacks, strategy patterns, and other higher-order programming techniques.
Special Return Types
NoReturn
Functions that never return use noreturn:
fn abort() noreturn {
@branchHint(.cold);
while (true) {}
}
Error Unions
Functions can return error unions:
fn parseU64(buf: []const u8, radix: u8) !u64 {
var x: u64 = 0;
for (buf) |c| {
const digit = charToDigit(c);
if (digit >= radix) {
return error.InvalidChar;
}
// ... parsing logic
}
return x;
}
Generic Functions
Functions can be generic by accepting type parameters:
fn List(comptime T: type) type {
return struct {
items: []T,
len: usize,
};
}
// Usage
var buffer: [10]i32 = undefined;
var list = List(i32){
.items = &buffer,
.len = 0,
};
Generic functions in Zig are evaluated at compile-time, providing zero-cost abstractions.
Method Syntax
Zig supports method-style calls through the first parameter:
const Vec3 = struct {
x: f32,
y: f32,
z: f32,
pub fn init(x: f32, y: f32, z: f32) Vec3 {
return Vec3{
.x = x,
.y = y,
.z = z,
};
}
pub fn dot(self: Vec3, other: Vec3) f32 {
return self.x * other.x + self.y * other.y + self.z * other.z;
}
};
test "dot product" {
const v1 = Vec3.init(1.0, 0.0, 0.0);
const v2 = Vec3.init(0.0, 1.0, 0.0);
// Method syntax
try expect(v1.dot(v2) == 0.0);
// Equivalent function call
try expect(Vec3.dot(v1, v2) == 0.0);
}
Branch Hints
You can provide hints to the optimizer:
fn abort() noreturn {
@branchHint(.cold); // Tell optimizer this is rarely executed
while (true) {}
}
fn fastPath(condition: bool) void {
if (condition) {
@branchHint(.likely);
// frequently executed code
}
}