Over the years I’ve helped a lot of folks investigate a lot of crashes. In some cases those crashes only make sense if you know a little about how Foundation and Core Foundation types are toll-free bridged. This post is my attempt to explain that.
If you have questions or comments, please put them in a new thread. Tag it with Foundation and Debugging so that I see it.
Share and Enjoy
—
Quinn “The Eskimo!” @ Devel
oper Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"
Crashes on the Toll-Free Bridge
Certain Core Foundation (CF) types are toll-free bridged to their Foundation equivalent. That allows you to pass the Foundation object to a CF routine and vice versa [1]. For example, NSData
and CFData
are toll-free bridged, allowing you to pass an NSData
object to a CF routine like CFDataGetBytePtr
. For more information on this topic, see Toll-Free Bridged Types within Core Foundation Design Concepts in the Documentation Archive.
This is cool, but it does present some interesting challenges. One of these relates to subclassing. Many of the toll-free bridge Foundation types support subclassing and, if you create a instance of your subclass and pass it to CF, CF has to do the right thing.
Continuing the NSData
example above, it’s legal [2] to create your own subclass of NSData
with a completely custom implementation. As long as you implement the -bytes
method and the length
property, all the other NSData
methods will just work. Moreover, this class works with CFData
routines as well. If you pass an instance of your subclass to CFDataGetBytePtr
, CF detects that it’s an Objective-C object and calls your -bytes
method.
Exciting!
So, how does this actually work? It relies on the fact that CF and Objective-C types share a common object header. That object header is an implementation detail, but the first word of the header is always an indication of the class [3]. CF uses this word to distinguish between CF and Objective-C objects.
Note This basic technique is used by other Objective-C compatible types, including Swift objects and XPC objects. If, for example, you call CFRetain
on Swift object, it detects that this is an Objective-C compatible object and calls objc_retain
, which in turns detects that this is a Swift object and calls swift_retain
.
To see this in action, check out the Swift Foundation open source. Continuing our NSData
example, the first line of CFDataGetBytePtr
uses the CF_OBJC_FUNCDISPATCHV
macro to check if this is an Objective-C object and, if so, call -bytes
on it.
Sadly, the open source trail goes cold here, because Objective-C integration is only supported on Apple platforms. However, some disassembly reveals the presence of an internal routine called CF_IS_OBJC
.
(lldb) disas -n CFDataGetBytePtr
CoreFoundation`CFDataGetBytePtr:
… <+0>: pacibsp
… <+4>: stp x20, x19, [sp, #-0x20]!
… <+8>: stp x29, x30, [sp, #0x10]
… <+12>: add x29, sp, #0x10
… <+16>: mov x19, x0
… <+20>: mov w0, #0x14
… <+24>: mov x1, x19
… <+28>: bl 0x19cc60100 ; CF_IS_OBJC
…
WARNING Do not rely on the presence or behaviour of CF_IS_OBJC
. This is an implementation detail. It has changed many times in the past and may well change in the future [4].
While this is an implementation detail, it’s useful to know about when debugging. If you pass something that’s not a valid object to CFDataGetBytePtr
, you might see it crash in CF_IS_OBJC
. For example, this code:
void test(void) {
struct { int i; } s = { 0 };
CFDataGetBytePtr( (const struct __CFData *) &s );
}
crashes like this:
Exception Type: EXC_BREAKPOINT (SIGTRAP)
…
Thread 0 Crashed:: Dispatch queue: com.apple.main-thread
0 CoreFoundation … CF_IS_OBJC + 76
1 CoreFoundation … CFDataGetBytePtr + 32
2 xxot … test + 24 …
…
In this case CF_IS_OBJC
has detected the problem and trapped, resulting in an EXC_BREAKPOINT
crash. However, if you pass it a pointer that looks more like an object, this might crash trying to dereference a bad pointer, which will result in a EXC_BAD_ACCESS
crash.
The other common failure you see occurs when you pass it an Objective-C object of the wrong type. Consider code like this:
void test(void) {
id str = [NSString stringWithFormat:@"Hello Cruel World!-%d", (int) getpid()];
const void * ptr = CFDataGetBytePtr( (__bridge CFDataRef) str);
…
}
When you run this code, it throws a language exception like this:
*** Terminating app due to uncaught exception 'NSInvalidArgumentException',
reason: '-[__NSCFString bytes]: unrecognized selector sent to
instance 0x6000028545a0'
*** First throw call stack:
(
0 CoreFoundation … __exceptionPreprocess + 176
1 libobjc.A.dylib … objc_exception_throw + 60
2 CoreFoundation … -[NSObject(NSObject) __retain_OA] + 0
3 CoreFoundation … ___forwarding___ + 1580
4 CoreFoundation … _CF_forwarding_prep_0 + 96
5 xxot … test + 92
…
)
CFDataGetBytePtr
has detected this is an Objective-C object and called -bytes
on it. However, this is actually an NSString
[5] and NSString
doesn’t implement the -bytes
method. The end result is an unrecognized selector
exception.
[1] To be clear, when using CF objects in Objective-C you first cast the CF object to its Foundation equivalent and then call Objective-C methods on it.
[2] While it’s legal to do this, it’s probably not very sensible. Subclassing Foundation types is something that might’ve made sense back in the day, but these days there are generally better ways to solve your problems.
[3] Historically this word was called isa
and was of type Class
, that is, a pointer to the actual Objective-C class. These days things are much more complex (-:
[4] Historically, CF_IS_OBJC
was very simple: If the object’s isa
word was 0, it was a CF object, otherwise it was an Objective-C object. That’s no longer the case.
[5] The actual type is __NSCFString
. That’s because NSString
is a class cluster. For more about that, see Class Clusters within Cocoa Fundamentals Guide in the Documentation Archive.