动手实现 NSInvocation(下)

Mike Ash Friday Q&A 中文译文:动手实现 NSInvocation(下)

作者 TommyWu
封面圖片: 动手实现 NSInvocation(下)

译文 · 原文: Friday Q&A 2013-03-22: Let's Build NSInvocation, Part II · 作者 Mike Ash

原文:https://www.mikeash.com/pyblog/friday-qa-2013-03-22-lets-build-nsinvocation-part-ii.html 发布:2013-03-22 作者:Mike Ash 译者:MiMo(mimo-v2.5-pro);代码块保留英文原样


上期的 Friday Q & A 中,我开始将部分 NSInvocation 重新实现为 MAInvocation。在那篇文章中,我讨论了基础理论、架构调用约定,并展示了实现所需的汇编语言粘合代码。今天,我将介绍 MAInvocation 的 Objective-C 部分。

回顾
MAInvocation 是我对 NSInvocation 大部分内容的重新实现。为简化起见,它不支持浮点参数或返回值,也不支持结构体参数。它仅支持 x86-64 架构。代码已在 GitHub 上发布:

https://github.com/mikeash/MAInvocation

函数的前六个参数通过六个寄存器传递:rdirsirdxrcxr8r9。后续参数(如果有)通过栈传递。返回值存储在 rax 中。对于双元素结构体的特殊情况,第二个元素返回在 rdx 中。更大的结构体则由调用方分配内存,然后该内存的指针会作为隐式第一个参数传入 rdi,所有显式参数的序号依次后移。这在 Objective-C 中称为 stret 调用(译注:即 structure-return 调用,用于返回大型结构体)。

汇编语言粘合代码用于在结构体中存储的值与实际的函数调用之间进行转换。该结构体包含所有相关的寄存器(registers),以及一个指向栈参数(stack arguments)的指针,此外还有一些额外的数据:

struct RawArguments
{
void *fptr;
uint64_t rdi;
uint64_t rsi;
uint64_t rdx;
uint64_t rcx;
uint64_t r8;
uint64_t r9;
uint64_t stackArgsCount;
uint64_t *stackArgs;
uint64_t rax_ret;
uint64_t rdx_ret;
uint64_t isStretCall;
};

MAInvocationCall 函数是用汇编语言编写的,在前一篇文章中已探讨过。其函数原型如下:

void MAInvocationCall(struct RawArguments *);

Objective-C 代码可以向 RawArguments 结构体中填入一个函数指针以及相应的寄存器值,然后调用此函数。该函数将执行实际调用,并在返回时,结构体中的两个返回值寄存器字段将被函数返回的结果填充。

此外,还有两个转发处理函数:

void MAInvocationForward(void);
void MAInvocationForwardStret(void);

这些设计旨在被任意 Objective-C 方法调用所触发。它们都会创建一个新的 RawArguments 结构体,填充参数寄存器和栈参数指针,然后调用名为 MAInvocationForwardC 的 C 函数。当该函数返回后,这些处理程序会将 rax_retrdx_ret 的值传递回调用方。这两个处理程序之间的唯一区别在于它们将 isStretCall 设置为 0 还是 1。

至此,MAInvocation 的 Objective-C 实现阶段已准备就绪。

接口 MAInvocation 的接口与 NSInvocation 相同:

@interface MAInvocation : NSObject
+ (MAInvocation *)invocationWithMethodSignature:(NSMethodSignature *)sig;
- (NSMethodSignature *)methodSignature;
- (void)retainArguments;
- (BOOL)argumentsRetained;
- (id)target;
- (void)setTarget:(id)target;
- (SEL)selector;
- (void)setSelector:(SEL)selector;
- (void)getReturnValue:(void *)retLoc;
- (void)setReturnValue:(void *)retLoc;
- (void)getArgument:(void *)argumentLocation atIndex:(NSInteger)idx;
- (void)setArgument:(void *)argumentLocation atIndex:(NSInteger)idx;
- (void)invoke;
- (void)invokeWithTarget:(id)target;
@end

实例变量 方法签名是调用对象的核心。它描述了方法接受多少个参数,以及参数的类型是什么。这是处理方法参数和返回值所需的关键信息。这就是为什么创建 MAInvocation 的唯一方式是传入一个 NSMethodSignature,并且该签名被存储在一个实例变量中:

@implementation MAInvocation {
NSMethodSignature *_sig;

调用对象内部维护了一个本地结构体 RawArguments。在设置或获取参数和返回值时,会直接操作该结构体。当调用被触发时,该实例变量的指针可以直接传递给汇编胶水代码(assembly language glue):

struct RawArguments _raw;

NSInvocation(调用对象)可以保留其参数。这会对所有对象类型的参数发送 retain 消息,同时也会复制 C 字符串类型的参数。由于需要追踪参数当前是否已被保留,以便能正确释放,并能对新设置的参数进行保留,因此需要一个标志位来记录此状态:

BOOL _argumentsRetained;

最后,需要一个缓冲区来存储 stret 调用(结构体返回调用)的返回值:

void *_stretBuffer;
}

初始化
工厂方法只是调用一个初始化方法:

+ (NSInvocation *)invocationWithMethodSignature: (NSMethodSignature *)sig
{
return [[[self alloc] initWithMethodSignature: sig] autorelease];
}

init 方法会保存方法签名:

- (id)initWithMethodSignature: (NSMethodSignature *)sig
{
if((self = [super init]))
{
_sig = [sig retain];

接着,它通过检查方法签名来确定是否满足结构体返回调用(stret call)的要求,并据此填充 RawArguments 结构体的 isStretCall 字段。这一过程是通过调用另一个方法实现的。该方法的代码相当复杂,将在后续展示:

_raw.isStretCall = [self isStretReturn];

接下来是设置栈参数。首先要从方法签名中获取参数总数:

NSUInteger argsCount = [sig numberOfArguments];

请注意,这个计数包含了两个隐式参数 self_cmd,因此该数字恰好等于实际传递的函数参数数量。

如果这是一个 stret 调用(结构体返回调用),那么实际上会多出一个参数,因为用于传递返回值的隐式指针会通过 rdi 寄存器传入:

if(_raw.isStretCall)
argsCount++;

如果参数超过六个(可能包含隐式的 stret 参数),那么就会出现栈参数。此时 stackArgsCount 会被设置为剩余参数的数量,并且会分配内存以便 stackArgs 能够容纳这些参数:

if(argsCount > 6)
{
_raw.stackArgsCount = argsCount - 6;
_raw.stackArgs = calloc(argsCount - 6, sizeof(*_raw.stackArgs));
}
}
return self;
}

Wrapper Methods(包装方法)

在 API 中有几个方法仅仅是其他方法的简单包装。在我们深入探讨核心内容之前,我会在这里介绍它们。

target方法只是以略微便捷的方式获取第一个参数的值。该方法只是对getArgument:atIndex:方法的小封装:

- (id)target
{
id target;
[self getArgument: &target atIndex: 0];
return target;
}

setTarget:方法是setArgument:atIndex:方法的一个更简单的包装器:

- (void)setTarget: (id)target
{
[self setArgument: &target atIndex: 0];
}

selectorsetSelector: 方法几乎完全相同,只是操作的是第二个参数:

- (SEL)selector
{
SEL sel;
[self getArgument: &sel atIndex: 1];
return sel;
}
- (void)setSelector: (SEL)selector
{
[self setArgument: &selector atIndex: 1];
}

最后,invoke 方法会调用 invokeWithTarget:,并传入 [self target]

- (void)invoke
{
[self invokeWithTarget: [self target]];
}

获取参数(Getting Arguments)

为了从调用对象(invocation)中获取一个参数,代码首先需要知道某个参数被存储在哪里。这个小的包装方法处理了这个问题:

- (uint64_t *)argumentPointerAtIndex: (NSInteger)idx
{
uint64_t *ptr = NULL;
if(idx == 0)
ptr = &_raw.rdi;
if(idx == 1)
ptr = &_raw.rsi;
if(idx == 2)
ptr = &_raw.rdx;
if(idx == 3)
ptr = &_raw.rcx;
if(idx == 4)
ptr = &_raw.r8;
if(idx == 5)
ptr = &_raw.r9;
if(idx >= 6)
ptr = _raw.stackArgs + idx - 6;
return ptr;
}

这个方法接受一个原始参数索引,即已经根据是否为 stret call(结构体返回调用)进行过调整的索引。它会将该索引映射到相应的寄存器或栈槽位。

为了能够复制正确字节数的小于 8 字节的参数,获取特定参数的大小也很实用。这个方法封装了 Foundation 函数 NSGetSizeAndAlignment,该函数接收一个 Objective-C 类型字符串并返回该类型的大小(以及对齐方式):

- (NSUInteger)sizeOfType: (const char *)type
{
NSUInteger size;
NSGetSizeAndAlignment(type, &size, NULL);
return size;
}

围绕此方法的另一个小型封装提供了特定参数的大小:

- (NSUInteger)sizeAtIndex: (NSInteger)idx
{
return [self sizeOfType: [_sig getArgumentTypeAtIndex: idx]];
}

为了实际获取一个参数,该方法首先会调整所请求的索引,以考虑到可能的 stret return(结构体返回值的特殊处理)情况:

- (void)getArgument: (void *)argumentLocation atIndex: (NSInteger)idx
{
NSInteger rawArgumentIndex = idx;
if(_raw.isStretCall)
rawArgumentIndex++;

接下来,它从上述方法中获取指针,并对其有效性进行检查:

uint64_t *src = [self argumentPointerAtIndex: rawArgumentIndex];
assert(src);

接着它获取参数大小,并从参数位置复制相应数量的字节:

NSUInteger size = [self sizeAtIndex: idx];
memcpy(argumentLocation, src, size);
}

获取和设置返回值

要获取和设置返回值,需要知道该值的大小。这很容易获取,只需从方法签名(method signature)获得返回类型的大小即可:

- (NSUInteger)returnValueSize
{
return [self sizeOfType: [_sig methodReturnType]];
}

还需要获取一个指向返回值存储位置的指针。如果本次调用是 stret call(结构体返回调用),那么它将返回 _stretBuffer。若该缓冲区尚未分配空间,则会进行分配:

- (void *)returnValuePtr
{
if(_raw.isStretCall)
{
if(_stretBuffer == NULL)
_stretBuffer = calloc(1, [self returnValueSize]);
return _stretBuffer;
}

对于常规调用,它只返回原始参数结构中 rax_ret 字段的地址:

else
{
return &_raw.rax_ret;
}
}

这部分处理了返回值同时使用两个返回寄存器的情况。由于这两个寄存器在结构体中是连续存放的,只要将足够大的数据复制到该地址,就能同时写入结构体中的两个寄存器字段。

有了这些方法,编写获取和设置返回值的方法就很简单了。它们只需要调用 memcpy 函数,传入计算得到的大小和指针即可:

- (void)getReturnValue: (void *)retLoc
{
NSUInteger size = [self returnValueSize];
memcpy(retLoc, [self returnValuePtr], size);
}
- (void)setReturnValue: (void *)retLoc
{
NSUInteger size = [self returnValueSize];
memcpy([self returnValuePtr], retLoc, size);
}

类型分类
确定某个方法的返回类型是否需要进行 stret call(结构体返回调用),需要依据 x86-64 调用约定对该类型进行分类。NSInvocation API 允许保留(retain)调用中的参数,这也需要对参数类型进行分类,以便找出所有对象类型。

不同的分类结果会被放入一个枚举(enum)中,该枚举结合了 x86-64 ABI 的相关部分与保留参数所需的区分条件。这最终归结为以下类型:对象、块(blocks)、C 字符串、其他整数类型(包括非对象指针)、包含两个整数的结构体、空结构体、其他任何结构体,以及任何其他尚未涵盖的类型:

enum TypeClassification
{
TypeObject,
TypeBlock,
TypeCString,
TypeInteger,
TypeTwoIntegers,
TypeEmptyStruct,
TypeStruct,
TypeOther
};

分类过程本身包含两个互递归(mutually-recursive)的方法:一个用于对任意类型进行分类,另一个则专门用于分类结构体类型。

通用方法首先通过 @encode 指令为 id、块(blocks)和 C 字符串创建类型字符串:

- (enum TypeClassification)classifyType: (const char *)type
{
const char *idType = @encode(id);
const char *blockType = @encode(void (^)(void));
const char *charPtrType = @encode(char *);

请注意,就 @encode 而言,所有 block 都具有相同的类型字符串,因此此处对 block 类型的选择完全是任意的。

有了这些,它会将类型与它们进行比较,如果匹配则返回相应的枚举值:

if(strcmp(type, idType) == 0)
return TypeObject;
if(strcmp(type, blockType) == 0)
return TypeBlock;
if(strcmp(type, charPtrType) == 0)
return TypeCString;

接下来,它检查整数类型(integer types)。这段复杂的代码构造了一个包含所有整数类型字符的 C 字符串,再加上函数指针(function pointers)(其编码仅为?),以及任何其他指针(均以^开头):

char intTypes[] = { @encode(signed char)[0], @encode(unsigned char)[0], @encode(short)[0], @encode(unsigned short)[0], @encode(int)[0], @encode(unsigned int)[0], @encode(long)[0], @encode(unsigned long)[0], @encode(long long)[0], @encode(unsigned long long)[0], '?', '^', 0 };

有了这个 C 字符串,可以使用 strchr 函数检查类型编码的首个字符是否匹配上述字符集中的任何一个。如果存在匹配,则该类型属于整数类型:

if(strchr(intTypes, type[0]))
return TypeInteger;

结构体类型以字符 { 开头。如果类型字符串以该字符开头,则调用结构体分类器:

if(type[0] == '{')
return [self classifyStructType: type];

如果所有条件都不满足,则返回 “other” 类型:

return TypeOther;
}

该结构体分类器使用了一个辅助方法,该方法接收结构体的类型字符串,并枚举其所有内容。它在每个节点跟踪结构体的分类情况,并在结构体遇到新元素时更新分类状态。它最初是从一个空结构体开始的:

- (enum TypeClassification)classifyStructType: (const char *)type
{
__block enum TypeClassification structClassification = TypeEmptyStruct;

然后它枚举并分类其中的每种类型:

[self enumerateStructElementTypes: type block: ^(const char *type) {
enum TypeClassification elementClassification = [self classifyType: type];

若当前分类是空结构体,则新分类与元素分类相同。仅含一个元素的结构体,其分类等同于该元素的分类:

if(structClassification == TypeEmptyStruct)
structClassification = elementClassification;

如果当前分类是整数类型且元素分类也是整数类型,那么该结构体会获得特殊分类:一个包含两个整数的结构体。

else if([self isIntegerClass: structClassification] && [self isIntegerClass: elementClassification])
structClassification = TypeTwoIntegers;

在其他任何情况下(结构体包含两个以上元素、结构体包含浮点元素等),其分类仅为通用结构体:

else
structClassification = TypeStruct;
}];
return structClassification;
}

枚举结构体类型字符串元素的方法很简短。一个结构体类型字符串由结构体名称、等号符号 =,以及后续连接的各个元素类型组成,整个内容包含在一对花括号 {} 中。例如,NSRange 看起来会是这样的形式:

{NSRange=LL}

该方法首先找到等号,并开始扫描紧随其后的部分:

- (void)enumerateStructElementTypes: (const char *)type block: (void (^)(const char *type))block
{
const char *equals = strchr(type, '=');
const char *cursor = equals + 1;

接着,它会遍历其中包含的每个类型,利用 NSGetSizeAndAlignment 函数将游标移至每个遇到的类型末尾 —— 即使该类型包含多个字符。这一过程持续进行,直至遇到右花括号:

while(*cursor != '}')
{
block(cursor);
cursor = NSGetSizeAndAlignment(cursor, NULL, NULL);
}
}

还有一个简短的辅助方法,用于判断某个特定的类型分类是否被视为整数。该方法仅检查该分类是否是对象、块、C 字符串,或是实际的整数或其他指针:

- (BOOL)isIntegerClass: (enum TypeClassification)classification
{
return classification == TypeObject || classification == TypeBlock || classification == TypeCString || classification == TypeInteger;
}

这样就完成了类型分类系统。相比 x86-64 规范的完整复杂度,这套系统还相当基础,但对于 MAInvocation 的需求已经足够。有了类型分类,我们终于可以实现初始化器中使用的 isStretReturn 方法了:

- (BOOL)isStretReturn
{
return [self classifyType: [_sig methodReturnType]] == TypeStruct;
}

设置参数
有了类型分类,现在终于可以实现参数设置了。setArgument:atIndex: 的基本形式与 getArgument:atIndex: 几乎相同,但由于需要支持被保留的(retained)参数,一切都变得更加复杂。

可以创建一个 NSInvocation 对象,对其进行设置,然后将其保留一段时间。为了使 NSInvocation 保持有效,它需要能够对其包含的参数执行正确的内存管理。为了灵活性,这是可选的。新创建的 NSInvocation 不会对其参数执行任何内存管理,但可以通过向其发送 retainArguments 消息来启用此功能。

MAInvocation 模仿了此功能。当它接收到 retainArguments 时,会对参数执行以下操作:

1. Block arguments are copied.
2. Non-block object arguments are retained.
3. C string arguments are copied.
4. All others are left alone.

除了在 retainArguments 的情况下需要这样做,setArgument:atIndex: 方法在每次设置新参数时也需要执行同样的操作。这正是该方法比 getArgument:atIndex: 复杂得多的原因。

该方法首先计算原始参数索引:

- (void)setArgument: (void *)argumentLocation atIndex: (NSInteger)idx
{
NSInteger rawArgumentIndex = idx;
if(_raw.isStretCall)
rawArgumentIndex++;

接着,它获取该索引处的参数指针:

uint64_t *dest = [self argumentPointerAtIndex: rawArgumentIndex];
assert(dest);

然后它会对该索引处的参数进行分类:

enum TypeClassification c = [self classifyArgumentAtIndex: idx];

如果参数需要被保留,它会接着检查参数的分类,判断它是块(block)、非块对象还是 C 字符串。如果是这些类型,它会使用适当的 retain 或 copy 语义,直接将 argumentLocation 处找到的值赋给 dest。如果参数是其他类型,或者参数未被保留,则执行简单的 memcpy。

第一种情况针对普通对象。这里只是进行了非常标准的 retain / release 组合,通过大量的类型转换将两个指针都当作对象指针处理。释放操作在最后使用旧变量进行,以避免释放旧值会使新值失效的问题:

if(_argumentsRetained && c == TypeObject)
{
id old = *(id *)dest;
*(id *)dest = [*(id *)argumentLocation retain];
[old release];
}

块(Block)得到了同样的处理,只不过它们获得的是复制(copy)而非保留(retain):

else if(_argumentsRetained && c == TypeBlock)
{
id old = *(id *)dest;
*(id *)dest = [*(id *)argumentLocation copy];
[old release];
}

C 字符串的处理方式类似,但使用 strdup(复制字符串)和 free(释放):

else if(_argumentsRetained && c == TypeCString)
{
char *old = *(char **)dest;
char *cstr = *(char **)argumentLocation;
if(cstr != NULL)
cstr = strdup(cstr);
*(char **)dest = cstr;
free(old);
}

在所有其他情况下,使用 memcpy 复制适当数量的字节:

else
{
NSUInteger size = [self sizeAtIndex: idx];
memcpy(dest, argumentLocation, size);
}
}

classifyArgumentAtIndex: 是对 classifyType: 的轻量封装,它从方法签名(method signature)中获取参数类型并进行分类:

- (enum TypeClassification)classifyArgumentAtIndex: (NSUInteger)idx
{
return [self classifyType: [_sig getArgumentTypeAtIndex: idx]];
}

保留参数
除了在 setArgument:atIndex: 中保留每个传入的参数外,MAInvocation 还需要在 retainArguments 方法中保留所有已存在的参数。由于只有首次调用会执行操作,该方法首先要检查参数是否已被保留,如果是则直接返回:

- (void)retainArguments
{
if(_argumentsRetained)
return;

接下来,它使用一个辅助方法来迭代所有可保留的参数。该方法对每一个可保留的参数调用一个闭包(block),并将参数的索引及其值传入该闭包。闭包中有三个值参数,而在任何一次调用中,只有其中一个会被设置。

[self iterateRetainableArguments: ^(NSUInteger idx, id obj, id block, char *cstr) {

如果它是一个对象参数,那么该参数就会被保留。

if(obj)
{
[obj retain];
}

如果它是一个 block(块)argument(参数),该块被复制,并将新值设置为参数值。注意 _argumentsRetained 尚未设置为 YES,因此 setArgument:atIndex: 不会尝试执行其自己的内存管理,避免两者之间的任何冲突:

else if(block)
{
block = [block copy];
[self setArgument: &block atIndex: idx];
}

如果是 C 字符串(C string)参数,它会使用 strdup

else if(cstr)
{
if(cstr != NULL)
cstr = strdup(cstr);
[self setArgument: &cstr atIndex: idx];
}
}];

最后,它设置了 _argumentsRetained

_argumentsRetained = YES;
}

iterateRetainableArguments: 方法使用类型分类系统来确定每个参数是什么,然后调用 getArgument:atIndex: 获取其值。它首先遍历每一个参数并对其进行分类:

- (void)iterateRetainableArguments: (void (^)(NSUInteger idx, id obj, id block, char *cstr))block
{
for(NSUInteger i = 0; i < [_sig numberOfArguments]; i++)
{
enum TypeClassification c = [self classifyArgumentAtIndex: i];

「Objects(对象)」和「blocks(块)」都由同一个分支处理。它首先将参数检索到一个局部的「id(Objective-C 中的动态类型)」变量中:

if(c == TypeObject || c == TypeBlock)
{
id arg;
[self getArgument: &arg atIndex: i];

接着,根据类型是块(block)还是普通对象,它会将 arg 移动到另外两个局部变量中的一个。

id o = c == TypeObject ? arg : nil;
id b = c == TypeBlock ? arg : nil;

至此,如果是一个普通参数,o 就包含参数值;如果是一个块(block),b 就包含参数值。随后可以用这些值来调用迭代块:

block(i, o, b, NULL);
}

C 字符串则类似,但不那么复杂,因为这里只有一种可能的类型:

else if(c == TypeCString)
{
char *arg;
[self getArgument: &arg atIndex: i];
block(i, nil, nil, arg);
}
}
}

既然我们在这里,这里有一个简单的 getter 方法用于 argumentsRetained

- (BOOL)argumentsRetained
{
return _argumentsRetained;
}

释放对象最难的部分是释放那些被保留的参数。iterateRetainableArguments: 方法承担了大部分工作:

- (void)dealloc
{
if(_argumentsRetained)
{
[self iterateRetainableArguments: ^(NSUInteger idx, id obj, id block, char *cstr) {
[obj release];
[block release];
free(cstr);
}];
}

最后需要做的就是释放方法签名、栈参数指针,并调用 super 方法。

[_sig release];
free(_raw.stackArgs);
[super dealloc];
}

目前为止,代码已将结构体RawArguments几乎完全更新到位。实现invokeWithTarget:方法只需填补最后的细节,然后调用MAInvocationCall汇编胶水函数(assembly glue function)。该方法首先设置target值:

- (void)invokeWithTarget: (id)target
{
[self setTarget: target];

它随后使用 methodForSelector: 获取该 invocation(调用)的 selector(选择子)对应的函数指针,并将其存入 fptr 字段。这便是胶水代码将要调用的内容:

_raw.fptr = [target methodForSelector: [self selector]];

如果这是一个 stret 调用(结构体返回调用),那么 rdi 寄存器需要被设置为指向可以存放返回值的空间:

if(_raw.isStretCall)
_raw.rdi = (uint64_t)[self returnValuePtr];

最后,调用汇编胶水代码:

MAInvocationCall(&_raw);
}

在设置好所有寄存器字段和栈参数指针,并将函数指针字段指向目标 IMP(方法实现)后,汇编胶水代码即可发起调用。函数返回时,汇编胶水代码会将 rax 和 rdx 寄存器中的值复制到 RawArguments 结构体的返回值字段中。这意味着当汇编胶水代码返回时,返回值已经设置完毕,可以通过 getReturnValue: 直接获取,无需 Objective-C 代码执行任何额外操作。

转发 MAInvocation 的最后一个主要部分是 MAInvocationForwardC 函数。汇编语言转发胶水代码会拦截未知消息调用。它随后根据函数调用在栈上构造 RawArguments 结构体,并传递指向该结构体的指针,通过它调用 MAInvocationForwardC。其余逻辑在 Objective-C 中实现:

void MAInvocationForwardC(struct RawArguments *r)
{

首先要获取消息发送的目标对象以及被发送的选择子(selector)。对于 stret 调用,目标对象位于 rsi 寄存器,选择子位于 rdx 寄存器。对于普通调用,目标对象位于 rdi 寄存器,选择子位于 rsi 寄存器:

id obj;
SEL sel;
if(r->isStretCall)
{
obj = (id)r->rsi;
sel = (SEL)r->rdx;
}
else
{
obj = (id)r->rdi;
sel = (SEL)r->rsi;
}

方法签名(method signature)对于创建调用对象(invocation object)至关重要。当获得对象和选择器(selector)后,只需调用 methodSignatureForSelector: 方法即可获取方法签名:

NSMethodSignature *sig = [obj methodSignatureForSelector: sel];

有了方法签名后,转发函数现在可以创建一个 MAInvocation

MAInvocation *inv = [[MAInvocation alloc] initWithMethodSignature: sig];

接下来要做的,是把 r 中所有相关信息复制到 invocation 的_raw 实例变量中。首先是寄存器:

inv->_raw.rdi = r->rdi;
inv->_raw.rsi = r->rsi;
inv->_raw.rdx = r->rdx;
inv->_raw.rcx = r->rcx;
inv->_raw.r8 = r->r8;
inv->_raw.r9 = r->r9;

此后,栈参数被复制。尽管 r 寄存器对于栈参数数量始终包含 0,但 invocation 对象此时已经计算出了实际的栈参数数量,因此可以查阅其 _raw 变量来获取该数量:

memcpy(inv->_raw.stackArgs, r->stackArgs, inv->_raw.stackArgsCount * sizeof(uint64_t));

现在,调用对象(invocation)已完全构造并填充完毕。该对象将被转发(sent forward)至 forwardInvocation: 方法,并携带新构造的调用对象。

[obj forwardInvocation: (id)inv];

调用返回后,需要将返回值复制回寄存器 r。随后汇编粘合代码(assembly glue)会将该值传回调用方(caller)。具体操作是先将两个返回值寄存器(return value registers)的内容复制过来:

r->rax_ret = inv->_raw.rax_ret;
r->rdx_ret = inv->_raw.rdx_ret;

如果这是一个结构体返回调用(stret call),并且该调用实例实际上拥有一个用于存放返回值的缓冲区,那么该调用实例返回值缓冲区中的值将被复制到 r->rdi 指向的内存中,这正是调用方指定的返回值存放位置:

if(r->isStretCall && inv->_stretBuffer)
{
memcpy((void *)r->rdi, inv->_stretBuffer, [inv returnValueSize]);
}

现在一切已就绪,于是释放 invocation(调用),并将控制权返回给汇编胶水代码。

[inv release];
}

胶水代码现在会将 raxrdx 字段的值复制回对应的 CPU 寄存器(register),然后将控制权交还给原方法调用方。调用方将通过这些寄存器或其通过 rdi 传入的结构体返回缓冲区(stret buffer)来获取返回值。

结论
以上便完成了 MAInvocation 的实现。尽管仅支持 x86-64 架构且忽略了所有结构体参数和浮点类型(这些占据了 x86-64 调用约定(calling conventions)的很大部分),其实现仍极为复杂繁琐。NSInvocation 不仅支持除少数边缘情况(如联合体参数(union parameters))外的所有参数与返回值类型,还至少兼容三种不同架构:i386、x86-64 和 ARM。

然而,尽管实现复杂,这一切完全可行。覆盖所有情况需要大量时间和精力,但其中并无任何神秘或魔法可言。为此需要为其他架构编写对应的汇编胶水函数(assembly glue functions)、扩展胶水函数以覆盖浮点寄存器(floating-point register),并在 MAInvocation 的 Objective-C 代码中实现所有关于参数传递位置的逻辑。

构建 MAInvocation 非常有趣,它让我们深刻理解了 NSInvocation 内部究竟在做什么。显而易见的是,请不要在任何实际项目中使用 MAInvocation。NSInvocation 具备它的全部功能甚至更多,而且无疑做得更好。

今天就到这里。下次再见时,我们将开启另一段激动人心的探索之旅。Friday Q & A 系列由读者的创意所驱动,因此在此期间,请继续向我发送你们想探讨的主题建议。


#Original (English)

Source: https://www.mikeash.com/pyblog/friday-qa-2013-03-22-lets-build-nsinvocation-part-ii.html

Last time on Friday Q&A, I began the reimplementation of parts of NSInvocation as MAInvocation. In that article, I discussed the basic theory, the architecture calling conventions, and presented the assembly language glue code needed for the implementation. Today, I present the Objective-C part of MAInvocation.

RecapMAInvocation is my reimplementation of a large chunk of NSInvocation. For simplicity, it doesn’t support floating point arguments or return values, and it also doesn’t support struct arguments. It only supports the x86-64 architecture. The code is on GitHub here:

https://github.com/mikeash/MAInvocation

The first six parameters to a function are passed in six registers: rdi, rsi, rdx, rcx, r8, and r9. Subsequent parameters, if any, are passed on the stack. Return values are returned in rax. For the special case of a two-element struct, the second element is returned in rdx. Larger structs are returned by having the caller allocate memory, and then a pointer to that memory is implicitly passed in as the first parameter in rdi, with all explicit parameters bumped down. These are called stret calls in the Objective-C world.

Assembly language glue is used to translate between values held in a struct and actual function calls. The struct holds all of the registers in question, plus a pointer to stack arguments, plus some additional data:

struct RawArguments
{
void *fptr;
uint64_t rdi;
uint64_t rsi;
uint64_t rdx;
uint64_t rcx;
uint64_t r8;
uint64_t r9;
uint64_t stackArgsCount;
uint64_t *stackArgs;
uint64_t rax_ret;
uint64_t rdx_ret;
uint64_t isStretCall;
};

The MAInvocationCall function is written in assembly, and was explored in the previous article. It has this prototype:

void MAInvocationCall(struct RawArguments *);

Objective-C code can fill out a struct RawArguments with a function pointer and the appropriate register values, then call this function. It will make the function call, and on return, the two return value register fields in the struct will be filled out with whatever the function returned.

There are also two forwarding handlers:

void MAInvocationForward(void);
void MAInvocationForwardStret(void);

These are designed to be invoked by an arbitrary Objective-C method call. They both create a new struct RawArguments, fill out the argument registers and stack arguments pointer, and then invoke a C function called MAInvocationForwardC. When that returns, the handlers pass the rax_ret and rdx_ret values back to the caller. The only difference between these two handlers is whether they set the isStretCall to 0 or 1.

The stage is now set for the Objective-C implementation of MAInvocation.

InterfaceThe interface to MAInvocation is the same as NSInvocation:

@interface MAInvocation : NSObject
+ (MAInvocation *)invocationWithMethodSignature:(NSMethodSignature *)sig;
- (NSMethodSignature *)methodSignature;
- (void)retainArguments;
- (BOOL)argumentsRetained;
- (id)target;
- (void)setTarget:(id)target;
- (SEL)selector;
- (void)setSelector:(SEL)selector;
- (void)getReturnValue:(void *)retLoc;
- (void)setReturnValue:(void *)retLoc;
- (void)getArgument:(void *)argumentLocation atIndex:(NSInteger)idx;
- (void)setArgument:(void *)argumentLocation atIndex:(NSInteger)idx;
- (void)invoke;
- (void)invokeWithTarget:(id)target;
@end

Instance VariablesThe method signature is central to an invocation object. The method signature describes how many parameters the method takes, as well as what the types are. This is critical information to be able to figure out how to deal with method parameters and return types. This is why the only way to create an MAInvocation is with an NSMethodSignature, and that signature is stored in an instance variable:

@implementation MAInvocation {
NSMethodSignature *_sig;

The invocation keeps a local struct RawArguments. This struct is manipulated directly when setting or getting arguments and return values. When the invocation is invoked, a pointer to the instance variable can be passed directly to the assembly language glue:

struct RawArguments _raw;

Invocations can retain their arguments. This sends retain to all object arguments, and it also copies C string arguments. Whether arguments are currently retained needs to be tracked, so that they can be properly freed, and so that newly-set arguments can be retained, so there’s a flag for that:

BOOL _argumentsRetained;

Finally, there needs to be a buffer to store the return value for stret calls:

void *_stretBuffer;
}

InitializationThe factory method just calls an init method:

+ (NSInvocation *)invocationWithMethodSignature: (NSMethodSignature *)sig
{
return [[[self alloc] initWithMethodSignature: sig] autorelease];
}

The init method saves the method signature:

- (id)initWithMethodSignature: (NSMethodSignature *)sig
{
if((self = [super init]))
{
_sig = [sig retain];

It then populates the isStretCall of the struct RawArguments by examining the method signature to determine whether it fits the requirements for a stret call. This is done by calling another method. The code for that method is rather involved, and will come later:

_raw.isStretCall = [self isStretReturn];

Next, the stack arguments are set up. The first thing to do here is to get the total number of arguments from the method signature:

NSUInteger argsCount = [sig numberOfArguments];

Note that this count includes the two implicit arguments, self and _cmd, so this number is exactly equal to the number of function arguments being passed.

If it’s a stret call, then there’s effectively one more argument, because of the implicit pointer for the return value passed in rdi:

if(_raw.isStretCall)
argsCount++;

If there are more than six arguments (potentially including the implicit stret parameter), then there are stack arguments. stackArgsCount is set to the number of remaining arguments, and memory is allocated so that stackArgs can hold them:

if(argsCount > 6)
{
_raw.stackArgsCount = argsCount - 6;
_raw.stackArgs = calloc(argsCount - 6, sizeof(*_raw.stackArgs));
}
}
return self;
}

Wrapper MethodsThere are a few methods in the API that are simply small wrappers around other methods. I’ll cover them here before we get to the real meat.

The target method just gets the value of the first argument in a slightly more convenient way. The method is just a small wrapper around the getArgument:atIndex: method:

- (id)target
{
id target;
[self getArgument: &target atIndex: 0];
return target;
}

The setTarget: method is an even simpler wrapper around the setArgument:atIndex: method:

- (void)setTarget: (id)target
{
[self setArgument: &target atIndex: 0];
}

The selector and setSelector: methods are virtually identical, but manipulate the second argument:

- (SEL)selector
{
SEL sel;
[self getArgument: &sel atIndex: 1];
return sel;
}
- (void)setSelector: (SEL)selector
{
[self setArgument: &selector atIndex: 1];
}

Finally, the invoke method calls invokeWithTarget:, passing [self target]:

- (void)invoke
{
[self invokeWithTarget: [self target]];
}

Getting ArgumentsIn order to get an argument out of the invocation, the code first needs to know where an argument is stored. This small wrapper method handles that:

- (uint64_t *)argumentPointerAtIndex: (NSInteger)idx
{
uint64_t *ptr = NULL;
if(idx == 0)
ptr = &_raw.rdi;
if(idx == 1)
ptr = &_raw.rsi;
if(idx == 2)
ptr = &_raw.rdx;
if(idx == 3)
ptr = &_raw.rcx;
if(idx == 4)
ptr = &_raw.r8;
if(idx == 5)
ptr = &_raw.r9;
if(idx >= 6)
ptr = _raw.stackArgs + idx - 6;
return ptr;
}

This method takes a raw argument index, which is to say that it’s already been adjusted to take into account whether or not this is a stret call. It then maps that index onto the appropriate register or stack slot.

It’s also handy to be able to get the size of a particular argument, so it can copy the right number of bytes for arguments that are smaller than 8 bytes. This method wraps the Foundation function NSGetSizeAndAlignment, which takes an Objective-C type string and returns the size (and alignment!) of the type in question:

- (NSUInteger)sizeOfType: (const char *)type
{
NSUInteger size;
NSGetSizeAndAlignment(type, &size, NULL);
return size;
}

Another small wrapper around this method provides the size of a given argument:

- (NSUInteger)sizeAtIndex: (NSInteger)idx
{
return [self sizeOfType: [_sig getArgumentTypeAtIndex: idx]];
}

To actually fetch an argument, the method first adjusts the requested index to take into account the possible stret return:

- (void)getArgument: (void *)argumentLocation atIndex: (NSInteger)idx
{
NSInteger rawArgumentIndex = idx;
if(_raw.isStretCall)
rawArgumentIndex++;

Next, it grabs the pointer from the above method, and checks it for sanity:

uint64_t *src = [self argumentPointerAtIndex: rawArgumentIndex];
assert(src);

Then it grabs the argument size and copies the appropriate number of bytes out of the argument location:

NSUInteger size = [self sizeAtIndex: idx];
memcpy(argumentLocation, src, size);
}

Getting and Setting Return ValuesTo get and set the return value, the value’s size is needed. This is easy to obtain by just getting the size of the return type obtained from the method signature:

- (NSUInteger)returnValueSize
{
return [self sizeOfType: [_sig methodReturnType]];
}

It’s also necessary to get a pointer to the location where the return value is stored. If the invocation is for a stret call, then it returns _stretBuffer. If the buffer isn’t allocated yet, it allocates it:

- (void *)returnValuePtr
{
if(_raw.isStretCall)
{
if(_stretBuffer == NULL)
_stretBuffer = calloc(1, [self returnValueSize]);
return _stretBuffer;
}

For regular calls, it just returns the address of the rax_ret field in the raw arguments struct:

else
{
return &_raw.rax_ret;
}
}

This takes care of the case where the return value uses both return registers. Since they’re contiguous in the struct, copying a sufficiently large value into this address will write to both register fields in the struct.

With these methods available, writing the methods to get and set the return value is easy. All they have to do is call memcpy with the computed size and pointer:

- (void)getReturnValue: (void *)retLoc
{
NSUInteger size = [self returnValueSize];
memcpy(retLoc, [self returnValuePtr], size);
}
- (void)setReturnValue: (void *)retLoc
{
NSUInteger size = [self returnValueSize];
memcpy([self returnValuePtr], retLoc, size);
}

Type ClassificationDetermining whether a method’s return type requires a stret call requires classifying that type according to the x86-64 calling conventions. The NSInvocation API allows for retaining the arguments to the invocation, which also requires classifying the argument types, so that all of the object types can be found.

The different classifications get put into an enum which combines the relevant parts of the x86-64 ABI with the distinctions necessary for retaining arguments. This boils down to objects, blocks, C strings, other integer types (including non-object pointers), a struct containing two integers, an empty struct, any other struct, and any other type not already covered:

enum TypeClassification
{
TypeObject,
TypeBlock,
TypeCString,
TypeInteger,
TypeTwoIntegers,
TypeEmptyStruct,
TypeStruct,
TypeOther
};

The classification process itself consists of two mutually-recursive methods: one that classifies arbitrary types, and one specialized to classify struct types.

The general method starts by creating the type strings for ‘id’, blocks, and C strings by using the @encode directive:

- (enum TypeClassification)classifyType: (const char *)type
{
const char *idType = @encode(id);
const char *blockType = @encode(void (^)(void));
const char *charPtrType = @encode(char *);

Note that all blocks have the same type string when it comes to @encode, so the choice of block type here is completely arbitrary.

With these in hand, it compares type against them, and returns the appropriate enum value if there’s a match:

if(strcmp(type, idType) == 0)
return TypeObject;
if(strcmp(type, blockType) == 0)
return TypeBlock;
if(strcmp(type, charPtrType) == 0)
return TypeCString;

Next, it checks integer types. This crazy bit of code constructs a C string that contains every the character for every integer type, plus function pointers (which is just ?), plus any other pointer (which all start with ^):

char intTypes[] = { @encode(signed char)[0], @encode(unsigned char)[0], @encode(short)[0], @encode(unsigned short)[0], @encode(int)[0], @encode(unsigned int)[0], @encode(long)[0], @encode(unsigned long)[0], @encode(long long)[0], @encode(unsigned long long)[0], '?', '^', 0 };

With that C string in hand, the strchr function can be used to check the first character in type aganist all of these characters. If there’s a match, then the type is an integer type:

if(strchr(intTypes, type[0]))
return TypeInteger;

struct types begin with the { character. If the type string starts with that character, then call into the struct classifier:

if(type[0] == '{')
return [self classifyStructType: type];

If nothing matches, then return the “other” type:

return TypeOther;
}

The struct classifier uses a helper method that takes the type string for the struct and enumerates over all of its contents. It tracks the struct’s classification at each point, and updates it with each new element in the struct. It starts out with an empty struct:

- (enum TypeClassification)classifyStructType: (const char *)type
{
__block enum TypeClassification structClassification = TypeEmptyStruct;

Then it enumerates and classifies each type within:

[self enumerateStructElementTypes: type block: ^(const char *type) {
enum TypeClassification elementClassification = [self classifyType: type];

If the current classification is an empty struct, then the new classification is the same as the element classification. A struct with one element is classified the same as the element it contains:

if(structClassification == TypeEmptyStruct)
structClassification = elementClassification;

If the current classification is an integer type and the element classification is also an integer type, then the struct gets the special classification of a struct containing two integers:

else if([self isIntegerClass: structClassification] && [self isIntegerClass: elementClassification])
structClassification = TypeTwoIntegers;

In any other circumstance (struct contains more than two elements, struct contains floating-point elements, etc.) the classification is just a generic struct:

else
structClassification = TypeStruct;
}];
return structClassification;
}

The method to enumerate over a struct type string’s elements is short. A struct type string consists of the struct’s name, the = symbol, and then each element’s type concatenated together, all contained within a pair of {}. For example, NSRange would look like:

{NSRange=LL}

The first thing the method does is find the = and start scanning just beyond it:

- (void)enumerateStructElementTypes: (const char *)type block: (void (^)(const char *type))block
{
const char *equals = strchr(type, '=');
const char *cursor = equals + 1;

Then it enumerates over each type contained within, taking advantage of NSGetSizeAndAlignment to move the cursor to the end of each type encountered, even if the type contains more than one character. It does this until it encounters a closing brace:

while(*cursor != '}')
{
block(cursor);
cursor = NSGetSizeAndAlignment(cursor, NULL, NULL);
}
}

There’s also a short helper method that determines whether a particular type classification is considered an integer. This just checks to see if the classification is an object, block, C string, or an actual integer or other pointer:

- (BOOL)isIntegerClass: (enum TypeClassification)classification
{
return classification == TypeObject || classification == TypeBlock || classification == TypeCString || classification == TypeInteger;
}

This finishes the type classification system. This is somewhat rudimentary compared to the full complexity of the x86-64 spec, but it’s enough for MAInvocation’s needs. With type classification available, we can finally implement the isStretReturn method used in the initializer:

- (BOOL)isStretReturn
{
return [self classifyType: [_sig methodReturnType]] == TypeStruct;
}

Setting ArgumentsWith type classification in place, it’s finally time to implement setting arguments. The basic form of setArgument:atIndex: is nearly identical to getArgument:atIndex:, but the need to support retained arguments makes everything far more complicated.

It’s possible to create an NSInvocation, set it up, and then keep it around for a while. In order for the NSInvocation to remain valid, it needs to be able to do proper memory management on the arguments it contains. In a nod to flexibility, this is optional. A freshly-made NSInvocation doesn’t do any memory management on its arguments, but it can be enabled by sending it a retainArguments message.

MAInvocation mimics this functionality. When it receives retainArguments it performs the following operations on its arguments:

1. Block arguments are copied.
2. Non-block object arguments are retained.
3. C string arguments are copied.
4. All others are left alone.

In addition to doing this for retainArguments, the setArgument:atIndex: method needs to do this for each newly set argument as well. This is what makes it so much more complex than getArgument:atIndex:.

The method starts off by computing the raw argument index:

- (void)setArgument: (void *)argumentLocation atIndex: (NSInteger)idx
{
NSInteger rawArgumentIndex = idx;
if(_raw.isStretCall)
rawArgumentIndex++;

Next, it gets the argument pointer at that index:

uint64_t *dest = [self argumentPointerAtIndex: rawArgumentIndex];
assert(dest);

Then it classifies the argument at this index:

enum TypeClassification c = [self classifyArgumentAtIndex: idx];

If arguments are retained, it will then check the classification of the arguments to see if it’s a block, a non-block object, or a C string. If it is, then it directly sets dest to the value found at argumentLocation using the appropriate retain or copy semantics. If argument is of a different type, or if arguments aren’t retained, it does a simple memcpy.

The first case is for plain objects. This just does a fairly standard retain/release combination, with a bunch of casting to treat both pointers as object pointers. The release is done at the end using the old variable to avoid problems where releasing the old value invalidates the new one:

if(_argumentsRetained && c == TypeObject)
{
id old = *(id *)dest;
*(id *)dest = [*(id *)argumentLocation retain];
[old release];
}

Blocks get the same treatment, except they get a copy rather than a retain:

else if(_argumentsRetained && c == TypeBlock)
{
id old = *(id *)dest;
*(id *)dest = [*(id *)argumentLocation copy];
[old release];
}

C strings are similar, but use strdup and free:

else if(_argumentsRetained && c == TypeCString)
{
char *old = *(char **)dest;
char *cstr = *(char **)argumentLocation;
if(cstr != NULL)
cstr = strdup(cstr);
*(char **)dest = cstr;
free(old);
}

In all other cases, the appropriate number of bytes is copied over using memcpy:

else
{
NSUInteger size = [self sizeAtIndex: idx];
memcpy(dest, argumentLocation, size);
}
}

The classifyArgumentAtIndex: is a small wrapper around classifyType: that retrieves the argument type from the method signature and classifies it:

- (enum TypeClassification)classifyArgumentAtIndex: (NSUInteger)idx
{
return [self classifyType: [_sig getArgumentTypeAtIndex: idx]];
}

Retaining ArgumentsIn addition to retaining each argument as it arrives in setArgument:atIndex:, MAInvocation also needs to retain all existing arguments in the retainArguments method. Only the first call does anything, so the first thing that method does is check to see if arguments are already retained, and bail out if so:

- (void)retainArguments
{
if(_argumentsRetained)
return;

Next, it iterates over all retainable arguments, using a helper method. This method invokes a block for each retainable argument that passes in the argument index as well as the argument’s value. There are three value arguments in the block, and only one is set for any given call.

[self iterateRetainableArguments: ^(NSUInteger idx, id obj, id block, char *cstr) {

If it’s an object argument, then that argument is retained:

if(obj)
{
[obj retain];
}

If it’s a block argument, the block is copied, and the new value set as the argument value. Note that _argumentsRetained has not yet been set to YES, so setArgument:atIndex: won’t try to do its own memory management, avoiding any conflict between the two:

else if(block)
{
block = [block copy];
[self setArgument: &block atIndex: idx];
}

If it’s a C string argument, it uses strdup:

else if(cstr)
{
if(cstr != NULL)
cstr = strdup(cstr);
[self setArgument: &cstr atIndex: idx];
}
}];

Finally, it sets _argumentsRetained:

_argumentsRetained = YES;
}

The iterateRetainableArguments: method uses the type classification system to figure out what each argument is, then calls getArgument:atIndex: to fetch the value. It first iterates over each argument and classifies it:

- (void)iterateRetainableArguments: (void (^)(NSUInteger idx, id obj, id block, char *cstr))block
{
for(NSUInteger i = 0; i < [_sig numberOfArguments]; i++)
{
enum TypeClassification c = [self classifyArgumentAtIndex: i];

Objects and blocks are both handled by the same branch. It first retrieves the argument into a local id variable:

if(c == TypeObject || c == TypeBlock)
{
id arg;
[self getArgument: &arg atIndex: i];

It then moves arg into one of two other local variables depending on whether the type is a block or a plain object:

id o = c == TypeObject ? arg : nil;
id b = c == TypeBlock ? arg : nil;

At this point, o contains the argument value if it’s a plain argument, and b contains the argument value if it’s a block. The iteration block can then be called with these values:

block(i, o, b, NULL);
}

C strings are similar, but less complex, because there’s only one possible type here:

else if(c == TypeCString)
{
char *arg;
[self getArgument: &arg atIndex: i];
block(i, nil, nil, arg);
}
}
}

While we’re at it, here’s a quick getter method for argumentsRetained:

- (BOOL)argumentsRetained
{
return _argumentsRetained;
}

DeallocThe hardest part of dealloc is freeing the retained arguments. The iterateRetainableArguments: method takes care of most of the work:

- (void)dealloc
{
if(_argumentsRetained)
{
[self iterateRetainableArguments: ^(NSUInteger idx, id obj, id block, char *cstr) {
[obj release];
[block release];
free(cstr);
}];
}

With that taken care of, all that remains is freeing the method signature, the stackArgs pointer, and calling super:

[_sig release];
free(_raw.stackArgs);
[super dealloc];
}

InvocationThe code so far has kept the struct RawArguments almost completely up to date. Implementing invokeWithTarget: is simply a matter of filling out the last details, then making a call to the MAInvocationCall assembly glue function. The method starts out by setting the target value:

- (void)invokeWithTarget: (id)target
{
[self setTarget: target];

It then uses methodForSelector: to get the function pointer for the invocation’s selector and places that into the fptr field. This is what the glue code will call:

_raw.fptr = [target methodForSelector: [self selector]];

If this is a stret call, then rdi needs to be set up to point to space that can hold the return value:

if(_raw.isStretCall)
_raw.rdi = (uint64_t)[self returnValuePtr];

Finally, call the assembly glue:

MAInvocationCall(&_raw);
}

With all of the register fields and the stack arguments pointer set up, and the function pointer field set to the target IMP, the assembly glue is able to make the call. Upon return, the assembly glue copies rax and rdx into the return value fields of the struct RawArguments. This means that the return value is already set when the assembly glue returns, and will be available from getReturnValue: without any additional action in the Objective-C code.

ForwardingThe last major piece of MAInvocation is the MAInvocationForwardC function. The assembly language forwarding glue intercepts unknown message calls. It then constructs a struct RawArguments on the stack from the function call, and then calls through to MAInvocationForwardC, passing it a pointer to the struct RawArguments. The remainder of the logic is implemented in Objective-C:

void MAInvocationForwardC(struct RawArguments *r)
{

The first order of business is to get the object that the message was sent to, and the selector being sent. For a stret call, the object is in rsi and the selector is in rdx. For a normal call, the object is in rdi and the selector is in rsi:

id obj;
SEL sel;
if(r->isStretCall)
{
obj = (id)r->rsi;
sel = (SEL)r->rdx;
}
else
{
obj = (id)r->rdi;
sel = (SEL)r->rsi;
}

A method signature is critical to creating an invocation object. With the object and selector available, a simple call to methodSignatureForSelector: obtains that:

NSMethodSignature *sig = [obj methodSignatureForSelector: sel];

With the method signature in hand, the forwarding function can now create an MAInvocation:

MAInvocation *inv = [[MAInvocation alloc] initWithMethodSignature: sig];

The next order of business is to copy all of the pertinent information from r into the invocation’s_raw` instance variable. First come the registers:

inv->_raw.rdi = r->rdi;
inv->_raw.rsi = r->rsi;
inv->_raw.rdx = r->rdx;
inv->_raw.rcx = r->rcx;
inv->_raw.r8 = r->r8;
inv->_raw.r9 = r->r9;

After that, stack arguments are copied. Although r always contains 0 for stackArgsCount, the invocation has now computed the number of actual stack arguments, so its _raw variable can be consulted to get the count:

memcpy(inv->_raw.stackArgs, r->stackArgs, inv->_raw.stackArgsCount * sizeof(uint64_t));

The invocation is now fully constructed and filled out. The object is sent forwardInvocation: with the newly constructed invocation.

[obj forwardInvocation: (id)inv];

After that call returns, the return value from the invocation needs to be copied back into r. The assembly glue will then pass the value back to the caller. It first copies the two return value registers over:

r->rax_ret = inv->_raw.rax_ret;
r->rdx_ret = inv->_raw.rdx_ret;

If it’s a stret call and the invocation actually has a buffer to hold the return value, the value in the invocation’s return value buffer is copied into the memory pointed to by r->rdi, which is where the caller specified that it wanted the return value placed:

if(r->isStretCall && inv->_stretBuffer)
{
memcpy((void *)r->rdi, inv->_stretBuffer, [inv returnValueSize]);
}

Everything is now complete, so the invocation is released, and control is returned to the assembly language glue:

[inv release];
}

The glue code will now copy the rax and rdx fields back into the respective CPU registers, then return control to the original method caller, which will see the return value either in those registers or in the stret buffer that it passed in rdi.

ConclusionThat wraps up the implementation of MAInvocation. It’s enormously complicated and involved, despite only supporting x86-64 and ignoring struct parameters and floating point of all kinds, which are a large part of the x86-64 calling conventions. NSInvocation not only supports all types of parameters and return values (aside from a few corner cases like union parameters), it also supports them on at least three different architectures: i386, x86-64, and ARM.

However, despite the complication, it’s all very much doable. Covering all of the cases requires a lot of time and effort, but there’s nothing mysterious or magical. It would require equivalents of the assembly glue functions for the other architectures, expanding the glue functions to cover the floating-point registers, and implementing all of the logic for which arguments go where in the MAInvocation Objective-C code.

MAInvocation was a lot of fun to build and gives great insight on just what NSInvocation is doing. It should be obvious, but don’t use MAInvocation for any real work. NSInvocation does all the same stuff and more, and no doubt does it better.

That’s it for today. Come back next time for another breathtaking adventure. Friday Q&A is driven by reader ideas, so until then, please keep sending in your ideas for topics to cover.