# How to Debug and Trace Runtime Calls in mulle-objc-runtime

> Debug and trace runtime calls in mulle-objc-runtime using MULLE_OBJC_TRACE_* environment variables. Gain insights without recompilation.

- Repository: [mulle-objc/mulle-objc-runtime](https://github.com/mulle-objc/mulle-objc-runtime)
- Tags: how-to-guide
- Published: 2026-03-07

---

**The mulle-objc-runtime provides a built-in tracing subsystem controlled via `MULLE_OBJC_TRACE_*` environment variables that outputs detailed runtime activity to stderr without requiring recompilation.**

The ability to debug and trace runtime calls is essential when diagnosing method dispatch issues or analyzing class loading behavior in Objective-C applications. The mulle-objc-runtime repository includes a comprehensive tracing infrastructure that logs universe initialization, method lookups, and individual call invocations through a centralized logging system. This guide explains how to activate and interpret these diagnostic features using environment variables, source code hooks, and debugger integration.

## Core Tracing Architecture

### The Universe Debug Structure

All tracing state lives inside the **`_mulle_objc_universe`** structure defined in **[`src/mulle-objc-universe.h`](https://github.com/mulle-objc/mulle-objc-runtime/blob/main/src/mulle-objc-universe.h)**. This universe object holds the entire runtime state, including a nested `debug` sub-structure that stores every trace flag. When you set an environment variable like `MULLE_OBJC_TRACE_METHOD_CALL`, the initialization code in `mulle_objc_universe_init` populates these bitfields so that subsequent runtime operations can check them quickly.

### Central Trace Functions

Every diagnostic line flows through **`mulle_objc_universe_trace`** (and its non-newline variant `mulle_objc_universe_trace_nolf`) implemented in **[`src/mulle-objc-universe.c`](https://github.com/mulle-objc/mulle-objc-runtime/blob/main/src/mulle-objc-universe.c)**. These functions handle the actual formatting and output to `stderr`.

Before printing the user message, the runtime invokes **`mulle_objc_universe_trace_preamble`**, which emits the thread ID (`t:#%2lu`) and, if enabled, a high-resolution timestamp. This preamble makes it possible to correlate events when debugging concurrent applications.

## Enabling Runtime Tracing via Environment Variables

During `mulle_objc_universe_init` (around line 360 in **[`src/mulle-objc-universe.c`](https://github.com/mulle-objc/mulle-objc-runtime/blob/main/src/mulle-objc-universe.c)**), the runtime scans the environment using `getenv_yes_no` and `mulle_objc_environment_get_int`. Any non-empty value evaluates to "true" for Boolean flags.

| Variable | Purpose | Example |
|----------|---------|---------|
| `MULLE_OBJC_TRACE_UNIVERSE` | Log universe creation and finalization events | `1` |
| `MULLE_OBJC_TRACE_CLASS_ADD` | Emit a line when a class is registered | `1` |
| `MULLE_OBJC_TRACE_METHOD_CALL` | Trace every method entry and exit (verbose) | `1` |
| `MULLE_OBJC_TRACE_METHOD_SEARCH` | Show method lookup attempts | `1` |
| `MULLE_OBJC_TRACE_THREAD` | Prefix each line with the thread identifier | `1` |
| `MULLE_OBJC_TRACE_TIMESTAMP` | Add a timestamp to each line | `1` |
| `MULLE_OBJC_TRACE_INSTANCE` | Control instance-level tracing granularity (0-2) | `2` |
| `MULLE_OBJC_WARN_HANG` | Force the process to spin in `mulle_objc_hang()` for debugger attachment | `1` |
| `MULLE_OBJC_WARN_CRASH` | Abort immediately on fatal errors instead of recovering | `1` |

## How Tracing Works Internally

When you launch a program with tracing enabled, the following sequence occurs:

1. **Setup** – `mulle_objc_universe_init` parses the environment and populates the `universe->debug` bitfields. Boolean flags use `getenv_yes_no`, while integer flags like `MULLE_OBJC_TRACE_INSTANCE` use `mulle_objc_environment_get_int`.

2. **Preamble Generation** – Every call to `mulle_objc_universe_trace` first invokes `mulle_objc_universe_trace_preamble`. It prints the thread ID (`t:#%2lu`) and, if `debug.trace.timestamp` is set, a value derived from `time(NULL)`.

3. **Message Formatting** – The trace function accepts a `printf`-style format string and arguments, builds the final message, and writes it to `stderr`.

4. **Method Call Path** – When `debug.method_call` contains `MULLE_OBJC_UNIVERSE_CALL_TRACE_BIT`, the dispatch path in **[`src/mulle-objc-call.c`](https://github.com/mulle-objc/mulle-objc-runtime/blob/main/src/mulle-objc-call.c)** (line 134) inserts a trace line before the actual IMP is invoked. The same flag disables caching in **[`src/mulle-objc-fastmethodtable.c`](https://github.com/mulle-objc/mulle-objc-runtime/blob/main/src/mulle-objc-fastmethodtable.c)** (line 67) to avoid hiding the call behind a fast-path.

## Practical Debugging Examples

### Selective Tracing from the Shell

To monitor class registration and method lookups while identifying which thread performs each action:

```bash
export MULLE_OBJC_TRACE_CLASS_ADD=1
export MULLE_OBJC_TRACE_METHOD_SEARCH=1
export MULLE_OBJC_TRACE_THREAD=1

./my-program

```

**Typical output:**

```

t:# 1 mulle_objc_universe traceuniverse "init begin"

t:# 2 mulle_objc_universe traceclass_add "+class_add %08lx \"MyClass\""

t:# 2 mulle_objc_universe tracemethod_search "lookup method %s on class %08lx"

```

### Attaching GDB to a Hanging Process

When you need to inspect the runtime state before it executes any significant logic, use the hang mechanism:

```bash
export MULLE_OBJC_WARN_HANG=1
./my-program

```

The process prints `Hanging for debugger to attach` and spins in `mulle_objc_hang()` defined in [`src/mulle-objc-universe.c`](https://github.com/mulle-objc/mulle-objc-runtime/blob/main/src/mulle-objc-universe.c). In another terminal:

```bash
gdb -p $(pgrep my-program)
(gdb) break mulle_objc_universe_trace
(gdb) continue

```

Every trace line now triggers a breakpoint, allowing you to inspect the full universe state with `p *universe`.

### Programmatic Trace Control

If you need to toggle tracing after the universe has initialized, modify the debug structure directly:

```c
#include "mulle-objc-universe.h"

int main(void)
{
    struct _mulle_objc_universe *universe = mulle_objc_universe_create(NULL);
    
    // Enable thread-ID and timestamp tracing at runtime
    universe->debug.trace.thread = 1;
    universe->debug.trace.timestamp = 1;
    
    // ... normal program execution ...
    
    mulle_objc_universe_destroy(universe);
    return 0;
}

```

## Key Source Files for Debugging

| File | Relevance to Tracing |
|------|---------------------|
| **[[`src/mulle-objc-universe.c`](https://github.com/mulle-objc/mulle-objc-runtime/blob/main/src/mulle-objc-universe.c)](https://github.com/mulle-objc/mulle-objc-runtime/blob/master/src/mulle-objc-universe.c)** | Contains `mulle_objc_universe_trace`, `mulle_objc_universe_trace_preamble`, environment parsing, and hang/abort helpers. |
| **[[`src/mulle-objc-universe.h`](https://github.com/mulle-objc/mulle-objc-runtime/blob/main/src/mulle-objc-universe.h)](https://github.com/mulle-objc/mulle-objc-runtime/blob/master/src/mulle-objc-universe.h)** | Defines the `_mulle_objc_universe` structure and the `debug` sub-structure that holds all trace flags. |
| **[[`src/mulle-objc-call.c`](https://github.com/mulle-objc/mulle-objc-runtime/blob/main/src/mulle-objc-call.c)](https://github.com/mulle-objc/mulle-objc-runtime/blob/master/src/mulle-objc-call.c)** | Implements method invocation tracing at line 134 when `MULLE_OBJC_UNIVERSE_CALL_TRACE_BIT` is active. |
| **[[`src/mulle-objc-class-impcache.c`](https://github.com/mulle-objc/mulle-objc-runtime/blob/main/src/mulle-objc-class-impcache.c)](https://github.com/mulle-objc/mulle-objc-runtime/blob/master/src/mulle-objc-class-impcache.c)** | Shows how method-cache lookups are conditionally traced based on debug flags. |
| **[[`src/mulle-objc-fastmethodtable.c`](https://github.com/mulle-objc/mulle-objc-runtime/blob/main/src/mulle-objc-fastmethodtable.c)](https://github.com/mulle-objc/mulle-objc-runtime/blob/master/src/mulle-objc-fastmethodtable.c)** | Demonstrates the fast-path shortcut that is disabled at line 67 during method-call tracing to ensure visibility. |
| **[[`test-debugger/20_gdb/simulate-gdb/gdb.c`](https://github.com/mulle-objc/mulle-objc-runtime/blob/main/test-debugger/20_gdb/simulate-gdb/gdb.c)](https://github.com/mulle-objc/mulle-objc-runtime/blob/master/test-debugger/20_gdb/simulate-gdb/gdb.c)** | Provides a minimal example of forcing a hang for debugger attachment. |

## Summary

- **Environment-based configuration**: Set `MULLE_OBJC_TRACE_*` variables before launch to enable specific tracing domains without recompiling.
- **Centralized logging**: All diagnostic output flows through `mulle_objc_universe_trace` in [`src/mulle-objc-universe.c`](https://github.com/mulle-objc/mulle-objc-runtime/blob/main/src/mulle-objc-universe.c), ensuring consistent formatting with optional thread IDs and timestamps.
- **Method call visibility**: When `MULLE_OBJC_TRACE_METHOD_CALL` is active, the runtime disables the fast-path method cache in [`src/mulle-objc-fastmethodtable.c`](https://github.com/mulle-objc/mulle-objc-runtime/blob/main/src/mulle-objc-fastmethodtable.c) to log every dispatch through [`src/mulle-objc-call.c`](https://github.com/mulle-objc/mulle-objc-runtime/blob/main/src/mulle-objc-call.c).
- **Debugger integration**: Use `MULLE_OBJC_WARN_HANG=1` to pause execution in `mulle_objc_hang()` for GDB attachment, or modify `universe->debug.trace` fields programmatically for runtime control.

## Frequently Asked Questions

### Can I enable tracing after the program has already started?

No. According to the source code in [`src/mulle-objc-universe.c`](https://github.com/mulle-objc/mulle-objc-runtime/blob/main/src/mulle-objc-universe.c), trace flags are read once during `mulle_objc_universe_init` from environment variables. Once the universe is initialized, you cannot toggle tracing via environment variables without restarting the process. However, you can programmatically modify `universe->debug.trace` fields at runtime as shown in the Programmatic Trace Control example above.

### Why does method call tracing slow down my application significantly?

Enabling `MULLE_OBJC_TRACE_METHOD_CALL` significantly impacts performance because it disables the fast-path method cache. As implemented in [`src/mulle-objc-fastmethodtable.c`](https://github.com/mulle-objc/mulle-objc-runtime/blob/main/src/mulle-objc-fastmethodtable.c) at line 67, when the trace bit is set, the runtime bypasses the cached IMP table to ensure every call is logged through `mulle_objc_universe_trace` in [`src/mulle-objc-call.c`](https://github.com/mulle-objc/mulle-objc-runtime/blob/main/src/mulle-objc-call.c). Use this flag only for targeted debugging sessions, not in production.

### Where does the trace output go by default?

All trace output is written to **stderr** via the `mulle_objc_universe_trace` and `mulle_objc_universe_trace_nolf` functions defined in [`src/mulle-objc-universe.c`](https://github.com/mulle-objc/mulle-objc-runtime/blob/main/src/mulle-objc-universe.c). The output format includes an optional thread ID prefix (when `MULLE_OBJC_TRACE_THREAD` is set) and optional timestamp, followed by the formatted message. You can redirect stderr to a file using standard shell redirection (`2> trace.log`) for later analysis.

### How can I make the runtime wait for a debugger attachment?

Set the environment variable `MULLE_OBJC_WARN_HANG=1` before launching your program. This triggers `mulle_objc_hang()` early in the universe initialization sequence (see [`src/mulle-objc-universe.c`](https://github.com/mulle-objc/mulle-objc-runtime/blob/main/src/mulle-objc-universe.c)), causing the process to spin in a visible loop printing "Hanging for debugger to attach". You can then attach GDB or LLDB, set breakpoints (e.g., `break mulle_objc_universe_trace`), and continue execution.