mulle-objc-runtime vs Apple Objective-C Runtime: Architecture and Calling Convention Differences

mulle-objc-runtime replaces Apple's pointer-centric global runtime with an ID-centric universe architecture that supports multiple isolated runtimes per process, uses hash tables for method lookup instead of linked lists, and implements a Meta-ABI calling convention that packs complex parameters into structs for greater flexibility and performance.

The mulle-objc/mulle-objc-runtime project reimplements the Objective-C runtime from the ground up to eliminate global locks and enable multiple runtimes within a single process. While maintaining source-level compatibility with Objective-C syntax, it fundamentally rearchitects how classes, selectors, and message dispatch are implemented compared to Apple's Objective-C runtime.

Architecture Differences

Universe-Based Runtime vs Single Global Runtime

Apple's Objective-C runtime relies on a single global runtime instance that exists for the entire process lifetime. In contrast, mulle-objc-runtime centers on the universe (struct _mulle_objc_universe defined in src/mulle-objc-universe.h), which can be instantiated per-thread or per-process. This design allows multiple isolated runtimes to coexist within the same application, each maintaining separate class hierarchies and selector namespaces.

Integer-Based Identity for Classes and Selectors

Rather than using pointers to structures (Class, SEL), mulle-objc-runtime identifies both classes and selectors as integer IDs (mulle_objc_classid_t and mulle_objc_methodid_t). The runtime maintains a global string-to-ID map via functions like mulle_objc_methodid_from_string() and mulle_objc_universe_lookup_methodname(), ensuring that identical selector strings always resolve to the same ID across the universe. This approach eliminates pointer indirection and enables more compact data structures.

Object Layout: The Prefixed isa Pointer

Apple's runtime stores the isa pointer inside the instance memory itself. mulle-objc-runtime inverts this model: the isa pointer is prefixed to the instance, allocated immediately before the object data begins. As documented in README.md lines 88-90, this separation allows the runtime to manage object headers independently of instance variables.

Hash Table Method Dispatch

Instead of storing methods in linked lists that require linear traversal or cache-dependent lookups, mulle-objc-runtime embeds a hash table directly into the class structure. The runtime maps selector IDs to method implementations (IMPs) through this hash table, enabling direct consultation without walking method lists. This design is detailed in book/chapter8-call-mechanism.md lines 33-36.

Lock-Free Thread Safety

Apple's runtime requires a global lock for many operations, including class registration. mulle-objc-runtime eliminates this bottleneck by using no global lock except during code loading, as stated in README.md lines 93-95. Normal message dispatch and class manipulation use per-universe locks or lock-free algorithms, significantly improving concurrency performance.

Multiple Runtime Support

While Apple's runtime permits only one runtime per process, mulle-objc-runtime supports multiple universes with different class hierarchies coexisting simultaneously. This capability, documented in dox/API_UNIVERSE.md lines 12-13, enables applications to load otherwise incompatible Objective-C libraries side-by-side without symbol conflicts.

Calling Convention Differences

The Meta-ABI vs objc_msgSend

Apple's objc_msgSend follows the platform ABI, passing self and _cmd in registers with remaining arguments on the stack or in registers according to the architecture. mulle-objc-runtime introduces the Meta-ABI, a flexible calling convention described in book/chapter8-call-mechanism.md lines 71-80. Simple methods with single register-sized arguments pass parameters directly, while complex calls use a generated parameter struct.

Parameter Packing for Complex Signatures

When a method accepts multiple arguments or any floating-point values (float or double), the Meta-ABI packs these into a generated struct using the mulle_metaabi_struct() macro defined in src/mulle-metaabi.h. The runtime passes a single pointer to this struct rather than multiple arguments. This approach, shown in book/chapter8-call-mechanism.md lines 84-97, abstracts away platform-specific register allocation complexities.

Return Value Optimization

Complex struct returns in Apple's runtime follow the platform ABI's hidden pointer mechanism. mulle-objc-runtime handles these through the mulle_metaabi_struct_void_return() macro, which generates a return-struct pointer for complex return types, as illustrated in book/chapter8-call-mechanism.md lines 98-102.

Optimized Dispatch Mechanisms

Unlike Apple's uniform objc_msgSend path, mulle-objc-runtime provides multiple dispatch variants. The mulle_objc_object_call_inline() function, documented in book/chapter8-call-mechanism.md lines 90-104 and declared in src/mulle-objc-call.h, allows the compiler to generate direct function calls when the method implementation is known at compile time, bypassing the hash table lookup entirely for the common case of single-parameter methods.

Super Call Implementation

Apple's super calls rely on compiler-generated structs that encode the target superclass and selector. mulle-objc-runtime implements super calls through mulle_objc_object_call_classid(), which accepts a class ID parameter to bypass the current class and jump directly to the superclass implementation. This runtime-based approach, contrasted with Apple's implementation in dox/super/SUPER.md lines 79-86, eliminates the need for compiler-generated trampolines.

Practical Implementation Examples

Converting Selector Strings to Integer IDs

#include <mulle-objc-runtime/mulle-objc-runtime.h>

int main(void)
{
    mulle_objc_methodid_t sel = mulle_objc_methodid_from_string("description");
    const char *name = mulle_objc_universe_lookup_methodname(
                           mulle_objc_global_get_defaultuniverse(),
                           sel);
    printf("Selector ID: %llu → %s\n",
           (unsigned long long)sel, name);
    return 0;
}

This example demonstrates the selector ID system using mulle_objc_methodid_from_string() and the reverse lookup via mulle_objc_universe_lookup_methodname(), as shown in book/chapter8-call-mechanism.md lines 22-28.

Hash Table Method Lookup

void *lookup_method_implementation(const char *class_name,
                                   const char *method_name)
{
    struct _mulle_objc_universe *u = mulle_objc_global_get_defaultuniverse();
    struct _mulle_objc_infraclass *infra =
        mulle_objc_universe_lookup_infraclass_nofail(
            u, mulle_objc_classid_from_string(class_name));

    struct _mulle_objc_method *method =
        mulle_objc_class_search_method_nofail(&infra->base,
                     mulle_objc_methodid_from_string(method_name));
    return method ? method->function_pointer : NULL;
}

This function retrieves method implementations through the hash table using mulle_objc_class_search_method_nofail(), defined in the class hierarchy accessed via src/mulle-objc-universe.h interfaces.

Using the Meta-ABI Parameter Struct

mulle_metaabi_struct(complex_params) {
    char *name;
    double value;
    int   count;
    struct _mulle_objc_object *other;
};

void demo_complex_call(void *obj)
{
    mulle_metaabi_struct(complex_params) p = {
        .name  = "demo",
        .value = 3.14,
        .count = 7,
        .other = obj
    };
    mulle_objc_object_call(obj,
        mulle_objc_methodid_from_string("processComplex:"), &p);
}

The mulle_metaabi_struct() macro, defined in src/mulle-metaabi.h, generates a packed parameter struct for methods with multiple arguments or floating-point parameters, as detailed in book/chapter8-call-mechanism.md lines 84-98.

Fast Inline Method Calls

char *fast_inline(id obj)
{
    mulle_objc_methodid_t sel = mulle_objc_methodid_from_string("description");
    return mulle_objc_object_call_inline(obj, sel, NULL);
}

When the method signature is known at compile time, mulle_objc_object_call_inline() bypasses the standard dispatch machinery for direct invocation, as illustrated in book/chapter8-call-mechanism.md lines 90-104.

Summary

  • Universe architecture enables multiple isolated runtimes per process through struct _mulle_objc_universe, replacing Apple's single global runtime.
  • Integer-based identifiers for classes (mulle_objc_classid_t) and selectors (mulle_objc_methodid_t) replace pointer-based Class and SEL types.
  • Prefixed isa pointers separate object headers from instance data, unlike Apple's inline isa storage.
  • Hash table dispatch provides direct method lookup by selector ID rather than traversing linked method lists.
  • Lock-free operation eliminates the global runtime lock required by Apple's implementation, using only per-universe synchronization.
  • Meta-ABI calling convention abstracts parameter passing through generated structs for complex signatures, supporting both direct register passing and packed struct modes.
  • Optimized dispatch paths including mulle_objc_object_call_inline() allow compile-time direct calls where Apple's runtime always routes through objc_msgSend.

Frequently Asked Questions

Can mulle-objc-runtime run existing Objective-C code without modification?

Yes, mulle-objc-runtime maintains source-level compatibility with Objective-C syntax. Existing code compiles against the runtime's headers, though it links against the mulle runtime library rather than Apple's libobjc.dylib. The runtime implements the same messaging semantics and object model, but underlying mechanics like selector lookup and method dispatch use the ID-centric architecture.

Why does mulle-objc use integer IDs instead of pointers for selectors?

Integer IDs (mulle_objc_methodid_t) provide a compact, comparable value that remains consistent across multiple universes and library boundaries. Unlike pointers, which vary between process invocations due to ASLR and memory layout, selector IDs derived from string hashes create a stable identity system. This enables the hash table dispatch mechanism and supports multiple runtime universes coexisting without pointer conflicts.

How does the Meta-ABI improve performance over objc_msgSend?

The Meta-ABI allows the compiler to generate direct function calls for simple methods (single register-sized arguments) while automatically packing complex parameters into structs when necessary. Functions like mulle_objc_object_call_inline() eliminate the indirection of objc_msgSend for known methods. Additionally, the hash table lookup in mulle-objc-runtime avoids the cache-miss penalties associated with Apple's method list traversal.

Is it possible to use both Apple and mulle runtimes in the same application?

Direct interoperability between the two runtimes is not supported because they use incompatible object layouts (prefixed vs inline isa) and calling conventions. However, mulle-objc-runtime's support for multiple universes allows an application to load separate libraries that might otherwise conflict, provided they are compiled for and linked against the mulle runtime. Each universe maintains isolated class hierarchies, preventing symbol collisions between different mulle-objc libraries.

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 →