Objective-C: Logging Messages to Nil

This post will make this post useful.

Jim Correia had asked the lazytwitter how to log all method invocations against nil objects.

Some background:

Objective-C is a “nil eats messages” language. That is, if you invoke a method on an object reference that is nil, the runtime will eat the method. It is mostly invisible, only causing outright failures for certain return types that are larger than a pointer — structs, floats, doubles, etc… — at which point, the return value is undefined. Actually, the return value behavior for message to nil isn’t quite that simple.

Regardless of whether you think “nil eats messages” is the correct behavior, that it exists means that you’ll invariably be caught in a situation where a receiver is surprisingly nil. When this happens, it can be a pain to debug for a number of reasons.

First, there may be any number of other perfectly legitimate places where a nil receiver is valid and expected, both within your code and in any frameworks your code uses.

Next, any failures related to “omgnilwtfbbq?” often come up well removed from the original point at which somthing was surprisingly nil. A chain of nil results cascading into messages to nil references may often only be differentiated from the working state by the blazing speed with which a message-to-nil is executed!

Finally, there isn’t a useful spot to set a breakpoint to catch messages to nil. Breaking on objc_msgSend() is just a bit too coarse grained, regardless of whether you can figure out how to set a conditional break point to break only on messages to nil (which works, but is really really really slow).

Some mechanism for catching, logging, and breaking on messages to nil (and only messages to nil) is needed for the rare — but often insanity inducing — case where you need to track down a bug of this nature.

The Objective-C runtime provides a mechanism for doing exactly that. An unsupported, undocumented, and very likely to change in the future mechanism. You can inspect the implementation in depth by grabbing the source tarball from Apple’s source repository (10.5 Objective-C 2.0 sourceball isn’t up yet, unfortunately).

The best way to explain is through a demonstration. Create a standard Cocoa application project in Xcode and replace the contents of the main.m file with the following code.

#import <cocoa /Cocoa.h>
#import <objc /runtime.h>
extern id _objc_setNilReceiver(id newNilReceiver);

@interface NilEatAndSpew
@end

@implementation NilEatAndSpew
+ (BOOL)resolveClassMethod:(SEL)sel;
{
    fprintf(stdout, "+ [<nil> %s]\n", sel_getName(sel));
    return NO;
}

+ (BOOL)resolveInstanceMethod:(SEL)sel;
{
    fprintf(stdout, "- [<nil> %s]\n", sel_getName(sel));
    return NO;
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
    return [NSObject instanceMethodSignatureForSelector: @selector(description)];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation;
{
//    fprintf(stdout, "[<nil> %s]\n", sel_getName([anInvocation selector]));
}
@end
 
int main(int argc, char *argv[]) {
    NSAutoreleasePool *p = [[NSAutoreleasePool alloc] init];
    id nilEater = class_createInstance(objc_getClass("NilEatAndSpew"), 0);
    _objc_setNilReceiver(nilEater);
    [p drain];
    return NSApplicationMain(argc,  (const char **) argv);
}

Some things of note:

  • This is completely undocumented and unsupported. It is a debugging tool only. Don’t go relying on this or you will hurt yourself. In particular…
    extern id _objc_setNilReceiver(id newNilReceiver);
    

    …is the root of the evil. _objc_setNilReceiver() is not available in any header. The rest of the code is header clean.

  • A lot of stuff in the various ‘kits rely on nil-eats-message. Often. You will likely want to instrument this code to filter all but the handful of selectors that are causing issue. Not hard to do; just use sel_isEqual().
  • The above example is configured to print each selector that is invoked on nil once, the first time it is invoked. If you want to see the full monty, comment out the two dynamic resolver methods and uncomment the fprintf() line in -forwardInvocation:.
  • Apps crash on quit in the presence of this code. I didn’t look into why as this code is “debug only” anyway. Likely, setting the nil handler to nil in the App’s delegate’s terminate method will fix it. Likely.
  • The implementation of -methodSignatureForSelector: is a total hack. It simpler returns a signature for a method that takes no args and, thus, will cause the least amount of traipsing about memory. Since the -forwardInvocation: implementation prevents the invocation of the invocation, the totally bogus signature is never really used.
  • Did I mention this is a complete hack? … and that it uses functionality that is not supported for a reason? Do not be surprised if it destabilizes things in potentially surprising ways. Normally, a method call on a nil receiver takes only a few instructions for objc_msgSend() to short-circuit the call. This hack incurs method calls, I/O and allocations. Beyond the above several orders of magnitude overhead, lots of stuff is tickled from an otherwise typically unexpected state — the middle of a method call.

But, caveats and limitations included, it is still a neat hack…



9 Responses to “Objective-C: Logging Messages to Nil”

  1. Adrian Milliner says:

    I’ve not tried this but could you use dtrace (and Instruments) to trace entry to objc_msgSend where arg0 is null ?

    The probe/instrument would look something like: pid::obj_msgSend:entry and arg0 would be available to the script that runs. You also have the dtrace ustack() method to show where you are.

    Sorry, I haven’t done any D for a while and almost none on OSX, so little vague on detail – but a good google should help.

    Note that dtrace probes execute *much* faster than a debugger or profiler, so maybe worth doing.

  2. Michael Tsai - Blog - Logging Messages to Nil says:

    [...] Bill Bumgarner shows how to use the private _objc_setNilReceiver() function and -resolveInstanceMethod: to log the first time each selector is sent to nil. I imagine that by uncommenting the print statement in -forwardInvocation: this could be made to work with Objective-C 1.x, but you’d get a log entry for every message sent to nil. [...]

  3. Kevin Ballard says:

    dtrace executes faster, but objc_msgSend may be called too frequently for dtrace to keep up. It’s not meant for incredibly high-volume stuff. That said, it may still be worth a try.

  4. Adrian Milliner says:

    ‘Keep up?’ it’ll trap every single call; dtrace is not (necessarily) a sampling profiler.

    a dtrace script like:

    pid$1::objc_msgSend:entry
    /arg0==0/
    {
      ustack();
    }
    

    make little difference to the Finder’s performance, but does really slow down Address Book (as it sends messages to nil tens of thousands of times in a few seconds).

  5. Kevin Ballard says:

    Sure, it traps every call, but if the dtrace kernel buffer gets filled it will start discarding events. But trapping messages to nil should be fine. Logging a message for every single invocation of objc_msgSend, on the other hand, would probably start dropping them on the floor.

  6. leeg says:

    Nice example, and thanks to Adrian for that D script :-). BTW bbum, HTML has swallowed the filenames in your #import lines.

  7. Macinsoft - Logging Messages to Nil says:

    [...] bbum’s weblog-o-mat: “Some mechanism for catching, logging, and breaking on messages to nil (and only messages to nil) is needed for the rare—but often insanity inducing—case where you need to track down a bug of this nature.” Comments RSS | Trackback URL [...]

  8. Daniel Bocksteger says:

    I tried this on iOS but it doesn’t work properly… I get always an “undefined symbols for architecture armv7 _objc_setNilReceiver”

    Is there any way to solve this ? I would be very happy to get this running on iOS :(

  9. Sandeep says:

    I tried it with just an array without actually allocating an instance and sending message to it. This works great. A great debugging tool and a good trick. Thanks for this.

Leave a Reply

Line and paragraph breaks automatic.
XHTML allowed: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>