Thursday, January 16, 2020

Optionals in Swift Objective-C Interoperability

Fabián Cañas (tweet):

The problem is that since Swift doesn’t think this value can be nil, it’s not trivial to check.

[…]

It says the non-optional value shouldn’t be compared to nil, and that it’s always false. But at run time, the nil is detected, and we print the statement.

[…]

What’s interesting here is that the argument to the bridge function is an Optional<NSCalendar>. The static method, by its signature, accepts nil. What’s happening then? In this case, The culprit for the crash and what saves us from unexpected behavior later on is a force unwrap. Though the value that’s actually passed in to the function is Optional<NSCalendar>.some(nil), which is still not a valid value and we’re still in undefined behavior territory, so it’s pleasantly surprising that a force unwrap catches this case.

[…]

Having the the compiler automatically check and assert that nonnull Objective-C types returned by Objective-C methods are indeed present would be fantastic, whether for debug builds or as an independent flag.

Brent Royal-Gordon:

To make sure we’re all on the same page: returning null from a nonnull imported API is full-on, demons-flying-out-of-your-nose undefined behavior. There’s no guarantee that it will do what you saw.

Unfortunately, it’s rather easy to get the annotations wrong, and even Apple does this. For example, the SecDigestTransformCreate() and SecTransformExecute() calls can return NULL in Objective-C, but Swift acts as if they can’t fail. I filed a bug about about this, which Apple recently said was so old that they wanted to close it and have me open a new one. Meanwhile, I’m able the work around the issue because these two APIs have a separate error pointer that can be examined. Without that, I think you would need an Objective-C wrapper to safely detect whether an error has occurred.

Update (2020-01-24): Quinn the Eskimo (via Thomas Clement):

The SecTransform API is effectively deprecated, and has been so since 10.12. Unfortunately it’s taken a while for us to formally deprecate it (r. 25183002).

5 Comments RSS · Twitter

If an API is incorrectly annotated as returning nonnull, you can tell the Swift compiler it actually can return nil anyway by assigning the results of the function to a variable with an explicit type. This might look like

let foo: SecTransformRef? = SecDigestTransformCreate(digestType, digestLength, &error)

This is explicitly supported by the compiler.

I got bitten by a more subtle version of nullability annotation the other day:

@interface Foo: NSObject

- (BOOL)isEqualToFoo:(Foo*);

@end

If you forget to add "nullable" in the isEqualToFoo: declaration then calling "foo.isEqual(to: otherFoo)" from Swift will only work as expected if "otherFoo" is not an Optional. Otherwise you get the NSObject version if isEqual(to:), which won't be what you expect. Took a bit of head scratching to work out that it was the nullable annotation which I was missing in the ObjC API.

I prefer avoiding Swift / Objective-C interop in my own code because of this kind of issue. Of course we can't avoid this when interacting with Apple frameworks.

@Lily Thanks. My recollection is that the compiler complained about that back when I first ran into this issue. It seems to work with Xcode 11, though.

Wes Campaigne

@John B:

Interesting. There’s a long-standing convention in Foundation (that was subtle in the pre-nullability annotation days) that isEqualToFoo: methods do not accept nil as a parameter (and crash if you pass it), while isEqual: implementations can be passed nil (or, well, anything) because they do extra input validation first before internally deferring to isEqualToFoo:. Consequently, I almost never call isEqualToFoo: methods directly ... basically only tolerate it when there is a strong local guarantee that the parameter is non-nil and of the expected type, but even then, I think I’ve never encountered this in code that was performance critical enough that the extra time to perform the safety checks provided by isEqual: would matter.

It’s really more nuanced than returning `nil` when you shouldn’t. That can be caught by the compiler or analyzer. The issue that concerns me most is if you have a `nonnull` property but one of the initializer paths doesn’t initialize the instance variable backing that property. It’s a very easy mistake to make and there is no warning (compile time, analyzer time, nor run time) that will catch it.

Leave a Comment