Cocoa and Objective-C: Up and Running (by me) is now available from O'Reilly.

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.
Design Element
Dynamic Objective-C with NSInvocation
Posted May 13, 2006 — 14 comments below




 

Romain Guy — May 13, 06 1224

I am just a beginner in Objective-C but this seems very much like the reflection in Java that lets you create a Method object that you can then invoke() on any given instance. What are the differences (granted you know them)?

Scott Ahten — May 13, 06 1225

Notice that Scott's example doesn't indicate what class is actually implementing the method. Due to the dynamic nature of Objective-C, the object passed to setTarget: could be any object, not just self or a object of a particular class.

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

You don't need to ask the class for the method signature, you can ask any object for the method signature corresponding to a selector:

sig = [self methodSignatureForSelector:mySelector];

jonnie savell — Aug 06, 06 1506

Thank you.

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

Here's a complete example:


@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

an excellent post. it's proven useful for catching accidental missing methods. here's a (perhaps naive) implementation:

- (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 16, 08 6494

This is the most beautiful web page I've ever seen in my life.
Great article too.

Scott Stevenson — Oct 17, 08 6496 Scotty the Leopard

@Kevin: This is the most beautiful web page I've ever seen in my life.
Great article too.


Kevin, much appreciated. Thanks.

Chris — May 23, 09 6779

Scott,
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 04, 09 6802

Thank you! Saved me a few more hours of really strange bugs!

Trenton Ahrens — Jun 25, 09 6817

a function i threw together for a project to store a message for later use

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

Almost right! What you're doing when you set the arguments is passing the address of the POINTERS of the objects, not the objects themselves because remember, in Objective-C, what we usually call objects are technically pointers. So in essence, you're passing a pointer to a pointer (a.k.a. an objective-c class).

Dalmazio Brisinda — Oct 27, 09 6971

Great article.

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 23, 09 7014

Nice piece of explanation. Better than Apple's. What would be interesting is talk about performance and introspection, especially on the iPhone. Does anyone know a solid benchmark available on the web?




 

Comments Temporarily Disabled

I had to temporarily disable comments due to spam. I'll re-enable them soon.




Technorati Profile
Copyright © Scott Stevenson 2004-2008