context


Context

The context is a struct that is implicitly passed from procedure to procedure as your program runs. It contains fields that control common cross cutting operations such as memory allocation, logging, assertion handling, and temporary storage. Every procedure in Jai has access to the context through the keyword context, unless the procedure is marked #no_context or #c_call.

The context establishes shared conventions so that independently written modules can coordinate without needing custom APIs for memory allocation, logging, or other common facilities. By changing the context before calling a procedure, the caller can control how that procedure (and everything it calls) performs these operations, even if the caller did not write that code and does not want to modify it.

    
bare_procedure :: () {
    print("The context is: %\n", context);
}
    

Context Base

The context begins with the fields defined in Context_Base, which is declared in the Runtime_Support module. These fields provide the default facilities available to every Jai program:

FieldPurpose
allocator The memory allocator used by alloc, New, and other allocation routines. Contains a procedure and a data pointer.
logger / logger_data The procedure called by log to emit messages, along with an associated data pointer.
log_level The current log severity level.
log_source_identifier A u64 identifier that can be set to tag log output from a particular source.
assertion_failed The procedure called when an assertion fails.
temporary_storage A pointer to the current thread's temporary storage arena.
stack_trace A pointer to the head of the current stack trace linked list.
thread_index A u32 identifying the current thread. Each thread receives its own independent context.
default_allocator The program's original allocator, preserved so that custom allocators can fall back to it.

Reading and Writing the Context

The context behaves like any other struct instance. You can read its fields with dot notation and assign to them directly. When you modify the context inside a procedure, the change is visible to that procedure's callees and persists after they return. Because the context is shared mutable state, changes also remain visible to the caller after the procedure returns:

    
proc_a :: () {
    print("before: %\n", context.silly_number);  // 33
    proc_b();
    print("after:  %\n", context.silly_number);  // 34, the change leaked back.
}

proc_b :: () {
    context.silly_number += 1;
}
    

This upward flow is sometimes intentional (for example, passing error state up through many layers without requiring exception handling), but often it is not desirable. To prevent changes from leaking, use push_context.

push_context

push_context replaces the active context for the duration of a block. When the block ends, the previous context is restored automatically. This prevents modifications made inside the block from leaking to the caller.

To use push_context, copy the current context into a local variable, modify whatever fields you need, and then enter the block:

    
wrapper :: () {
    print("before: %\n", context.silly_number);  // 33

    new_context := context;
    new_context.silly_number += 1;

    push_context new_context {
        print("inside: %\n", context.silly_number);  // 34
        some_procedure();  // Also sees 34, and any further changes stay inside.
    }

    print("after: %\n", context.silly_number);   // 33, restored.
}
    

You can also write push_context with no argument to push a default initialized context. This is especially useful inside #c_call procedures that need to call back into normal Jai code:

    
push_context {
    // A default context is active here.
    print("Hello from inside a default context.\n");
}
    

Memory Allocation

The standard allocation procedure alloc obtains memory by calling context.allocator. Because the allocator lives in the context, you can replace it for any subtree of your call graph without modifying the source code of the procedures in that subtree.

An allocator is a single procedure that handles allocation, reallocation, and freeing, selected by the mode parameter of type Allocator_Mode. Packaging all operations into one procedure means you swap them all at once, which prevents mismatches between the allocator used to obtain memory and the one used to free it.

    
my_allocator_proc :: (mode: Allocator_Mode, size: s64, old_size: s64, old_memory_pointer: *void, proc_data: *void) -> *void {
    if mode == .ALLOCATE && size == 42 {
        print("Hello, Sailor! 42 byte allocation requested!\n");
    }
    return context.default_allocator.proc(mode, size, old_size, old_memory_pointer, proc_data);
}

install_example :: () {
    new_context := context;
    new_context.allocator.proc = my_allocator_proc;
    new_context.allocator.data = null;

    push_context new_context {
        // Everything called here uses my_allocator_proc.
        mem := alloc(42);  // Prints the greeting.
        free(mem);
    }

    // Outside the block, the original allocator is active again.
    mem := alloc(42);  // No greeting.
    free(mem);
}
    

Long lived data structures such as resizable arrays and hash tables store the allocator that was active when they were first used. They call that saved allocator consistently afterward, rather than using whatever context.allocator happens to be at the moment. This ensures that memory is always freed with the same allocator that allocated it.

push_allocator

push_allocator is a macro that changes only the allocator in the current context for the current scope. When the scope exits, the previous allocator is restored. It is a lighter weight alternative to push_context when only the allocator needs to change.

    
{
    push_allocator(temp);
    // Allocations in this scope use temporary storage.
    s := copy_string("ephemeral");
}
// Previous allocator is restored here.
    

Logging

The log procedure from the Basic module formats a message and sends it to context.logger. Its signature is:

    
log :: (format_string: string, args: .. Any, loc := #caller_location, flags := Log_Flags.NONE, user_flags: u32 = 0);
    

To redirect log output, replace context.logger and optionally context.logger_data in a push_context block, exactly as you would replace the allocator. This makes it straightforward to, for example, send logging from one subsystem to a file and logging from another subsystem to the console, or to silence logging entirely in a release build unless a special flag is set.

    
my_logger :: (message: string, data: *void, info: Log_Info) {
    // Write to a file, filter by level, etc.
}

new_context := context;
new_context.logger = my_logger;
new_context.logger_data = null;

push_context new_context {
    log("This message is routed to my_logger.\n");
}
    

Temporary Storage

Temporary storage is a per thread arena allocator accessible through context.temporary_storage. It provides fast allocation for short lived data. Memory obtained from temporary storage does not need to be freed individually; instead, the arena is reset in bulk, typically once per frame or once per task. Procedures such as tprint allocate from temporary storage instead of the heap.

    
print("temporary storage: %\n", context.temporary_storage.*);
// Shows fields: data, size, occupied, high_water_mark, overflow_allocator, etc.
    

Stack Trace

The stack trace is a cross platform linked list of Stack_Trace_Node values that records the active call chain at any point during execution. It is accessible through context.stack_trace. Each node contains the procedure name, source file, line number, call depth, and a hash. The stack trace is maintained automatically when the stack_trace build option is enabled (which is the default).

You can walk the stack trace at any time by following the next pointers:

    
node := context.stack_trace;
while node {
    if node.info {
        print("[%] at %:%\n", node.info.name, node.info.location.fully_pathed_filename, node.line_number);
    }
    node = node.next;
}
    

The Basic module provides print_stack_trace for convenience. To save a stack trace for later inspection, use pack_stack_trace, which copies the trace into a standalone data structure. The address of the current procedure is available through context.stack_trace.info.procedure_address.

Stack traces are useful for writing instrumentation code such as profilers and memory debuggers. When the build option is enabled, every procedure call emits code to push a Stack_Trace_Node onto the linked list and pop it on return.


Assertion Handler

The assertion_failed field in the context is the procedure called when assert from the Basic module detects a false condition. By replacing this field, you can customize how assertion failures are reported or handled, for example by logging to a file, triggering a debugger break, or performing cleanup before aborting.


#add_context

The #add_context directive adds a new field declaration to the Context struct. The declaration is written exactly as it would appear inside a struct body. Any file in the program, including modules, can use #add_context, and the field becomes accessible through the context keyword everywhere. Because each thread has its own context, fields added with #add_context are effectively thread local without requiring any special thread local storage mechanism.

    
#add_context silly_number: int = 33;

example :: () {
    print("%\n", context.silly_number);  // 33
    context.silly_number = 99;
    print("%\n", context.silly_number);  // 99
}
    

You are not limited to simple types; any data type can be added. However, there is potential for naming conflicts: if two modules or a module and the main program add a field with the same name, the program will not compile.


Print Style

The context contains a print_style field that holds the default format settings used by print. It includes formatters for integers, floats, structs, arrays, and pointers. By modifying these formatters inside a push_context block, you can change how values are displayed without passing explicit format arguments to every print call.

    
NUMBERS :: u32.[1, 69105, 1491625];

new_context := context;
push_context new_context {
    format_int := *context.print_style.default_format_int;

    for NUMBERS  print("% ", it);   // 1 69105 1491625
    print("\n");

    format_int.base = 16;
    for NUMBERS  print("% ", it);   // 1 10df1 16c2a9
    print("\n");

    format_int.base = 2;
    for NUMBERS  print("% ", it);   // 1 10000110111110001 101101100001010101001
    print("\n");
}

// Outside the block, the default base 10 formatting is restored.
for NUMBERS  print("% ", it);       // 1 69105 1491625
    

Thread Index

Each thread has its own context, and context.thread_index is a u32 that uniquely identifies the thread. Because the context is per thread, any field added with #add_context is automatically thread local, and accessing it does not require paying the cost of a thread local storage (TLS) lookup.


#no_context

A procedure marked #no_context does not receive or have access to the context. Such procedures cannot call any procedure that requires a context unless they first establish one with push_context. Many low level procedures in the Preload module are marked #no_context because they do not need it.

#c_call

A procedure marked #c_call uses the C calling convention and does not receive a context. This is necessary for procedures that are registered as callbacks with C libraries. To call a normal Jai procedure from inside a #c_call procedure, you must push a context first:

    
callback :: (x: int) -> int #c_call {
    // print("...", x);  // Error: no context available.

    push_context {
        print("callback received %\n", x);  // Works: default context is active.
    }

    return x * x;
}
    

Summary of Replaceable Facilities

The context provides a uniform mechanism for controlling operations that, in other languages, each require their own library specific API. The following table summarizes the main replaceable fields:

FacilityContext Field(s)Used By
Memory allocation allocator.proc, allocator.data alloc, New, free, resizable arrays
Logging logger, logger_data log
Assertion handling assertion_failed assert
Temporary allocation temporary_storage tprint, talloc, push_allocator(temp)
Print formatting print_style print