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

> Explore mulle-objc-runtime vs Apple Objective-C Runtime architectural differences. Discover how ID-centric universes, hash tables, and Meta-ABI calling conventions boost performance.

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

---

**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`](https://github.com/mulle-objc/mulle-objc-runtime/blob/main/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`](https://github.com/mulle-objc/mulle-objc-runtime/blob/main/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`](https://github.com/mulle-objc/mulle-objc-runtime/blob/main/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`](https://github.com/mulle-objc/mulle-objc-runtime/blob/main/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`](https://github.com/mulle-objc/mulle-objc-runtime/blob/main/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`](https://github.com/mulle-objc/mulle-objc-runtime/blob/main/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`](https://github.com/mulle-objc/mulle-objc-runtime/blob/main/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`](https://github.com/mulle-objc/mulle-objc-runtime/blob/main/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`](https://github.com/mulle-objc/mulle-objc-runtime/blob/main/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`](https://github.com/mulle-objc/mulle-objc-runtime/blob/main/book/chapter8-call-mechanism.md) lines 90-104 and declared in [`src/mulle-objc-call.h`](https://github.com/mulle-objc/mulle-objc-runtime/blob/main/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`](https://github.com/mulle-objc/mulle-objc-runtime/blob/main/dox/super/SUPER.md) lines 79-86, eliminates the need for compiler-generated trampolines.

## Practical Implementation Examples

### Converting Selector Strings to Integer IDs

```c
#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`](https://github.com/mulle-objc/mulle-objc-runtime/blob/main/book/chapter8-call-mechanism.md) lines 22-28.

### Hash Table Method Lookup

```c
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`](https://github.com/mulle-objc/mulle-objc-runtime/blob/main/src/mulle-objc-universe.h) interfaces.

### Using the Meta-ABI Parameter Struct

```c
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`](https://github.com/mulle-objc/mulle-objc-runtime/blob/main/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`](https://github.com/mulle-objc/mulle-objc-runtime/blob/main/book/chapter8-call-mechanism.md) lines 84-98.

### Fast Inline Method Calls

```c
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`](https://github.com/mulle-objc/mulle-objc-runtime/blob/main/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.