Functions

Functions in Odin are clean, powerful, and explicitly designed to cut down on unnecessary boilerplate. Below is a structured reference guide covering standard usage, procedure overloading, return tricks, and practical tips tailored for engine and graphics development.


📌 Quick-Reference Syntax Summary

Feature Syntax Example Common Use Case
Basic Function my_func :: proc(a: int) -> int Standard procedures
Multiple Returns proc() -> (Value, bool) Error checking without exceptions
Explicit Overloads draw :: proc{ draw_v2, draw_v3 } Unified API names (Raylib style)
C Interop Callback callback :: proc "c" () Vulkan/GLFW native function pointers
Defer Execution defer cleanup() Running teardown code at scope exit
Inlining fast_math :: #force_inline proc() Hot-path performance loops

🔑 Core Syntax Quick Reference

1. Basic Declaration & Multiple Returns

Odin functions are defined using the proc keyword. Functions can return multiple values, which completely eliminates the need for temporary output structs or out-pointers.

package main

import "core:fmt"

// Standard function signature
add :: proc(a: int, b: int) -> int {
    return a + b
}

// Multiple return values (very common for error handling/status checks)
divide :: proc(a, b: f32) -> (f32, bool) {
    if b == 0.0 do return 0.0, false
    return a / b, true
}

main :: proc() {
    result, ok := divide(10.0, 2.0)
    if ok {
        fmt.println("Result:", result)
    }
}

2. Named Return Values

You can name the return variables directly in the function declaration. They act as local variables inside the function body.

// Named returns auto-initialize to zero/default values
get_bounds :: proc(size: f32) -> (min_val: f32, max_val: f32) {
    min_val = -size / 2.0
    max_val =  size / 2.0
    return // Naked return automatically passes min_val and max_val back
}

âš¡ Essential Features & Tips

Unlike C++, Odin does not support implicit function overloading based on argument signatures. Instead, overloading is explicit. You write individual functions and group them together into a single polymorphic identifier using proc().

Why this matters for your engine: This allows you to expose clean Raylib-style functions like draw_cube() that accept different parameter sets (e.g., passing a Vector3 position vs. raw x, y, z floats) while keeping code strongly typed under the hood.

Vec3 :: [3]f32

draw_cube_vec :: proc(pos: Vec3, size: Vec3) {
    // Render using vectors
}

draw_cube_raw :: proc(x, y, z: f32, size: f32) {
    // Render using raw floats
}

// Explicit overload group
draw_cube :: proc{
    draw_cube_vec,
    draw_cube_raw,
}

// Usage:
// draw_cube({0, 1, 0}, {1, 1, 1})
// draw_cube(0.0, 1.0, 0.0, 1.0)

💡 Tip 2: Use Default Arguments for Clean API Surfaces

You can assign default values to parameters. When callers omit an argument, Odin automatically supplies the default value. Combine this with Named Arguments at the call site for incredible clarity.

init_window :: proc(
    title: string = "Odin Game Engine",
    width: i32 = 1280,
    height: i32 = 720,
    fullscreen: bool = false,
) {
    // Window creation logic...
}

main :: proc() {
    // Use all defaults
    init_window()

    // Override only specific fields using named parameters
    init_window(title = "Custom Title", fullscreen = true)
}

💡 Tip 3: Pass Arrays and Large Structs Efficiently (#by_ptr)

In Odin, parameters are passed by value by default. Large structures passed to functions are copied unless passed via a pointer (^T) or designated with directives.

VulkanMesh :: struct {
    vertices: [10000]f32,
    indices:  [15000]u32,
}

// Passed by pointer because we plan to update internal state
update_mesh :: proc(mesh: ^VulkanMesh) {
    mesh.vertices[0] = 1.0
}

// Passed `#by_ptr` for high performance reading (no copy cost, read-only inside body)
render_mesh :: proc(#by_ptr mesh: VulkanMesh) {
    // mesh.vertices[0] = 1.0 // ERROR: read-only!
}

💡 Tip 4: Leverage Context (context)

Odin has a built-in implicit context passed silently to every procedure. It holds crucial implicit environment data, including:

  1. Allocator: Default dynamic memory allocation system (context.allocator).

  2. Logger: System logging callback (context.logger).

If you're writing Vulkan initialization helpers, you can set custom allocators or loggers at the root function, and nested functions will inherit them automatically.

import "core:log"

custom_vulkan_loader :: proc() {
    // Uses whatever logger was set in the current execution context!
    log.info("Loading Vulkan Vulkan extensions...")
}

main :: proc() {
    // Attach a standard console logger to the current thread's context
    context.logger = log.create_console_logger()
    
    custom_vulkan_loader() // Log message automatically formats and prints
}

💡 Tip 5: Contextless Procedures (proc "c")

When interfacing directly with C libraries or writing low-level callbacks (like Vulkan debug callbacks or GLFW window handlers), Odin must not expect its implicit context parameter to be passed.

Mark C-compatible callbacks with proc "c" or proc "system".

import vk "vendor:vulkan"

// Vulkan debug messenger callback must be contextless
vulkan_debug_callback :: proc "system" (
    messageSeverity: vk.DebugUtilsMessageSeverityFlagsEXT,
    messageTypes: vk.DebugUtilsMessageTypeFlagsEXT,
    pCallbackData: ^vk.DebugUtilsMessengerCallbackDataEXT,
    pUserData: rawptr,
) -> b32 {
    
    // To use Odin's logging inside a C callback, reconstitute the context:
    context = runtime_default_context()
    
    // Now you can log or process normally
    return vk.FALSE
}