将参数传递给不接受任何参数的选择器是否安全?

Is it safe to pass an argument to a selector that doesn't accept any?

我开始学习 Objective-C,想知道如果将一个对象传递给对方法的动态调用,而该方法不接受任何对象,会发生什么情况。

#import <Foundation/Foundation.h>

# pragma mark Forward declarations 

@class DynamicWorker;
@class DynamicExecutor;

# pragma mark Interfaces

// Protocol for a worker object, not receiving any parameters
@protocol Worker<NSObject>

-(void)doStuff;

@end

// Dynamic worker returns a selector to a private method capable of
// doing work.
@interface DynamicWorker : NSObject<Worker>

- (SEL)getWorkDynamically;

@end

// Dynamic executor calls a worker with a selector it provided. The
// executor passes itself, in case the worker needs to launch more 
// workers. The method signature should be of the form
//    (void)method:(DynamicExecutor *)executor
@interface DynamicExecutor : NSObject

- (void)runWorker:(id<Worker>)worker withSelector:(SEL)selector;

@end

#pragma mark Implementations

@implementation DynamicWorker;

- (SEL)getWorkDynamically {
    return @selector(doStuff);
}

-(void) doStuff {
    NSLog(@"I don't accept parameters");
}

@end

@implementation DynamicExecutor;

// Here I get a warning, that I choose to ignore now:
// 
- (void)runWorker:(id<Worker>)worker withSelector:(SEL)selector {
    [worker performSelector:selector withObject:self];
}

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
      NSLog(@"Getting busy");

      DynamicWorker *worker = [[DynamicWorker alloc] init];
      DynamicExecutor *executor = [[DynamicExecutor alloc] init];
      [executor runWorker:worker withSelector:[worker getWorkDynamically]];
    }
    return 0;
}

到目前为止,它似乎没有引起任何问题,实际上看起来类似于 Javascript 事件处理程序,其中接受事件是可选的。不过,根据我对裸机的理解,我相信参数会被放在堆栈上,并且不知道运行时如何知道它应该被丢弃。

From my understanding of the bare metal, though, I believe the argument would be placed on the stack, and have no idea how the runtime would know that it should be discarded.

你是对的,调用者将参数放在堆栈上。在调用 returns 之后,调用者删除它放在堆栈上的参数,因此丢弃 callee 不期望的任何额外参数不是问题。

然而,这还不足以知道您的代码是否有效,被调用者 需要知道参数在堆栈中的位置。堆栈通常随着项目被推入堆栈而向下增长,被调用者将参数定位为堆栈指针的正偏移量。如果参数从左到右压入,则最后一个参数位于堆栈指针的最小偏移量处,第一个参数位于最大偏移量处。如果在这种情况下推送额外的参数,那么预期参数的偏移量将全部改变。但是 (Objective-)C 支持可变参数函数,那些参数数量未指定的函数(想想 printfstringWithFormat: 等),因此调用中的参数被推向右-向左,至少对于可变参数函数,这样第一个参数是最后一个被压入的,因此无论压入多少参数,第一个参数都位于堆栈指针的已知常量偏移量处。

最后,Objective-C 方法调用被转换为对运行时函数 objc_msgSend() 的调用,它实现了动态方法查找。此函数是可变的(因为不同的消息采用不同数量的参数)。

所以你的 Objective-C 方法调用变成了对可变运行时函数的调用,如果你提供了太多的参数,它们将被 callee 忽略并被清除来电者.

希望一切都有意义!

附录

在评论中@newacct 正确地指出 objc_msgSend 不是可变的;我应该写 "effectively variadic" 因为我为了简单起见模糊了细节。他们还争辩说它是 "trampoline" 而不是函数;虽然这在技术上是正确的,但蹦床本质上是一个跳转到其他代码而不是直接 returning 的函数,其他代码执行 return 返回给调用者(这类似于尾调用优化所做的).

回到"essentially variadic":objc_msgSend函数,像所有实现Objective-C方法的函数一样,第一个参数是调用方法的对象引用,一个第二个参数是所需方法的选择器,然后是该方法采用的任何参数的顺序 - 因此调用采用 可变数量的参数 但严格来说并不是 可变函数.

要找到要在运行时调用的实际方法实现 objc_msgSend 使用前两个参数;对象引用和选择器;并执行查找。当它找到适当的实现时,它 jumps/tail calls/trampolines 就可以了。由于 objc_msgSend 在检查选择器(即第二个参数)之前无法知道传递了多少个参数,因此它需要能够将第二个参数定位到距堆栈指针的已知偏移量处,为此(很容易)可能的参数必须以相反的顺序推送 - 就像可变参数函数一样。由于调用者以相反的顺序推送参数,因此它们对被调用者没有影响,并且其他参数将被忽略且无害提供调用者负责在调用后删除参数。

对于可变函数,调用者必须是删除参数的调用者,因为只有它知道传递了多少参数,对于非可变函数,被调用者可以删除参数 - 这包括 objc_msgSend 尾调用 - 但许多编译器,包括 Clang,让调用者删除它们。

因此,对 objc_msgSend 的调用(方法调用的编译)将在 Clang 下通过与可变参数函数基本相同的机制忽略任何额外参数。

希望这能让它更清楚,不会增加混乱!

(注意:实际中有些参数可能是在寄存器中传递的,而不是在堆栈中传递的,这对上面的描述没有重大影响。)

您正在调用方法 -[NSObject performSelector:withObject:],其 documentation 表示

aSelector should identify a method that takes a single argument of type id.

所以你违反了API的合同。

如果你看-[NSObject performSelector:]-[NSObject performSelector:withObject:]-[NSObject performSelector:withObject:withObject:]方法在Objective-C运行时的source code,它们都很简单objc_msgSend 周围的包装器——它们中的每一个都将 objc_msgSend 强制转换为具有适当数量的 id 参数的方法的实现将具有的函数类型。他们能够执行这些强制转换,因为他们假设您传递的选择器对应于具有适当数量 id 参数的函数,如文档中指定的那样。

当您调用 objc_msgSend 时,您必须调用它 ,就好像 它具有被调用方法的底层实现函数的类型。这是因为 objc_msgSend 是一个用汇编语言编写的蹦床,它使用所有寄存器和堆栈 space 调用实现函数,参数与调用 objc_msgSend 完全相同,因此调用者必须完全按照被调用者(底层实现函数)的预期设置参数。这样做的方法是将 objc_msgSend 转换为该方法的实现函数将具有的函数指针类型(考虑其参数和 return 类型),然后使用该函数进行调用。

对于所有效果和目的,我们可以将对 objc_msgSend 的调用视为与直接对底层实现函数的调用相同(即我们可以将 ((id(*)(id, SEL, id))objc_msgSend)(self, sel, obj) 视为与 ((id(*)(id, SEL, id))[self methodForSelector:sel])(self, sel, obj)).因此,将 performSelector:withObject: 与参数较少的方法一起使用的问题基本上可以归结为:使用参数比函数实际参数更多的类型的函数指针调用 C 函数是否安全(即函数指针类型拥有函数拥有的所有参数,具有相同的类型,但它在末尾有额外的参数)?

根据 C 标准,对此的一般回答是不,使用不同类型的函数指针调用函数是未定义的行为。例如,参见C99标准第6.5.2.2节第9段:

If the function is defined with a type that is not compatible with the type (of the expression) pointed to by the expression that denotes the called function, the behavior is undefined.

然而,对于使用 Objective-C 的所有平台(32 位和 64 位 x86;32 位和 64 位 ARM),我相信函数调用约定是这样的 对于调用者设置参数比被调用者预期更多的函数调用是安全的,额外传递的参数将被简单地忽略(被调用者不知道它们在那里,但是这没有任何负面影响;即使被调用者使用寄存器和堆栈 space 作为其他参数的额外参数,被调用者仍然可以这样做)。我没有详细检查 ABI,但我相信这是真的。

但是,如果 Objective-C 被移植到一个新平台,您将需要检查该平台的函数调用约定以确定调用者使用比被调用者预期更多的参数进行调用是否会导致任何该平台上的问题。您不能假设它适用于所有平台。