2

I'm starting to learn Objective-C, and wondering what happens if you pass in an object to a dynamic call to a method, where the method doesn't accept any.

#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;
}

So far, it doesn't seem to cause any issues, and in fact looks similar to Javascript event handlers, where accepting the event is optional. 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.

Bruno Kim
  • 2,300
  • 4
  • 17
  • 27

2 Answers2

2

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.

You are correct, the caller places the argument on the stack. After the call returns the caller removes the arguments it placed on the stack, so discarding any extra arguments the callee doesn't expect is not an issue.

However that is not enough to know your code will work, the callee needs to know where the arguments are on the stack. The stack usually grows downwards as items are pushed onto it and the callee locates arguments as positive offsets from the stack pointer. If arguments are pushed left-to-right then the last argument is at the smallest offset from the stack pointer, the first at the largest offset. If additional arguments are pushed in this scenario then the offsets to the expected arguments would all change. However (Objective-)C supports variadic functions, those that take an unspecified number of arguments (think printf, stringWithFormat:, etc.), and so arguments in a call are pushed right-to-left, at least for variadic functions, so that the first argument is the last pushed and hence at a known constant offset from the stack pointer regardless of how many arguments are pushed.

Finally an Objective-C method call is translated into a call to a runtime function, objc_msgSend(), which implements the dynamic method lookup. This function is variadic (as different messages take different numbers of arguments).

So your Objective-C method call becomes a call to a variadic runtime function, and if you supply too many arguments they are ignored by the callee and cleared up by the caller.

Hope all that makes sense!

Addendum

In the comments @newacct has correctly pointed out that objc_msgSend is not variadic; I should have written "effectively variadic" as I was blurring details for simplicity. They also argued that it is a "trampoline" and not a function; while this is technically correct a trampoline is essentially a function which jumps to other code rather than returning directly, that other code doing the return back to the caller (this is similar to what tail call optimisation does).

Back to "essentially variadic": The objc_msgSend function, like all functions which implement Objective-C methods, take a first argument which is the object reference the method is being called on, a second argument which is the selector of the desired method, and then in order any arguments the method takes - so the call takes a variable number of arguments but is not strictly a variadic function.

To locate the actual method implementation to invoke at runtime objc_msgSend uses the first two arguments; the object reference and the selector; and performs a lookup. When it locates the appropriate implementation it jumps/tail calls/trampolines to it. As objc_msgSend cannot know how many arguments have been passed until it has examined the selector, which is the second argument, it needs to be able to locate the second argument at a known offset from the stack pointer, and for this to (easily) possible arguments must be pushed in reverse order - just as with a variadic function. As the arguments are pushed by the caller in reverse order they have no impact on the callee and additional ones will be ignored and harmless provided the caller is responsible for removing the arguments after the call.

For variadic functions the caller has to be the one which removes the arguments, as only it knows how many are passed, for non-variadic functions the callee could remove the arguments - and this includes the callee that objc_msgSend tail calls - but many compilers, including Clang, have the caller remove them.

So the call to objc_msgSend, which is the compilation of a method call, will under Clang ignore any extra arguments by essentially the same mechanism as variadic functions do.

Hope that makes it clearer and doesn't add confusion!

(Note: In practice some arguments may be passed in registers and not on the stack, this does not have significant impact on the above description.)

CRD
  • 52,522
  • 5
  • 70
  • 86
  • "This function is variadic (as different messages take different numbers of arguments)." `objc_msgSend` is NOT a variadic function, and trying to pass arguments to it as if it were a variadic function will not always work. Rather, `objc_msgSend` is a trampoline that acts as the same function type as the implementing function that is called, and to use `objc_msgSend` correctly you need to first cast it to the appropriate implementing function type, and then pass the appropriate arguments to the resulting expression, in order for the compiler to construct the call correctly. – newacct Sep 18 '17 at 03:34
  • Although Objective-C supports variadic functions, that is irrelevant here as the functions here are not variadic, and the ABI for variadic and non-variadic functions may be different. Your argument that it works for variadic functions does not imply it works for non-variadic functions. C has variadic functions, but says if you try to call a function with a function pointer of the wrong type (e.g. wrong number or types of arguments, or wrong return type), it is undefined behavior. – newacct Sep 18 '17 at 03:39
  • @newacct - I probably should have thrown an "essentially" or two in the answer - sometimes blurring the edges helps, but you clearly believe the answer is simply wrong. Can you please add what you believe is the correct answer? Thank you. – CRD Sep 18 '17 at 10:56
  • @newacct - no alternative answer from you yet so I've added an addendum which explains the details I blurred over as the behavour is essentially the same as for variadic functions. Hope that clears things up, but please add your own answer if not! – CRD Sep 21 '17 at 02:03
1

You are calling the method -[NSObject performSelector:withObject:], whose documentation says

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

So what you are violating the contract of the API.

If you look at the source code of the -[NSObject performSelector:], -[NSObject performSelector:withObject:], and -[NSObject performSelector:withObject:withObject:] methods in the Objective-C runtime, they are all just simple wrappers around objc_msgSend -- each of them casts objc_msgSend to the function type that the implementation of the method with the appropriate number of id parameters would have. They are able to perform these casts because they assume that you are passing a selector corresponding to a function with the appropriate number of id parameters, as specified in the documentation.

When you call objc_msgSend, you must call it as if it had the type of the underlying implementing function of the method that is being called. That is because objc_msgSend is a trampoline written in assembly that calls the implementing function with all the registers and stack space for arguments exactly the same as in the call to objc_msgSend, so the caller must set up the arguments exactly as expected by the callee (the underlying implementing function). The way to do that is to cast objc_msgSend to the function pointer type that the implementing function of the method would have (considering its parameter and return types), and then make the call using that.

For all effects and purposes, we can consider a call to objc_msgSend the same as a call directly to the underlying implementing function (i.e. we can consider ((id(*)(id, SEL, id))objc_msgSend)(self, sel, obj) as the same as ((id(*)(id, SEL, id))[self methodForSelector:sel])(self, sel, obj)). So the question of using performSelector:withObject: with a method with less arguments basically reduces down to: Is it safe to call a C function with a function pointer of type that has more parameters than the function actually has (i.e. the function pointer type has all the parameters the function has, with the same types, but it has additional ones at the end)?

The general answer to this, according to the C standard, is that No, it is undefined behavior to call a function with a function pointer of a different type. For example, see C99 standard section 6.5.2.2 paragraph 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.

For all the platforms that Objective-C is used on, however (32-bit and 64-bit x86; 32-bit and 64-bit ARM), I believe the function calling convention is such that it is safe for a caller to set up a function call with more arguments than the callee expects, and the extra passed arguments will simply be ignored (the callee won't know they are there, but that doesn't have any negative effects; even if the callee uses the registers and stack space for those extra arguments for other things, a callee is allowed to do that anyway). I have not examined the ABIs in detail but I believe that this is true.

However, if Objective-C is ported to a new platform, you will need to examine the function calling convention for that platform to determine whether a caller making a call with more parameters than the callee expects will cause any problems on that platform. You cannot just assume that it will work on all platforms.

newacct
  • 119,665
  • 29
  • 163
  • 224
  • Knowing that we are depending on undefined behavior is a very welcome knowledge, thanks for your analysis. – Bruno Kim Sep 26 '17 at 17:50