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

时间:2017-09-08 19:13:42

标签: objective-c selector

我开始学习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:
// https://stackoverflow.com/q/7017281/946814
- (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事件处理程序,其中接受事件是可选的。但是,根据我对裸机的理解,我相信这个参数会放在堆栈上,并且不知道运行时如何知道它应该被丢弃。

2 个答案:

答案 0 :(得分:2)

  

但是,从我对裸机的理解来看,我相信这个论点会放在堆栈上,并且不知道运行时如何知道它应该被丢弃。

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

然而,这还不足以让你知道你的代码会起作用, callee 需要知道参数在堆栈中的位置。当项目被推到它上面时,堆栈通常会向下增长,而被调用者将参数定位为堆栈指针的正偏移量。如果从左到右推动参数,则最后一个参数位于堆栈指针的最小偏移处,第一个参数位于最大偏移处。如果在此场景中推送了其他参数,则预期参数的偏移量将全部更改。但是(Objective-)C支持可变参数函数,那些采用未指定数量的参数(想象printfstringWithFormat:等),因此调用中的参数从右向左推, 至少对于可变函数,因此第一个参数是最后一个参数,因此与堆栈指针的已知常量偏移量无论推送了多少个参数。

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

因此,您的Objective-C方法调用将成为对可变运行时函数的调用,如果您提供的参数太多,则被调用者会忽略它们并被调用者清除

希望所有这些都有意义!

<强>附录

在评论中@newacct正确地指出objc_msgSend不是变量的;我应该写有效的变量&#34;因为我为了简单而模糊了细节。他们还认为这是一个&#34;蹦床&#34;而不是一个功能;虽然这在技术上是正确的,但是trampoline本质上是一个跳转到其他代码而不是直接返回的函数,其他代码返回调用者(这与尾调用优化的类似)。

回到&#34;本质上是变量的&#34 ;: objc_msgSend函数,就像所有实现Objective-C方法的函数一样,采用第一个参数,该参数是调用该方法的对象引用,第二个参数是所需方法的选择器,然后按顺序获取方法所采用的任何参数 - 因此调用采用可变数量的参数但不严格地是可变参数函数

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

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

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

希望能让它更清晰,不会造成混乱!

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

答案 1 :(得分:1)

您正在调用-[NSObject performSelector:withObject:]表示

的方法aSelector
  

id应该标识一个采用单个参数的方法   输入-[NSObject performSelector:]

那么你违反了API的合同。

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

当您致电objc_msgSend时,您必须将其称为,就像它具有正在调用的方法的基础实现函数的类型一样。这是因为objc_msgSend是一个用程序集编写的trampoline,它调用实现函数,所有寄存器和参数的堆栈空间与调用objc_msgSend完全相同,因此调用者必须设置参数与被调用者(底层实现函数)完全一样。这样做的方法是将objc_msgSend转换为方法的实现函数将具有的函数指针类型(考虑其参数和返回类型),然后使用它进行调用。

对于所有效果和目的,我们可以考虑调用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段:

  

如果使用与。不兼容的类型定义函数   表达式的表达式类型(表达式)   被称为函数,行为未定义。

对于所有使用Objective-C的平台(32位和64位x86; 32位和64位ARM),我相信函数调用约定是对于调用者来说是安全的,可以使用比被调用者期望的更多参数来设置函数调用,并且将忽略额外传递的参数(被调用者不会知道他们在那里,但是并没有这样做。 t有任何负面影响;即使被调用者使用寄存器和堆栈空间用于其他事物的额外参数,无论如何都允许被调用者执行此操作)。我没有详细研究过ABI,但我相信这是真的。

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