How MulleObjC Implements Method Invocation Serialization Using NSInvocation and NSMethodSignature
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, 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.
+ (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:
- (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:
- (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
_storagebuffer 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 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:
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.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →