Calling Conventions
A calling convention defines how functions communicate at the machine level.
When one function calls another, many low-level details must be agreed upon:
- where arguments are stored
- where return values go
- which registers are preserved
- how the stack is used
- how the function returns
The calling convention defines these rules.
Without calling conventions, separately compiled code could not safely call each other.
Calling conventions are extremely important in:
- operating systems
- C interoperability
- assembly programming
- embedded systems
- dynamic libraries
- foreign function interfaces
Most beginners do not think about them initially because compilers handle the details automatically. But systems programming requires understanding them.
A Mental Model
Suppose function A calls function B.
Both sides must agree on rules like:
where is parameter 1 stored?
where is parameter 2 stored?
where is the return value placed?
who cleans up the stack?The calling convention answers these questions.
Default Zig Calling Convention
Normally, Zig chooses the default calling convention automatically.
Example:
fn add(a: i32, b: i32) i32 {
return a + b;
}Most of the time, this is what you want.
The compiler selects the best convention for the current target platform.
Explicit Calling Conventions
You can specify a calling convention explicitly.
Example:
fn add(
a: i32,
b: i32,
) callconv(.C) i32 {
return a + b;
}This uses the C calling convention.
The syntax:
callconv(.C)means:
use C ABI rulesABI means:
Application Binary InterfaceAn ABI defines low-level compatibility between compiled programs.
Why the C Calling Convention Matters
C is the universal systems language.
Many libraries expose C APIs.
If Zig wants to call C safely, both sides must agree on:
- stack layout
- parameter passing
- return conventions
Example:
extern fn printf(
format: [*:0]const u8,
...
) c_int;This uses the C calling convention automatically because it is an external C function.
Without matching conventions, calls would corrupt memory or crash.
Example: Calling C Functions
const std = @import("std");
extern fn puts(
str: [*:0]const u8,
) c_int;
pub fn main() void {
_ = puts("Hello from C");
}Here Zig calls the C standard library.
The ABI compatibility is critical.
Machine-Level Parameter Passing
Modern CPUs have registers.
Calling conventions define which registers store arguments.
Example conceptually:
argument 1 -> register A
argument 2 -> register B
return value -> register CDifferent platforms use different rules.
Example:
| Platform | Common ABI |
|---|---|
| Linux x86_64 | System V ABI |
| Windows x86_64 | Microsoft x64 ABI |
| ARM64 | AArch64 ABI |
Zig abstracts this complexity for you.
Stack Cleanup
Some calling conventions specify:
caller cleans stackOthers specify:
callee cleans stackHistorically this mattered heavily in 32-bit systems.
Modern 64-bit ABIs are more standardized.
Variadic Functions
Some C functions accept variable numbers of arguments.
Example:
printf("%d %d", 10, 20);Zig supports this with C calling conventions.
Example:
extern fn printf(
format: [*:0]const u8,
...
) c_int;The ... means variadic arguments.
Variadic functions require special ABI handling.
Zig Calling Convention Enum
Zig exposes calling conventions through enum values.
Examples include:
| Convention | Purpose |
|---|---|
.C | C ABI |
.Inline | inline calling behavior |
.Naked | no generated prologue/epilogue |
.Async | async execution support |
Platform-specific conventions may also exist.
Naked Functions
A naked function disables normal function setup code.
Example conceptually:
fn handler() callconv(.Naked) void {
}Normally, compilers generate setup instructions automatically:
- stack adjustment
- register saving
- frame creation
Naked functions skip this.
This is extremely low-level and dangerous.
They are mainly used for:
- interrupt handlers
- bootloaders
- kernels
- assembly integration
Inline Calling Convention
Zig internally supports inline calling behavior.
Conceptually:
callconv(.Inline)This relates to compile-time inlining behavior.
Normally you use the inline keyword instead.
Async Calling Convention
Async functions may use specialized conventions internally.
Example conceptually:
callconv(.Async)Async execution requires special stack/state handling.
You will study async systems later.
Exported Functions
When Zig exposes functions to external programs, ABI compatibility matters.
Example:
export fn add(
a: i32,
b: i32,
) i32 {
return a + b;
}This creates a symbol visible to external code.
Usually exported APIs use:
callconv(.C)to ensure compatibility.
Function Pointer Compatibility
Function pointers must match calling conventions too.
Example:
const Callback =
*const fn(i32) callconv(.C) void;A mismatched convention can corrupt execution.
The compiler checks compatibility carefully.
Why Calling Conventions Exist
Different CPUs and operating systems evolved differently.
Calling conventions standardized communication between separately compiled code.
Without them:
library A could not safely call library BThey are foundational to binary compatibility.
Assembly Interaction
Calling conventions are critical when working with assembly.
Example conceptually:
assembly code must know:
- where arguments are
- where return values go
- which registers must surviveOtherwise execution breaks immediately.
Debuggers and Stack Traces
Calling conventions also affect:
- debuggers
- profilers
- stack traces
- exception systems
The debugger needs to understand stack layout rules.
Why Beginners Rarely Notice Them
Most code uses default conventions automatically.
Example:
fn add(a: i32, b: i32) i32No explicit convention needed.
The compiler handles everything.
But systems programmers eventually must understand what happens underneath.
A Practical Example
const std = @import("std");
fn zigAdd(
a: i32,
b: i32,
) i32 {
return a + b;
}
fn cAdd(
a: i32,
b: i32,
) callconv(.C) i32 {
return a + b;
}
pub fn main() void {
const x = zigAdd(10, 20);
const y = cAdd(30, 40);
std.debug.print(
"{} {}\n",
.{ x, y },
);
}Output:
30 70Both functions behave similarly at the source-code level.
The difference is the machine-level ABI contract.
Real-World Importance
Calling conventions matter heavily in:
| Area | Why |
|---|---|
| Operating systems | interrupt and syscall handling |
| C interoperability | ABI compatibility |
| Game engines | plugin APIs |
| Embedded systems | hardware interfaces |
| Dynamic libraries | binary linking |
| Compilers | code generation |
| Virtual machines | execution models |
This is core systems-programming knowledge.
Mental Model
A calling convention is:
a machine-level agreement for how functions communicateIt defines rules such as:
- where parameters go
- where results go
- how the stack behaves
- which registers are preserved
Most of the time, Zig handles these details automatically.
But when interacting with:
- C
- assembly
- operating systems
- low-level runtimes
understanding calling conventions becomes essential.