Ruby-like nil messaging in Objective-C

来源:互联网 发布:生猪体重精确计算法 编辑:程序博客网 时间:2024/05/22 06:08

http://ddeville.me/2013/06/ruby-like-nil-messaging-in-objective-c/?utm_source=iOS+Dev+Weekly&utm_campaign=iOS_Dev_Weekly_Issue_101&utm_medium=email


Ruby-like nil messaging in Objective-C

Nil messaging in Objective-C has a fairly well document behavior. In brief, messaging nilhas not effect and the return value from a message sent to nil is discussed in theProgramming 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_msgSendimplementation 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_msgSendDW_START _objc_msgSendNilTest NORMALGetIsaFast NORMAL       // r11 = self->isaCacheLookup 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_fLNilTestDone:.endmacro.macro NilTestSupport    .align 3LNilTestSlow:.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 methodsmethodSignatureForSelector: and forwardInvocation:. Since our implementation offorwardInvocation: 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 anNSInternalInconsistencyException 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.


原创粉丝点击