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

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. 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. 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), 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 (line 134) inserts a trace line before the actual IMP is invoked. The same flag disables caching in 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:

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:

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. In another terminal:

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:

#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/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/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/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/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/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/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, 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 to log every dispatch through 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, 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 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. 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. 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), 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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →