Jeremy W. Sherman

stay a while, and listen

Introducing OS Object

GCD came along with 10.6 and made concurrent programming easy. ARC came along with 10.7 and let us mostly forget about this whole refcounting business.

But 10.7’s GCD was left behind in manual retain-release land. (XPC was too, but GCD is our hero this time.) 10.8 fixed that oversight via a clever hack hidden away in <os/object.h>.

Behold, I bring you an object!

The magic happens in the interaction between two macros, OS_OBJECT_DECL and OS_OBJECT_DECL_SUBCLASS.

OS_OBJECT_DECL is used to declare the base object type of your refcounted C library. It conceptually creates a new root class:

OS_OBJECT_DECL(dispatch_object);

Once you’ve declared a root class using OS_OBJECT_DECL, you use OS_OBJECT_DECL_SUBCLASS to declare new subclasses:

OS_OBJECT_DECL_SUBCLASS(dispatch_queue, dispatch_object);
OS_OBJECT_DECL_SUBCLASS(dispatch_source, dispatch_object);

And magically, you now have types dispatch_object_t, dispatch_queue_t, and dispatch_source_t.

Presto-Changeo

As far as casts are concerned, these new types behave just like NSObject, NSString, and NSNumber. If you declare variables like so:

NSObject *o;
NSNumber *n;
NSString *s;

The compiler will allow you to implicitly upcast without complaint:

/* hunky dory */
o = n;
o = s;

but not down or crosswise:

dispatch_cast.m:13:4: warning: incompatible pointer types
assigning to 'NSNumber *__strong' from 'NSObject *__strong'
    [-Wincompatible-pointer-types]
        n = o;
          ^ ~
dispatch_cast.m:14:4: warning: incompatible pointer types
assigning to 'NSNumber *__strong' from 'NSString *__strong'
    [-Wincompatible-pointer-types]
        n = s;
          ^ ~

Similarly, with these declarations:

dispatch_object_t o;
dispatch_queue_t q;
dispatch_source_t s;

This is fine:

/* hunky dory */
o = q;
o = s;

But this is not:

q = o;
q = s;

The error messages hint at how this is implemented:

dispatch_cast.m:27:4: warning: incompatible pointer types
assigning to '__strong dispatch_queue_t'
(aka 'NSObject<OS_dispatch_queue> *__strong')
from '__strong dispatch_object_t'
(aka 'NSObject<OS_dispatch_object> *__strong')
    [-Wincompatible-pointer-types]
        q = o;
          ^ ~
dispatch_cast.m:28:4: warning: incompatible pointer types
assigning to '__strong dispatch_queue_t'
(aka 'NSObject<OS_dispatch_queue> *__strong')
from '__strong dispatch_source_t'
(aka 'NSObject<OS_dispatch_source> *__strong')
    [-Wincompatible-pointer-types]
        q = s;
          ^ ~

OSObject Is Protocols!

And that’s the trick, you see. There aren’t any classes, just protocols. Because protocols can be declared as conforming to other protocols, we have a protocol hierarchy parallel to our class hierarchy. By using a protocol-qualified type – NSObject<OS_dispatch_queue> * meaning, “Any NSObject, so long as it conforms to OS_dispatch_queue” – we can make our hierarchy concrete in terms of which OS objects can be pointed at by which pointers.

Why NSObject and not id? Because ARC needs to be able to use retain/release/autorelease, and NSObject provides a convenient declaration of those and other methods.

Or Is It?

Of course, there would have to be more to this OSObject thing than just protocols for ARC to work: whatever type-level hackery you might perpetrate, the message send [pointer retain] is only going to work if the whole Objective-C message send machinery can use what’s at *pointer as an Obj-C object.

Consequently, things look a lot different from inside libdispatch. There are covert class interfaces and corresponding implementations that go along with the public protocols.

A shame you can’t just sprinkle a few macros over a C library that uses refcounting and have it work automagically with ARC. Now, there’s a thought…