How the `_param` Struct Simplifies Method Forwarding in mulle-objc

The _param struct in mulle-objc packages all method arguments into a single pointer, enabling zero-copy message forwarding by passing the same struct address to forward targets without marshaling individual parameters.

Every method implementation in the mulle-objc runtime uses a canonical calling convention that replaces variadic argument lists with a unified parameter struct. This design choice, rooted in the Meta-ABI specification, eliminates the need for runtime argument reconstruction when forwarding messages to superclass implementations.

The Canonical Method Signature and _param

In mulle-objc/mulle-objc-runtime, every method implementation adheres to a strict three-parameter prototype:

void *implementation( struct Foo *self,
                      mulle_objc_methodid_t _cmd,
                      void *_params );

The third argument, _params, is a pointer to a compiler-generated struct containing all method arguments. Rather than pushing individual arguments onto the stack or registers, the compiler packages them into a temporary struct and passes its address. This convention applies uniformly regardless of whether the method accepts zero, two, or twelve parameters.

Zero-Copy Forwarding via Parameter Structs

When a selector cannot be resolved, the runtime forwards the message to another implementation—typically the superclass's method. Because the forward target expects exactly the same prototype, the forwarding code passes the original _params pointer unchanged.

In src/mulle-objc-call.c (lines 306-311), the dispatch logic checks for missing methods and initiates forwarding:

if( ! method)  // must be forward, so check later
    return;
// forward: is not "methodid", that's what we forward to

The forward implementation then calls mulle_objc_object_call_super with the identical _params pointer:

self = (void *) mulle_objc_object_call_super( (void *)self,
                                               _cmd,
                                               _params,      // <-- reused unchanged
                                               ___Foo_init_superid );

This parameter-struct pass-through eliminates argument marshaling, preserving register state and avoiding extra stack operations during forwarding chains.

Meta-ABI and Struct Generation

The Meta-ABI defines that methods with multiple parameters or floating-point values use a single pointer to a parameter struct. This abstraction is implemented in src/mulle-metaabi.h, which provides macros for generating these structs:

/* if you need to "manually" call a MetaABI function with a _param block
 * use mulle_metaabi_union to generate it. DO NOT CALL IT `_param` */

Callers use mulle_metaabi_struct to create properly aligned parameter blocks at the call site:

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

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

Implementation Details

Dispatch and Forwarding Logic in src/mulle-objc-call.c

The core dispatch mechanism recognizes when a method is missing and triggers the forward path. Because the _params pointer is opaque to the dispatcher, the same forwarding logic works for methods with any signature complexity. The file handles the transition from normal dispatch to mulle_objc_object_call_super without inspecting the struct contents.

Signature Analysis in src/mulle-objc-signature.c

At line 96, the signature parser determines when a method requires a parameter struct based on the current architecture's ABI requirements. The comment explaining "_param structs" indicates that this decision happens during method registration, ensuring the compiler and runtime agree on the calling convention.

Method Body Implementation

Method implementations unpack arguments from the struct directly. In test/demo/demo1.c (lines 301-308), a method receiving two integers extracts them from the _params struct:

static void Foo_setA_b_( struct Foo *self,
                         mulle_objc_methodid_t _cmd,
                         struct { int _a; int _b; } *_params )
{
    int a = _params->_a;   // read from the passed-in struct
    int b = _params->_b;
    self->a = a;
    self->b = b;
}

Similarly, initializers forward to super using the same pointer (lines 285-293):

static void *Foo_init( struct Foo *self,
                       mulle_objc_methodid_t _cmd,
                       void *_params )
{
    /* forward to super, re-using the same _params pointer */
    self = (void *) mulle_objc_object_call_super( (void *)self,
                                                  _cmd,
                                                  _params,
                                                  ___Foo_init_superid );

    self->a = 1;
    self->b = 2;
    return self;
}

Performance and Design Benefits

  • Single-Pointer ABI: All complex argument lists collapse to a void *, providing a stable calling convention across method signatures.
  • Fast Forwarding: The forwarder reuses the original _params without rebuilding argument lists, avoiding expensive stack frame reconstruction.
  • Cross-Platform Abstraction: The Meta-ABI shields generated code from architecture-specific differences between ARM and x86 calling conventions.
  • Cleaner Generated Code: Compiler-generated struct literals make method bodies concise and type-safe while maintaining C-level performance.

Summary

  • The _param struct packages all method arguments into a single pointer passed as the third parameter to every mulle-objc method.
  • Zero-copy forwarding is achieved by passing the identical _params pointer to mulle_objc_object_call_super without unpacking or remarshaling arguments.
  • The Meta-ABI specification in src/mulle-metaabi.h defines how compilers generate these structs for methods with multiple parameters or floating-point values.
  • Signature analysis in src/mulle-objc-signature.c determines when parameter structs are required based on platform ABI rules.
  • Method implementations in test/demo/demo1.c demonstrate unpacking arguments from the struct and forwarding to superclass implementations.

Frequently Asked Questions

What is the Meta-ABI in mulle-objc?

The Meta-ABI is a calling convention specification that requires methods with more than one parameter or floating-point arguments to package those values into a struct passed by pointer. This abstraction ensures a uniform binary interface across different hardware architectures, allowing the runtime to handle complex method signatures with a single void * parameter.

Why does mulle-objc use a struct for method parameters instead of individual arguments?

Using a parameter struct eliminates variadic calling conventions and reduces the dispatch surface area to a consistent three-parameter signature (self, _cmd, _params). This design simplifies the compiler's code generation, enables trivial message forwarding by pointer passing, and abstracts away platform-specific register and stack allocation rules.

How does method forwarding work without copying arguments?

When a method is not found, the runtime calls mulle_objc_object_call_super (or a generic forward handler) with the original _params pointer received by the initial dispatch. Because both the original method slot and the forward target expect the same struct pointer type, the runtime does not need to inspect, copy, or transform the argument data, resulting in zero-overhead forwarding.

Where is the _param struct defined in the source code?

The macros for generating parameter structs are defined in src/mulle-metaabi.h, while the logic determining when such structs are required lives in src/mulle-objc-signature.c. Concrete examples of methods receiving these structs appear in test/demo/demo1.c, and the dispatch logic that handles forwarding is implemented in src/mulle-objc-call.c.

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 →