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 — 6 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?




 

Add Your Thoughts

Use UBB tags: [b] [i] [url] [code], and so on
Use [i] for quoting: [i]Quoted Text[/i]
Omit double quotes in links: [url=http://apple.com]Apple[/url]
Accented characters are currently stripped from names



Please enter the company who makes the Mac:
this is an anti-spam measure
Some comments may be edited for formatting, or removed if too far off-topic.




Technorati Profile
Copyright © Scott Stevenson 2004-2008