Let's find out!
Here's what I do know. When you send a message to an object, such as [someObject doSomething], the compiler compiles that into a function call to the objc_msgSend() function (or objc_msgSend_stret() if the method is declared to return a struct; more on that below). So what does objc_msgSend() do with nil?
(I am displaying a few snippets of that code in this post. All such code is Copyright Apple Computer.)
Where's the objc_msgSend() function? Oh, there it is; search for "ENTRY _objc_msgSend".
ENTRY _objc_msgSend ; check whether receiver is nil cmplwi r3,0 ; receiver nil? beq- LMsgSendNilSelf ; if so, call handler or return nil
So the first instruction cmplwi r3,0 compares r3 (that's the object) against 0, and the second instruction beq- LMsgSendNilSelf jumps to LMsgSendNilSelf if they were equal.
Ok, so what's this LMsgSendNilSelf thingy? Search for it within the file...ah-ha, there it is. This is where we go if the message receiver is nil, but what does it do?
LMsgSendNilSelf: mflr r0 ; load new receiver bcl 20,31,1f ; 31 is cr7[so] 1: mflr r11 addis r11,r11,ha16(__objc_nilReceiver-1b) lwz r11,lo16(__objc_nilReceiver-1b)(r11) mtlr r0 cmplwi r11,0 ; return nil if no new receiver beqlr mr r3,r11 ; send to new receiver b LMsgSendReceiverOk
The first six instructions within LMsgSendNilSelf are just there to load the global variable __objc_nilReceiver into register r11 (yeah, six instructions just to load a variable! Globals with position independent code sure isn't pretty). Then it compares that variable against nil, via cmplwi r11, 0, and if it was nil the whole objc_msgSend() function returns. No return value; it just returns.
(If __objc_nilReceiver were not nil, it moves __objc_nilReceiver into r3, where the message receiver goes, and acts like the receiver was __objc_nilReceiver all along, meaning that __objc_nilReceiver is there to let you replace the default message-to-nil behavior with your own! How cool is that!?)
What gets returned
So to sum up, if you send a message to nil, it's as if you executed a C function that just called return without specifying any return value. So what return value does that give us? I guess we need to know how Mac OS X on the PowerPC returns values.
Well, that depends on the type the function is declared to return! That stuff is all documented in the lovely PowerPC Runtime Architecture Guide, which is also hiding out somewhere in /Developer on your system. Ugh, what a mess. Ok, here's what we're after: Function Return. This is what it says, paraphrased:
- If a function returns a pointer or an integral type (other than long long), the return value goes in r3, the fourth general purpose register.
- If a function returns a floating point value, the return value goes in FPR1, a floating point register.
- If a function returns long long, the upper half goes in r3, the lower half in r4.
- Struct returning works like this: the caller makes space for the return value and sticks a pointer to the space in r3 (and therefore starts putting parameters in r4 rather than r3), and the callee is responsible for copying its return value into that space. Now we see why struct returns need that special objc_msgSend_stret() function: all the parameters are one register off from where they usually are.
But wait, isn't r3 the same register where we put the object we sent the message to in the first place? Sure is! So when you send a message to nil where the method is declared to return an object, you get back the very same nil for the return value. The function doesn't touch the object parameter, and since the return value is in the same place as the receiver parameter to objc_msgSend(), if the receiver is nil then so is the return value. Lucky!
Types, types, types
And since ints, shorts, chars, and longs are also returned in r3, the same thing happens. Sending a message to nil where you expect to get any of those types back will always give you 0.
What about long long, in which case the return value is half in r3 and the other half in r4? Well, since objc_msgSend() doesn't touch r4 either, what we send is what we get back, and in this case we send the selector in r4. So for the case of a long long, we expect to get the top 32 bits all 0 and the bottom 32 bits equal to the selector we sent. We will never get 0 back if we send a message to nil and expect a long long. Instead, we get the selector converted to a long long!
And floating point? objc_msgSend() doesn't touch the floating point registers, so we'll get back whatever happened to be in FPR1 when we sent the message.
Ok, now for the trickiest case: structs. objc_msgSend_stret() behaves identically to objc_msgSend() with regard to a nil-receiver. And remember that it's the callee's responsibility to copy the return value into the caller provided space. Since objc_msgSend_stret() neglects to do that, the data in the space provided by the caller is unchanged. This often means that code like myPoint = [nil someMethodReturningPoint]; will leave myPoint unchanged (but this behavior is dependent on the compiler's optimizations, and so you shouldn't rely on it).
Phew, my hat's off to you if you trudged through all that. Hopefully you found something new to you.