# How MulleObjC Implements Method Invocation Serialization Using NSInvocation and NSMethodSignature

> Discover how MulleObjC serializes Objective-C method invocations with NSInvocation and NSMethodSignature for deferred execution and cross-thread message passing.

- Repository: [mulle-objc/mulleobjc](https://github.com/mulle-objc/mulleobjc)
- Tags: internals
- Published: 2026-03-07

---

**MulleObjC serializes Objective-C method calls into contiguous memory buffers using NSInvocation and NSMethodSignature, enabling deferred execution, cross-thread message passing, and argument retention through MetaABI-compatible frame layouts.**

MulleObjC provides a lightweight **method invocation serialization** layer that transforms runtime message sends into storable binary frames. The `mulle-objc/mulleobjc` repository achieves this through the coordinated operation of `NSInvocation` and `NSMethodSignature`, which capture argument layouts, manage memory lifecycle, and dispatch calls through the MetaABI.

## The Serialization Architecture

The serialization pipeline relies on two core components that separate layout description from data storage.

**NSMethodSignature** defines the *structure* of the call. It calculates the **MetaABI** frame size, stores Objective-C type encodings for each argument, and provides offset information via `mulleSignatureTypeInfoAtIndex:`. According to [`src/class/NSMethodSignature.h`](https://github.com/mulle-objc/mulleobjc/blob/main/src/class/NSMethodSignature.h), this includes the natural size and alignment requirements for each parameter.

**NSInvocation** implements the *storage* mechanism. It maintains a contiguous buffer (`_storage` through `_sentinel`) that holds the raw argument bytes and return value. As implemented in `src/class/NSInvocation.m`, this buffer maps each argument to its correct offset using the signature’s layout data, enabling byte-perfect serialization of complex method calls.

## Building the Serialized Frame

The factory method `+mulleInvocationWithTarget:selector:` constructs an invocation by walking variadic arguments and packing them into the storage buffer according to signature-derived offsets.

```objc
+ (NSInvocation *) mulleInvocationWithTarget:(id)target
                                   selector:(SEL)sel, ...
{
    NSMethodSignature *signature = [target methodSignatureForSelector:sel];
    NSInvocation *inv = [self invocationWithMethodSignature:signature];
    
    NSInvocationSetTarget(inv, target);
    NSInvocationSetSelector(inv, sel);
    
    mulle_vararg_start(args, sel);
    for (i = 2; i < [signature numberOfArguments]; ++i) {
        info = [signature mulleSignatureTypeInfoAtIndex:i + 1];
        adr  = &((char *)inv->_storage)[info->invocation_offset];
        size = info->natural_size;
        src  = _mulle_vararg_aligned_struct(&args, size, info->natural_alignment);
        _mulle_objc_typeinfo_demote_value_to_natural(info, adr, src);
    }
    mulle_vararg_end(args);
    return inv;
}

```

*Lines 35‑68 of* `src/class/NSInvocation.m` *show this exact implementation*, where the code demotes larger types to their natural MetaABI size while respecting alignment constraints.

## Argument Access and Memory Management

Once serialized, arguments are accessed through `getArgument:atIndex:` and `setArgument:atIndex:`. These methods use `pointerAndSizeOfArgumentValue` to compute the exact memory location within the `_storage` buffer based on the signature’s offset table.

For cross-thread or deferred execution, **retainArguments** performs a deep copy of object parameters:

```objc
- (void)retainArguments {
    for (i = 0; i < n; ++i) {
        info = [_methodSignature mulleSignatureTypeInfoAtIndex:i+1];
        switch (*info->type) {
        case _C_RETAIN_ID:
            NSInvocationGetArgumentAtIndexWithInfo(self, &obj, i, info);
            [obj retain];
            break;
        case _C_COPY_ID:
            NSInvocationGetArgumentAtIndexWithInfo(self, &obj, i, info);
            obj = [(id<NSCopying>)obj copy];
            NSInvocationSetArgumentAtIndexWithInfo(self, &obj, i, info);
            break;
        }
    }
}

```

This logic, found in lines 79‑124 of `src/class/NSInvocation.m`, handles retained objects, copied blocks, and `strdup` for C strings. Corresponding `_releaseArguments` and `_releaseReturnValue` methods clean up these resources when the invocation is deallocated.

## Executing Serialized Invocations

The `-invokeWithTarget:` method reverses the serialization process by mapping the storage buffer back to the MetaABI calling convention. It inspects `_methodMetaABIParameterType` to determine how to unpack arguments:

```objc
- (void)invokeWithTarget:(id)target {
    pType = [_methodSignature _methodMetaABIParameterType];
    rType = [_methodSignature _methodMetaABIReturnType];

    switch (pType) {
    case MulleObjCMetaABITypeVoid:
        rval = mulle_objc_object_call_inline_variable(target, sel, target);
        break;
    case MulleObjCMetaABITypeVoidPointer:
        param = &self->_storage[info->invocation_offset];
        rval  = mulle_objc_object_call_inline_variable(target, sel, *(void **)param);
        break;
    case MulleObjCMetaABITypeParameterBlock:
        param = &self->_storage[info->invocation_offset];
        rval  = mulle_objc_object_call_inline_variable(target, sel, param);
        break;
    }

    if (rType == MulleObjCMetaABITypeVoidPointer) {
        [self setReturnValue:&rval];
    }
}

```

*See lines 124‑194 of* `src/class/NSInvocation.m` *for the full dispatch logic*. The method uses `mulle_objc_object_call_inline_variable` to perform the actual message send with the prepared argument frame.

## Invocation Pooling for Performance

To reduce heap allocation overhead, MulleObjC implements a reuse pool for standard-size invocations. The `reuseInvocations` FIFO stores objects up to `NSInvocationStandardSize` bytes.

When deallocated, an invocation checks `_isStandardInvocation`. If true, `pushStandardInvocation` returns the buffer to the pool rather than freeing memory. This optimization targets the common case of small method calls with few arguments, significantly reducing churn in high-frequency serialization scenarios.

## Summary

- **NSMethodSignature** provides the layout blueprint for argument offsets, sizes, and MetaABI types.
- **NSInvocation** maintains a contiguous `_storage` buffer that serializes arguments into a portable binary frame.
- The `mulleInvocationWithTarget:selector:` factory packs variadic arguments while respecting natural alignment and MetaABI demotion rules.
- **retainArguments** performs deep copies of objects, blocks, and strings for safe asynchronous execution.
- Execution uses **MetaABI parameter types** to reconstruct the proper calling convention before dispatching via `mulle_objc_object_call_inline_variable`.
- A built-in pooling mechanism recycles standard-size invocations to minimize allocation overhead.

## Frequently Asked Questions

### How does MulleObjC differ from Apple's NSInvocation implementation?

MulleObjC uses the **MetaABI** calling convention to standardize argument passing across platforms, whereas Apple's implementation follows platform-specific ABI rules. According to `src/class/NSInvocation.m`, MulleObjC demotes types to their natural sizes and stores arguments in a contiguous buffer with calculated offsets, while Cocoa typically stores arguments in a `void**` argument list without the same level of type normalization.

### Can NSInvocation be safely passed between threads in MulleObjC?

Yes, provided you call **retainArguments** before transferring ownership. This method, implemented in lines 79‑124 of `src/class/NSInvocation.m`, retains object references, copies blocks, and duplicates C strings to ensure the argument frame remains valid independent of the original caller's stack. The `NSThread` header in [`src/class/NSThread.h`](https://github.com/mulle-objc/mulleobjc/blob/main/src/class/NSThread.h) explicitly accepts `NSInvocation` objects for thread-start arguments, confirming cross-thread serialization support.

### What is the MetaABI and why does it matter for serialization?

The **MetaABI** is MulleObjC's abstract calling convention that defines how arguments are packed into parameter blocks. It uses three fundamental types: `MulleObjCMetaABITypeVoid` (no arguments), `MulleObjCMetaABITypeVoidPointer` (single pointer), and `MulleObjCMetaABITypeParameterBlock` (structured data). This abstraction allows `NSInvocation` to serialize calls consistently across different hardware architectures without knowing the underlying physical ABI, as seen in the `invokeWithTarget:` implementation at lines 124‑194.

### How do I extract primitive values from a serialized invocation?

Use `getArgument:atIndex:` with the correct index, remembering that indices 0 and 1 are reserved for `self` and `_cmd`. The method internally calls `pointerAndSizeOfArgumentValue` to locate the data within the storage buffer:

```objc
NSInteger value;
[inv getArgument:&value atIndex:2];  // First user argument

```

This accessor respects the alignment and size information from the method signature, ensuring proper deserialization of the raw bytes stored in the invocation frame.