Norway


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 for compile-time checking of key paths.
  • When you build your app with Xcode with the -Wnullable-to-nonnull-conversion (CLANG_WARN_NULLABLE_TO_NONNULL_CONVERSION) warning enabled, new pop up in the context of @:

    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).

With libextobjc, an expression like:

@keypath(Array.alloc, count)

would resolve to:

The reason for the new warnings is that in Xcode 10, the boxing operator @ now apparently has a return type of NSString * _Nullable instead of the previous NSString * when the argument is a C string, even if it’s a literal. The warning would pop up anywhere you used a keypath expression in a place that expects a non-null NSString.

Unfortunately, there was no super-simple fix for this: since the @ symbol is not part of the macro, placing a simple cast to _Nonnull into the macro doesn’t work. Similarly, silencing the warning with #pragma in the macro body is not possible because the warning originates at the @ sign, which isn’t part of the macro.

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 expression from above now expands to this:

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

The @ 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.

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

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 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 procedure.

Macros and parentheses

The second downside 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 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 @ is not part of the macro bopdy and it must only apply to part of the body. I don’t think this will be an issue in practice, but the possibility exists. If you see a weird compiler warning or error that seemingly makes no sense around a @keypath expression, try if wrapping it in parentheses helps.

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

I believe libextobjc isn’t actively being maintained anymore, but we managed to get this patch committed because my Stephan is a collaborator on the repo. There isn’t a new release at this point (and I’m not sure if there will be), but if you pull the latest changes from libextobjc, you’ll get the new behavior.


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

I’m not great 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):

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

I then loaded the resulting binary into Hopper and compared the disassembled pseudo Objective-C code Hopper generates. This is the disassembly for the old 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.



Source link
Based Blockchain Network

LEAVE A REPLY

Please enter your comment!
Please enter your name here