Dynamic Objective-C with NSInvocation
Although Objective-C may seem very much like Java or C++ to the untrained eye, there's actually a lot of trickery just waiting to be tapped. The dynamic nature of the language allows us to bundle up a method call as an object, and either customize it or reuse it with different targets and arguments.First, let's create a method that we want to call:
- (NSString *) stringForDate: (NSDate *)date
usingFormatter: (NSDateFormatter *)formatter
{
// yes, this doesn't do anything interesting.
// just using it as a simple example
return [formatter stringFromDate: date];
}
A brilliant masterpiece, isn't it? Now, outside of this method, let's prepare an NSInvocation object. First, we need an Objective-C selector and a matching NSMethodSignature:
// get an Objective-C selector variable for the method
SEL mySelector;
mySelector = @selector(stringForDate:usingFormatter:);
// create a singature from the selector
NSMethodSignature * sig = nil;
sig = [[self class] instanceMethodSignatureForSelector:mySelector];
Notice that we ask [self class] for the method signature. This is because the instanceMethodSignatureForSelector: class method is built into NSObject.
Now, make NSInvocation object itself:
// create an actual invocation object and set the target
// to self
NSInvocation * myInvocation = nil;
myInvocation = [NSInvocation invocationWithMethodSignature:sig];
[myInvocation setTarget:self];
[myInvocation setSelector:mySelector];
Pretty straightforward. We use the NSMethodSignature as input, and set the target to "self". You can, of course, send messages to other objects by setting a different target here.
Now, we need to add the arguments for the method:
// add first argument
NSDate * myDate = [NSDate date];
[myInvocation setArgument:&myDate atIndex:2];
// add second argument
NSDateFormatter * dateFormatter = [[NSDateFormatter alloc] init];
[dateFormatter setDateStyle:NSDateFormatterMediumStyle];
[myInvocation setArgument:&dateFormatter atIndex:3];
Again, pretty simple. We create objects and simply assign them as arguments. We need to pass in the address of the objects, so we use the addressof operator (&).
One obvious question here is why do we add the first argument at index 2. The first argument should be index 0, right? Here's the answer:
Indices 0 and 1 indicate the hidden arguments self and _cmd, respectively; these values can be retrieved directly with the target and selector methods. Use indices 2 and greater for the arguments normally passed in a message.
Finally, we want to call the actual method and get a result:
// now activate the invocation and get the result
NSString * result = nil;
[myInvocation retainArguments];
[myInvocation invoke];
[myInvocation getReturnValue:&result];
NSLog(@"The result is: %@", result);
The result is written directly to the NSString variable. The output looks something like this:
The result is: 05/13/06
This may seem like an awkward way to send a message, but the point is that NSInvocation objects can be used to call arbitrary methods with arbitrary arguments which are determined at runtime, and the objects are mutable so you can send the same message to multiple targets easily, or just change arguments.
Dynamic Objective-C with NSInvocation
Posted May 13, 2006 — 14 comments below
Posted May 13, 2006 — 14 comments below
Romain Guy — May 13, 06 1224
Scott Ahten — May 13, 06 1225
Instances of Java's Method class represent a "fully qualified" method. That is, the instance is a reference to both the method signature and the class that implements it. As such, you can't successfully invoke the method on an instance just because it has a method with the same signature. The target must be an instance of the same class as well.
Selectors in Objective-C represent "messages" that can be sent to any object, regardless of what class they are instantiated from. As long as the target object responds to the selector, the invocation is successful. If the target object does not respond to the selector, an exception is thrown or the target can optional forward the message on to another object.
Ken — May 21, 06 1307
sig = [self methodSignatureForSelector:mySelector];
jonnie savell — Aug 07, 06 1506
I was playing around with NSInvocation to get around the argument number limitation in the performSelectorXXX methods.
I failed to pass in the addresses of the arguments and got the folowing announcement: *** Uncaught exception: <NSInvalidArgumentException> *** +[NSCFString length]: selector not recognized
I spent a lot of time trying to figure out the error but failed.
Finally, I ran to Google and found this page.
Again, thank you.
Robin — Sep 19, 06 1826
@interface CurrentDate: NSObject
{
}
- (NSString *) stringForDate: (NSDate *)date usingFormatter: (NSDateFormatter *)formatter;
@end
@implementation CurrentDate;
- (NSString *) stringForDate: (NSDate *)date usingFormatter: (NSDateFormatter *)formatter
{
// yes, this doesn't do anything interesting.
// just using it as a simple example
return [formatter stringFromDate: date];
}
@end
int main (int argc, char *argv[]) {
NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
NSDateFormatter * dateFormat = [[NSDateFormatter alloc] initWithDateFormat:@"%b %d %Y" allowNaturalLanguage: NO];
CurrentDate * currentDateClassObject = [[CurrentDate alloc] init];
NSString * currentDate = [currentDateClassObject stringForDate: [NSDate date] usingFormatter: dateFormat];
NSLog(@"currentDate: %@", currentDate);
// get an Objective-C selector variable for the method
SEL mySelector;
mySelector = @selector(stringForDate:usingFormatter:);
// create a singature from the selector
NSMethodSignature * sig = nil;
sig = [[currentDateClassObject class] instanceMethodSignatureForSelector: mySelector];
// create an actual invocation object and set the target to currentDateClassObject
NSInvocation * myInvocation = nil;
myInvocation = [NSInvocation invocationWithMethodSignature: sig];
[myInvocation setTarget: currentDateClassObject];
[myInvocation setSelector: mySelector];
// add first argument
NSDate * myDate = [NSDate date];
[myInvocation setArgument: &myDate atIndex: 2];
// add second argument
NSDateFormatter * dateFormatter = [[NSDateFormatter alloc] init];
[dateFormatter setDateStyle: NSDateFormatterMediumStyle];
[myInvocation setArgument: &dateFormatter atIndex: 3];
NSString * result = nil;
[myInvocation retainArguments];
[myInvocation invoke];
[myInvocation getReturnValue: &result];
NSLog(@"The result is: %@", result);
[pool release];
return 0;
}
Zak — Apr 07, 08 5709
- (void) forwardInvocation: (NSInvocation*)invocation
{
NSString* missingMethod = NSStringFromSelector([invocation selector]);
NSObject* object = [invocation target];
[invocation setArgument: &missingMethod atIndex: 2];
[invocation setArgument: &object atIndex: 3];
[invocation setSelector: NSSelectorFromString(@"methodMissing:object:arguments:")];
return [invocation invokeWithTarget:self];
}
-(void) methodMissing: (NSString*)method object:(id)object arguments:(NSMutableArray*)arguments {
println(@"%@ is missing %@", ((NSObject*)object).description, method);
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel
{
NSMethodSignature* sig = nil;
if ([self respondsToSelector:sel]) {
sig = [[self class] instanceMethodSignatureForSelector:sel];
}
else {
SEL methodMissingSelector = NSSelectorFromString(@"methodMissing:object:arguments:");
sig = [[self class] instanceMethodSignatureForSelector: methodMissingSelector];
}
return sig;
}
I wonder though if you know how to capture the arguments sent to a method without knowing the method signature? In this case, I'd like to actually capture the arguments being sent to the unknown method, put them into an array, and then send that along to my methodMissing method for later use. something like Ruby's method_missing.
any thoughts?
Kevin — Oct 17, 08 6494
Great article too.
Scott Stevenson — Oct 17, 08 6496
Great article too.
Kevin, much appreciated. Thanks.
Chris — May 23, 09 6779
thanks for you post. It helped me in two respects:
a) I over-read the information about having to start at index 2 with setArgument: in Apple's documentation.
b) I assumed, I wouldn't need to use setSelector: on the invocation as that information was already contained in the NSMethodSignature that I used to create the NSInvocation using invocationWithMethodSignature:
So thanks again,
Chris
Elliot — Jun 05, 09 6802
Trenton Ahrens — Jun 25, 09 6817
NSInvocation* storeMessage(id target, SEL selector, ...) { NSMethodSignature *sig = [[target class] instanceMethodSignatureForSelector:selector]; NSInvocation *nsInvocation = [NSInvocation invocationWithMethodSignature:sig]; [nsInvocation setTarget:target]; [nsInvocation setSelector:selector]; id eachObject; int cur = 2; va_list args; va_start(args, selector); while( eachObject = va_arg(args, id) ) { [nsInvocation setArgument:&eachObject atIndex:cur]; cur++; } va_end(args); [nsInvocation retain]; return nsInvocation; }
use:
NSInvocation* inv = storeMessage(targetObject, @selector(functionToCall), arg1, arg2, arg3, nil); [inv invoke];
Mark Donohoe — Aug 13, 09 6848
Dalmazio Brisinda — Oct 27, 09 6971
I was wondering if this would also work for invoking class methods on a class object? For example if +stringForDate:usingFormatter: was a class method instead of an instance method:
SEL mySelector; mySelector = @selector(stringForDate:usingFormatter:); NSMethodSignature * sig = nil; sig = [self methodSignatureForSelector:mySelector]; NSInvocation * myInvocation = nil; myInvocation = [NSInvocation invocationWithMethodSignature:sig]; [myInvocation setTarget:[self class]]; // <-- will this work? [myInvocation setSelector:mySelector];
and the rest given as above?
Best,
Dalmazio
douard Mercier — Dec 24, 09 7014