Nullability warnings with libextobjc’s @keypath macro

Updates:

  1. Jul 29, 2018
    Edited for clarity and readability. Fixed a few typos and added a few more links.
  2. Jul 31, 2018
    There is now a new release for libextobj that includes the fix discussed here, v0.6.

This article is for you if:

  • You work on an app that is still (partly or fully) written in Objective-C.

  • You use Justin Spahr-Summer’s libextobjc library, specifically the @keypath macro for compile-time checking of key paths.

  • When you build your app with Xcode 10 with the -Wnullable-to-nonnull-conversion (CLANG_WARN_NULLABLE_TO_NONNULL_CONVERSION) warning enabled, new warnings pop up in the context of @keypath:

    warning: Implicit conversion from nullable pointer ‘NSString * _Nullable’ to non-nullable pointer type ‘NSString * _Nonnull’

And now you want to know how to silence the warnings (and where they came from).

Xcode 10 warns on boxed C string literals

Before the change I discuss below, the keypath macro in libextobj would expand an expression like this:

@keypath(myArray, count)

into a boxed C string literal:

@("count")

In Xcode 9 and below, the type of this expression was NSString *. The reason for the new warnings is that in Xcode 10 the type of @("count") has changed to NSString * _Nullable.

Apparently, the boxing operator @ now always produces a nullable NSString from a C string, even when its argument is a literal or a compile-time constant. The warning will pop up anywhere you use a keypath expression in a place that expects a non-null NSString.

(It would be great if the compiler treated literals and compile-time constants differently. I filed an issue, Radar #42684601.)

The fix

Unfortunately, there was no simple fix for this: since the @ symbol is not part of the macro’s body, placing a cast to _Nonnull into the macro doesn’t work — the cast would have to be outside the macro, in client code. Similarly, silencing the warning with #pragma in the macro’s body is not possible.

After some trial and error, I found a workaround and submitted a patch to libextobjc (see the primary PR and a follow-up). The same @keypath(myArray, count) expression now expands to this:

@(NO).boolValue ? ((NSString * _Nonnull)nil) : ((NSString * _Nonnull)@("count"))

The @ sign in @keypath now only applies to a Boolean literal, (NO), which generates no warnings. We then use the ternary operator to convert the Boolean value into the desired NSString, and here we can apply the cast to _Nonnull inside the macro’s body.

Downsides

I believe this solution is better than the previous situation, but it has two downsides:

Performance impact

Firstly, it’s a (hopefully negligible) performance degradation: before, the macro would effectively be compiled into [NSString stringWithUTF8String:"count"]. The new implementation adds the boxing of a Boolean value (which should be cheap with tagged pointers) and a branch. The new compiled code is roughly equivalent to this:

NSString *kp;
if (@(NO).boolValue == false) {
    kp = [NSString stringWithUTF8String:"count"];
}

If you use @keypath in a tight loop, this might be an issue.

Scroll down for a more detailed description of my testing procedure.

Macros and parentheses

The second potential issue is a common problem with C macros that aren’t properly parenthesized. Because the C preprocessor performs simple text replacements, each macro parameter and the macro body as a whole should be wrapped in parentheses to avoid surprising results caused by the macro contents interacting with surrounding code.

The problem with keypath is that we can’t enclose the entire macro body in parentheses because the @ sign (which isn’t part of the macro) must only apply to part of the macro body. I don’t think this will be a problem in practice, but the possibility exists. If you see a weird compiler warning or error around a @keypath expression that seemingly makes no sense, try if wrapping it in parentheses helps.

Use libextobjc 0.6

Many thanks to my colleague Stephan Diederich and the folks at Lightricks who helped me figure this out, especially Barak Weiss.

I don’t believe libextobjc is actively being maintained anymore, but we managed to get this patch committed because Stephan is a collaborator on the repo. Stephan released a new version (v0.6), which includes the fix discussed in this post.


Appendix: generated code for the new vs. the old implementation

I’m not very good at reading assembly, so to test the performance impact I wrote a minimal test program that looks like this:

// keypath-test.m
@import Foundation;

int main() {
    NSString *old_macro = @("count");
    NSLog(@"old: %@", old_macro);
    NSString *new_macro = @(NO).boolValue ? ((NSString * _Nonnull)nil) : ((NSString * _Nonnull)@("count"));
    NSLog(@"new: %@", new_macro);
    return 0;
}

I compiled this with the Clang that comes with Xcode 10 (beta 4) with optimizations enabled:

> clang -O -fmodules -fobjc-arc -o keypath-test keypath-test.m

I then loaded the binary into Hopper and compared the disassembled pseudo Objective-C code Hopper generates. This is the disassembly for the old macro implementation:

r15 = [[NSString stringWithUTF8String:"count"] retain];
NSLog(@"old: %@", r15);
[r15 release];

And the new implementation:

r12 = 0x0;
rbx = [@(NO) retain];
if ([rbx boolValue] == 0x0) {
    r12 = [[NSString stringWithUTF8String:"count"] retain];
}
[rbx release];
NSLog(@"new: %@", r12);
[r12 release];

As you can see, there’s also one more retain/release pair in the new implementation.