Problems with Objective-C annotations

Datetime:2017-04-16 05:22:12         Topic: Objective-C          Share        Original >>
Here to See The Original Article!!!

April 15, 2017

By Jeff Johnson, Developer of Underpass

In recent years, the clang compiler has added a number of source annotations to Objective-C in order to clarify API intent. For example, NS_DESIGNATED_INITIALIZER formalizes the traditional Objective-C designated initializer pattern. To facilitate interoperability with Swift, Objective-C adopted nullibility annotations: nullable, _Nullable, nonnull, _Nonnull . As with all changes, however, there are tradeoffs. The use of these annotations comes with a price. Consider the following (dummy) code illustrating the designated initializer pattern (and nothing else):

Without annotations, the designated initializer is indicated by a comment, as is the requirement that the DNSServiceRef be non-NULL. The latter requirement is enforced at runtime. If you call -init , it raises an internal inconsistency exception, and if you call -initWithService: with a NULL argument, it crashes. You'll see *** CFRetain() called with NULL *** in the crash log. You could add NSParameterAssert for clarity, but the CFRetain is sufficient to enforce the API.

Now let's add an annotation:

-(instancetype)initWithService:(DNSServiceRef)service NS_DESIGNATED_INITIALIZER; // service must be non-NULL

Suddenly we get a build warning:

convenience initializer missing a 'self' call to another initializer [-Wobjc-designated-initializers]
-(instancetype)init

The rule of designated initializers is that all initializers must eventually call the designated initializer. However, -init isn't meant to be called at all, it raises an exception when called. Nonetheless, we can easily silence the warning:

-(instancetype)init
{
    return [self initWithService:NULL];
}

The NULL argument causes a crash when called, so we've again enforced the API contract at runtime. Now let's add nullability annotations:

-(nonnull DNSServiceRef)service;
-(nonnull instancetype)initWithService:(nonnull DNSServiceRef)service NS_DESIGNATED_INITIALIZER;

This causes another build warning in that same pesky -init method:

null passed to a callee that requires a non-null argument [-Wnonnull]
        return [self initWithService:NULL];

Ugh. This leaves us with a dilemma. On the one hand, the NS_DESIGNATED_INITIALIZER annotation forces -init to call -initWithService: , and on the other hand, the nonnull disallows calling -initWithService: with NULL.

There are ways to work around this problem, but I warn you now that they're going to make your eyes bleed, so please turn away if you're squeamish, and make your children leave the room. For those still with us, behold!

-(instancetype)init
{
    return [self initWithService:(DNSServiceRef _Nonnull)NULL];
}

Yup. We're casting NULL to non-NULL. That's the "best practice". Seriously. No more needs to be said. Whereof one cannot speak, thereof one must be silent.

Appendix

Ok, I lied. More needs to be said. After some … philosophical investigations … I found a better workaround:

-(instancetype)init UNAVAILABLE_ATTRIBUTE;

Still, it's worth noting a logical consequence of our ability to cast NULL to _Nonnull . The nullability annotations are compiler checks, not runtime checks. Their presence in an API does not prevent an unexpected NULL argument. You would still need to check at runtime to guarantee it's not happening.

The problems are solved, not by giving new information, but by arranging what we have known since long.








New