1Raise your hand if you’re an Objective-C developer and you’re sick of writing this:
if([obj respondsToSelector:@selector(foo)])
{
id foo = [obj foo];
}
Unless you’ve switched to full-time Swift, in which case you can do this:
let foo = obj.foo?()
I want that, but for Objective-C.
id foo = [[obj please] foo];
I had been thinking about doing this for years, but only actually tried recently.2 It turns out it’s not very difficult, but we’ll have to cover a lot of topics first:
- Objective-C Message Dispatch
- Forwarding
- NSInvocations
- Boxing
ObjC Message Dispatch — A (largely incomplete) summary
Messages are the fundamental feature of Objective-C, far more than objects and class inheritance; classes are just the basic mechanism for dispatching messages. It probably shoud have been called “Messaging-C”.
Sending a message to an object goes like this:3 4
- Messages are compiled to objc_msgSend().
- objc_msgSend() looks for an implementation in the Class’s Methods
- No implementation? Is there another object that can handle it?
- Nobody? Maybe we can just create a new Method out of nowhere?
- Well then,
doesNotRespondToSelector:
. ☠️.
Let’s examine it step by step.
objc_msgSend
At compile time, clang replaces all messages with objc_msgSend()
calls.
[obj foo]
➡️ objc_msgSend(obj, "foo")
There are a lot of gory details regarding how parameters and return values are passed. In particular, methods that return struct
s are compiled to a different version of objc_msgSend()
:
[view frame]
➡️ objc_msgSend_stret(view, "frame")
The reason for this is that functions that return objects (id
) put their return value in a register. This isn’t possible for struct
s, for which we must allocate space on the stack. 5
Inside objc_msgSend()
, the regular method lookup works like one would expect: it uses the object’s isa
(its class pointer) and the SEL
(the selector, or method name6) to find the IMP
(the method implementation) which is just a function pointer of the form:
*(id self, SEL _cmd, ...)
The two first args are the implicit arguments available in all Objective-C methods: self
, and _cmd
. With the types of the other arguments and the return value, they form the Method Signature, which is expressed as an Objective-C Type encoding. For example:
- (void)foo:(int)bar; // encodes as "v@:i"
v
means the return type isvoid
,@
the first argument is anid
(self
),:
the second argument is aSEL
(_cmd
),i
the third argument (the first explicit arguemnt) is anint
.
objc_msgSend
then jumps to the address of the IMP
function, and program execution continues from here7.
Forwarding
That was just the regular case. But what if there’s no IMP
? Enter Message Forwarding.8
In the forwarding mechanism, messages are objects, too. An NSInvocation is an object that represents an actual message (a selector) send to an object (its target, or receiver), with its arguments.
NSInvocations are strange and beautiful beasts. They can be seen are blocks, or closures: they are Function Objects. But NSInvocations can also be worked with entirely at runtime. As clang emits the correct call to obj_sendMsg
, NSInvocation setups the call, but at runtime.9
Using NSInvocation goes like this:
NSInvocation * invocation = [NSInvocation invocationWithMethodSignature:/*...*/];
invocation.target = (id) ;
invocation.selector = (SEL) ;
[invocation setArgument:(void*) atIndex:(NSInteger)];
...
[invocation invoke];
This iss how you setup an NSInvocation yourself in order to send a message.
When you receive an invocation, that is how you set the return value for the caller:
...
id result = ...
[invocation setReturnValue:&result]; // a void*
NSInvocation copies the result, and uses the method signature to return it properly for objc_msgSend
or objc_msgSend_stret
.
Boxing
Let’s look at our proposed solution again:
id foo = [[obj please] foo];
It looks like -foo
returns an id
. What about those?
CGRect r = [[obj please] frame];
NSInteger i = [[obj please] count];
Moreover, I’ve purposedly left out an important detail:
What do we return if the method isn’t implemented?
When the method returns an object, nil
is the easy answer, but what about other types? We need a way to specify a fallback value. Something like:
[[obj pleaseOtherwiseReturn:42] count];
So. Boxing.
Boxing is what we use whenever we need an object, but really only have a scalar or a struct, or anything non-object.
In Objective-C, Boxing is “putting stuff in NSValue
” — NSNumber
s are NSValue
s, by the way. It’s used to store int
s in NSArray
s.
Key-Value Coding uses it all the time: the KVC primitives return objects, but sometimes the underlying values are structs. KVC automatically boxes them:
CGRect r = [view frame];
NSValue * v = [view valueForKey:@"frame"]; // Get the CGRect with -CGRectValue
Unsurprisingly, NSValue
is based on Objective-C Type Encodings. Any C or Obj-C value can be boxed in an NSValue
:
typedef struct { int x, y } MyStruct;
MyStruct myStruct = ...;
[NSValue valueWithBytes:&myStruct objCType:@encode(MyStruct)]
That’s what the @( <thing> )
syntax does at compile time. It just creates an NSValue, or directly an NSNumber with the contents.10
@implementation details
Here’s what I’d like to write:
[[obj performIfResponds] doSomething];
[[obj performOrReturn:@"foo"] foo];
[[obj performOrReturnValue:@YES] bar];
The “perform…” methods return a proxy object, that forwards messages to the original target, only if it responds to them. Otherwise, the proxy grabs the NSInvocation and sets a fallback return value.
The public methods are this NSObject category:
@interface NSObject (PerformProxy)
- (instancetype)performIfResponds;
- (instancetype)performOrReturn:(id)object;
- (instancetype)performOrReturnValue:(NSValue*)value;
@end
instancetype
, of course, is a lie, to make clang and Xcode happy: the returned object is a proxy. Additionally, we need two performOrReturn-
methods to deal with the boxing issues we’ve seen earlier: the underlying objc_msgSend
variant is different, the NSInvocation
has to match it.
Internally, the proxy object looks like that:
@interface PerformProxy : NSProxy
{
id _target;
const char * _returnType;
void(^_returnValueHandler)(NSInvocation* invocation_);
}
We’ll initialize a PerformProxy using the original receiver as the target and set the correct return type; let’s keep the NSInvocation handler aside for now..
In PerformProxy
itself, NSForwarding works in two steps: the first is the “fast path” that’s used to redirect the message to another object — that’s what we want, if the target actually responds to the selector — and the “slow path”, where we can actually play with the NSInvocation.
@implementation PerformProxy
...
// Try to forward to the real target
- (id)forwardingTargetForSelector:(SEL)sel_
{
if ([_target respondsToSelector:sel_]) {
return _target;
} else {
return self;
}
}
// Otherwise, handle the message ourselves
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel_
{
NSString * types = [NSString stringWithFormat:@"%s%s%s",_returnType,@encode(id),@encode(SEL)];
return [NSMethodSignature signatureWithObjCTypes:types.UTF8String];
}
- (void)forwardInvocation:(NSInvocation *)invocation_
{
_returnValueHandler(invocation_);
}
@end
What about _returnType
and _returnValueHandler
? They were passed to the PerformProxy at initialization, by the -performIfResponds
methods of the NSObject category. Again, it’s only useful in cases where the real target doesn’t respond to the message.
@implementation NSObject (PerformProxy)
// Messages returning `void`: no need to set a fallback return value.
- (instancetype)performIfResponds
{
return [[PerformProxy alloc] initWithTarget:self returnType:@encode(void)
returnValueHandler:^(NSInvocation* invocation_){}];
}
// Messages returning an object: simply set the fallback as the invocation return value.
- (instancetype)performOrReturn:(id)object_
{
return [[PerformProxy alloc] initWithTarget:self returnType:@encode(id)
returnValueHandler:^(NSInvocation* invocation_){
id obj = object_;
[invocation_ setReturnValue:&obj];
}];
}
// Messages returning a “value”: use the objcType of the passed NSValue, and “unbox” it into a local buffer to return it.
- (instancetype)performOrReturnValue:(NSValue*)value_
{
return [[PerformProxy alloc] initWithTarget:self returnType:value_.objCType
returnValueHandler:^(NSInvocation* invocation_){
char buf[invocation_.methodSignature.methodReturnLength];
[value_ getValue:&buf];
[invocation_ setReturnValue:&buf];
}];
}
@end
That’s all! Easy, right.
Well, OK, That wasn’t exactly trivial. That’s not much code, either. So what does it give us?
The good stuff
Existing patterns become easier to write, and easier to read.
We never use respondsToSelector:
anymore: all our delegates, and protocols with @optional
methods are sent through performIfResponds
, and we don’t worry about it anymore.
Another great usecase is for using recent API while maintaining compatibility with older systems. For example, here’s how we’re dealing with 3D Touch Quick Actions, while maintaining compatibility with iOS8:
UIApplication.sharedApplication.performIfResponds.shortcutItems = /* array of items */;
The -setShortcutItems:
message is simply ignored is UIApplication
doesn’t handle it. Before that, we used to write abstraction layers for compatibility.
It’s completely useless now.
We’re starting to use new coding patterns.
It’s OK not to check anything: just send the message, and if the receiver can handle it, that’ll work.
In the latest version of Captain Train for iOS, we’ve added a new feature called “Contextual Help”. The context is inferred from tags, and tags come from all kinds of objects: ViewControllers, because the the help isn’t the same in the search form or in the cart; and model objects, because there are specific help articles for e.g. Eurostar or Deutsche Bahn train tickets.
In the end, the easiest method was to simply “ask” objects for their help tags:
for(id obj in contextObjects) {
tags = [obj performOrReturn:nil].helpTags;
}
Instead of defining categories and superclasses, we just have abstract protocols.
PerformIfResponds
allows to specify a default implementation, and this is starting to look very similar to the Protocol-Oriented-Programming from that famous “Grumpy” talk at WWDC 2015.
The not-that-good stuff
Well, there are few drawbacks.
- Forwarding is slow. In the good scenario, when the target object does respond to the message, it’s 20 times slower than regular method dispatch. The slow path, with NSInvocation, is 100x slower.
- There’s no type safety. At all. It’s very easy to use the wrong
-performOrReturn:
variant, and then it’s Game Over. 11
What if we made a new language that made this kind of things easy, fast, and safe?
Oh, wait.
I’ve made the most “Objective-C” thing that I could, using dynamic message resolution, invocations, and other runtime features, and the end result a fundamental feature of Swift.
Mmm.
–
Source code is on github for your amusement, and I’m on Twitter for comments.
-
This is the blogpost version of a talk at Cocoaheads Paris in March 2016. Also, it may be my last post ever on Objective-C. Let’s make it count. ↩
-
The solution is very “Objective-C” in spirit. I’m disappointed to only find it now. ↩
-
See also the official documentation ↩
-
… and Bill Bumgarner’s tour of
objc_msgSend
, from the last decade, which is still largely relevant ↩ -
This is the dirty little secret of Objective-C: although we don’t know which method will be actually called until runtime, its return “variant” has to match the version of
objc_msgSend
that was chosen at compile time. Not so dynamic. ↩ -
Selectors are (almost) C string pointers. They’re uniqued at compile time, so they can be compared with
==
. ↩ -
it doesn’t call the implementation function, it jumps to it. That’s “tail call optimization”: the called method can return directly to the caller, without passing through
objc_msgSend
again. That’s also whyobjc_msgSend
doesn’t appear in call stacks. . ↩ -
There’s this terrific blogpost reverse-engineering how
__forwarding__
exactly works internally. ↩ -
By the way, blocks are objects too. And you can send
-invoke
to an NSBlock. ↩ -
Xcode 7.3 brings
objc_boxable
, which lets you declare boxing support for custom structures. ↩ -
Foundation logs a rather nice error message for this:
NSForwarding: warning: method signature and compiler disagree on struct-return-edness of 'someMethod'. Signature thinks it does not return a struct, and compiler thinks it does.
↩