Skip to content

A Mental Model

A calling convention defines how functions communicate at the machine level.

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 rules

ABI means:

Application Binary Interface

An 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 C

Different platforms use different rules.

Example:

PlatformCommon ABI
Linux x86_64System V ABI
Windows x86_64Microsoft x64 ABI
ARM64AArch64 ABI

Zig abstracts this complexity for you.

Stack Cleanup

Some calling conventions specify:

caller cleans stack

Others specify:

callee cleans stack

Historically 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:

ConventionPurpose
.CC ABI
.Inlineinline calling behavior
.Nakedno generated prologue/epilogue
.Asyncasync 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 B

They 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 survive

Otherwise 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) i32

No 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 70

Both functions behave similarly at the source-code level.

The difference is the machine-level ABI contract.

Real-World Importance

Calling conventions matter heavily in:

AreaWhy
Operating systemsinterrupt and syscall handling
C interoperabilityABI compatibility
Game enginesplugin APIs
Embedded systemshardware interfaces
Dynamic librariesbinary linking
Compilerscode generation
Virtual machinesexecution models

This is core systems-programming knowledge.

Mental Model

A calling convention is:

a machine-level agreement for how functions communicate

It 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.