Callable Objects in Cocoa: NSInvocation
In Java 5 the java.util.concurrent.Callable interface was introduced. This is a generic interface that works like a Runnable, except that it returns a value and can throw checked exceptions.
Using Cocoa, we have a tool that can be used for similar purposes in Objective-C called NSInvocation. We use it transparently in the forwarding implementation of the Strategy Pattern, but don't really get into the details of how it works. Here we'll do that and set one up manually, as well as develop a factory-style method for generating them "on" the fly using variable argument lists.
First, let's build a Math class for testing purposes. It won't do much: just add two NSNumbers using their double values.
Math Header
Math Class File
Note that we made this an instance-level method on a Singleton. This is done for the sake of cleanliness and so we can extend the pattern later if we so choose.
Now, for the sake of illustration, we will create a function which adds two numbers and returns the result:
- #import "Math.h"
- ...
- NSNumber *retval;
- Math *mathInstance = [Math instance];
- SEL selector = @selector(addNSNumber:withNumber:);
- [invocation setTarget:mathInstance];
- [invocation setSelector:selector];
- [invocation setArgument:&arg1 atIndex:2];
- [invocation setArgument:&arg2 atIndex:3];
- [invocation invoke];
- [invocation getReturnValue:&retval];
- return retval;
- }
Let's break this down step at a time:
- Line 6:
NSNumber *retval -
Here we are creating a pointer to store the return value. We want to do this ahead of time, though truthfully we can do it at any point before we call
[NSInvocation getReturnValue:].
- Line 8:
Math *mathInstance = [Math instance] - Stores a copy of the math instance from the Singleton.
- Line 10—11:
NSNumber *arg1 =... - Declaring the arguments that we are going to use.
- Line 13:
SEL selector = @selector(addNSNumber:withNumber:) - Creates the selector object for the specific method. Note that it has the type
SELand includes all of the parts of the message (bothaddNSNumber:andwithNumber:. - Line 15:
NSMethodSignature *sig = [mathInstance methodSignatureForSelector:selector] - In order to create the
NSInvocationwe need a copy of theNSMethodSignature.Note that we could have also used
[[mathInstance class] instanceMethodSignatureForSelector:selector]here, but that it wouldn't work with classes that employ forwarding, such as our previous implementation of the Strategy Pattern.
- Line 16:
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:sig] - Creates the actual invocation object. While the invocation object has been created by this step, we still need to configure it.
- Line 18:
[invocation setTarget:mathInstance]; - Sets the Target, or object which will execute the method.
- Line 19:
[invocation setSelector:selector] - Sets the Selector that we used to create the
NSMethodSignaturein Line 15. - Line 20-21:
[invocation setArgument:&arg1 atIndex:2] - Set the arguments. Notice how the custom arguments start from index 2. This is because indices 0 and 1 represent
selfand_cmd, respectively. This is similar to Python's convention of passingselfas the first argument to a method, but is hidden from the user.The other thing to notice, for people who don't have a strong C background, is how the reference to the argument is used by placing an ampersand (
&) in front of it.This concludes the configuration.
- Line 22:
[invocation invoke] - Now that the
NSInvocationobject is configured, we can invoke it. If we hadn't declared the target earlier, we could use theinvokeWithTarget:method. - Line 23:
[invocation getReturnValue:&retval] - Get the return value the same way we gave it objects in Lines 20 and 21.
This opens the door for a lot of black magic. It allows us to call methods like [NSObject performSelectorOnMainThread:withObject:waitUntilDone:] with selectors that have more than one argument, or delay execution to happen at a specific time. These are topics we will address more later.
NSInvocation Creation Pattern
It looks like most of this code is just boilerplate, so we should be able to produce a factory-style method to generate these as-needed, taking advantage of the previously discussed variable argument lists:
- return [NGInvoker createInvocationOnTarget:target selector:selector withArguments:nil];
- }
- + (NSInvocation *)createInvocationOnTarget:(id)target selector:(SEL)selector withArguments:(id)arg1, ... {
- [invocation setTarget:target];
- [invocation setSelector:selector];
- if(arg1) {
- va_list args;
- [invocation setArgument:&arg1 atIndex:2];
- id obj;
- int ct = 3;
- NSLog(@"%@", obj);
- [invocation setArgument:&obj atIndex:ct];
- ct++;
- }
- }
- return invocation;
- }
Using this, we can then replace everything from Line 15 down with:
- NSInvocation *invocation = [NGInvoker createInvocationOnTarget:mathInstance selector:selector withArguments: arg1, arg2, nil];
- [invocation invoke];
- [invocation getReturnValue:&retval];
- return retval;
