Ruby-like nil messaging in objective-c
Nil
messaging in Objective-C has a fairly well document behavior. In brief, messaging nil
has not effect and the return value from a message sent to nil
is discussed in the Programming with Objective-C
If you expect a return value from a message sent to nil, the return value will be nil for object return types, 0 for numeric types, and NO for BOOL types.
This is a very handy feature of Objective-C that might seem at first unfamiliar to Ruby programmers. In fact, invoking a method on nil
in Ruby gives a NoMethodError
as following:
NoMethodError: undefined method `length' for nil:NilClass
It’s a very different behavior from Objective-C that many Ruby programmers have come to rely on. I myself prefer the way nil
is handled in Objective-C but let’s assume for a second that we wanted to achieve the Ruby behavior in Objective-C.
Needless to say that it is probably a very bad idea but since it could be fun, let’s have a look at the ObjC runtime and give it a try!
We first need to find which part of the runtime is in charge of handling messages to nil
. As described very nicely in this post, a message to nil
is handled directly in objc_msgSend
.
Given that the ObjC runtime is open-source, we can have a look at the objc_msgSend
implementation for x86_64. It is assembly but, trust me, it is fairly readable.
Our first step is to find the entry point of objc_msgSend
that looks as following:
ENTRY _objc_msgSend
DW_START _objc_msgSend
NilTest NORMAL
GetIsaFast NORMAL // r11 = self->isa
CacheLookup NORMAL, _objc_msgSend // r11=method, eq set (nonstret fwd)
jmp *method_imp(%r11) // goto *imp
...
We can quickly notice that one of the first instruction is a NilTest
macro that itself looks as following:
.macro NilTest
.if $0 != STRET
testq %a1, %a1
.else
testq %a2, %a2
.endif
jz LNilTestSlow_f
LNilTestDone:
.endmacro
.macro NilTestSupport
.align 3
LNilTestSlow:
.if $0 != STRET
movq __objc_nilReceiver(%rip), %a1
testq %a1, %a1 // if (receiver != nil)
.else
movq __objc_nilReceiver(%rip), %a2
testq %a2, %a2 // if (receiver != nil)
.endif
jne LNilTestDone_b // send to new receiver
.if $0 == FPRET
fldz
.elseif $0 == FP2RET
fldz
fldz
.endif
.if $0 != STRET
xorl %eax, %eax
xorl %edx, %edx
xorps %xmm0, %xmm0
xorps %xmm1, %xmm1
.endif
ret
.endmacro
After a few tests for the return type (struct and floating-point need special handling), we can notice that a nil
receiver, if set (not nil
) is given a chance to act as the message receiver. Otherwise, a few registers usually holding return values are cleant and the function returns.
The __objc_nilReceiver
is not usually set but if we found a way to set it to an object that we create we could alter the behavior of nil
messaging!
Luckily, objc-private.h declares the following function:
extern id _objc_setNilReceiver(id newNilReceiver);
That’s it, if we call this function with our custom object we will able to intercept any message to nil
.
Our NilCatcher
class will only need to implement two methods methodSignatureForSelector:
and forwardInvocation:
. Since our implementation of forwardInvocation:
won’t actually need a valid NSMethodSignature
we will return the method signature of a basic method on NSObject
in methodSignatureForSelector:
. Eventually, we will throw an exception in forwardInvocation:
, logging the selector.
Instead of addign a new class in our project we will create the class at runtime and provide a couple of method implementations with blocks, just because it quicker and more fun. The code is shown below but also available as a gist which should be slightly easier to read.
#import <Foundation/Foundation.h>
#import <objc/runtime.h>
extern id _objc_setNilReceiver(id newNilReceiver);
static id _createNilCatcherObject(void)
{
Class NilCatcher = objc_allocateClassPair([NSObject class], "NilCatcher", 0);
NSMethodSignature * (^methodSignatureForSelectorBlock)(id, SEL) = ^ NSMethodSignature * (id _block, SEL selector) {
/*
We will not actually use the method signature in forwardInvocation so any signature will do it.
*/
return [NSObject instanceMethodSignatureForSelector:@selector(description)];
};
IMP methodSignatureForSelectorIMP = imp_implementationWithBlock(methodSignatureForSelectorBlock);
Method methodSignatureForSelectorMethod = class_getClassMethod([NSObject class], @selector(methodSignatureForSelector:));
class_addMethod(NilCatcher, @selector(methodSignatureForSelector:), methodSignatureForSelectorIMP, method_getTypeEncoding(methodSignatureForSelectorMethod));
void (^forwardInvocationBlock)(id, NSInvocation *) = ^ void (id _block, NSInvocation * invocation) {
@throw [NSException exceptionWithName:NSInternalInconsistencyException reason:[NSString stringWithFormat:@"Attempting to message %s to nil", sel_getName([invocation selector])] userInfo:nil];
};
IMP forwardInvocationIMP = imp_implementationWithBlock(forwardInvocationBlock);
Method forwardInvocationMethod = class_getClassMethod([NSObject class], @selector(forwardInvocation:));
class_addMethod(NilCatcher, @selector(forwardInvocation:), forwardInvocationIMP, method_getTypeEncoding(forwardInvocationMethod));
return [NilCatcher new];
}
int main(int argc, const char **argv)
{
@autoreleasepool {
id nilCatcher = _createNilCatcherObject();
_objc_setNilReceiver(nilCatcher);
[(id)nil isEqualToString:@"Cat"];
}
return 0;
}
And that’s it! If you build and run you should crash on an NSInternalInconsistencyException
when attempting to messaging nil
, which should make any Rubyist feel at home! ;)
I cannot stress enough on the fact that you should probably never even think of using this. The Cocoa frameworks surely rely heavily on nil
messaging being allowed and having no effect.
That said, it was a fun experiment and I hope you learnt something new.