Skip to content

(take 2) SR-0140: Bridge Optionals to nonnull ObjC objects by bridging their payload, or using a sentinel. #4869

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Sep 20, 2016

Conversation

jckarter
Copy link
Contributor

id-as-Any lets you pass Optional to an ObjC API that takes nonnull id, and also lets you bridge containers of Optional to NSArray etc. When this occurs, we can unwrap the value and bridge it so that inhabited optionals still pass into ObjC in the expected way, but we need something to represent none other than the nil pointer. Cocoa provides NSNull as the canonical "null for containers" object, which is the least bad of many possible answers. If we happen to have the rare nested optional T??, there is no precedented analog for these in Cocoa, so just generate a unique sentinel object to preserve the nil-ness depth so we at least don't lose information round-tripping across the ObjC-Swift bridge.

Making Optional conform to _ObjectiveCBridgeable is more or less enough to make this all work, though there are a few additional edge case things that need to be fixed up. We don't want to accept AnyObject?? as an @objc-compatible type, so special-case Optional in getForeignRepresentable.

Implements SR-0140 (rdar://problem/27905315).

@jckarter
Copy link
Contributor Author

@swift-ci Please test

@jckarter
Copy link
Contributor Author

@jrose-apple Do you mind reviewing this for inclusion in 3.0.1?

#if _runtime(_ObjC)
extension Optional : _ObjectiveCBridgeable {
// The object that represents `none` for an Optional of this type.
internal static var _nilSentinel : AnyObject {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this worth caching, for SIL optimization purposes?

internal static let _nilSentinel = _swift_Foundation_getOptionalNilSentinelObject()

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately, we don't support stored static properties on generic types. We have to rely on the runtime's caching.

}

public static func _forceBridgeFromObjectiveC(_ source: AnyObject,
result: inout Optional<Wrapped>?) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick: unconventional alignment here. Standard library style would be to treat the parameters as a block bound with parens.

// NB that the signature of _forceBridgeFromObjectiveC adds another level
// of optionality, so we need to wrap the immediate result of the conversion
// in `.some`.
if source === _nilSentinel {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are all NSNulls guaranteed to be identical?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is documented as a singleton, but I would double-check with someone from Foundation. @phausler?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's example ObjC code that uses x == [NSNull null] too.

Copy link
Contributor

@phausler phausler Sep 20, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NSNull in Darwin is a singleton (swift-corelibs-foundation it cannot be since we don't have factory patterns)

@@ -2513,6 +2513,18 @@ bool swift::swift_dynamicCast(OpaqueValue *dest,
// unwrapping the target. This handles an optional source wrapped within an
// existential that Optional conforms to (Any).
if (auto srcExistentialType = dyn_cast<ExistentialTypeMetadata>(srcType)) {
#if SWIFT_OBJC_INTEROP
// If coming from AnyObject, we may want to bridge.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this specific to AnyObject?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suppose we could also support it from NSNull. AnyObject is the only Swift-level formal type that'd make sense to cast back to Optional otherwise.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, I somehow missed that this was only when casting to Optional. Thanks for clarifying.

return [NSString stringWithFormat:@"<_SwiftNull %u>", self->depth];
}

- (void)dealloc {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick: This is conventionally omitted when empty (and may allow run-time optimizations).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops, I had put this in so I could put breakpoints there, but forgot to remove it.

}
}

tests.test("wrapped value") {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any tests for optionals of non-bridged types? For optionals of reference types?

//
//===----------------------------------------------------------------------===//

#include <Foundation/Foundation.h>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick: I think this counts as a system include, and so it should go at the bottom. Also, #import would be more appropriate. (ObjC headers typically don't have include guards.)

@jrose-apple
Copy link
Contributor

Some comments made, but mostly seems fine to me…except that someone else should take a look at the lib/SIL/ changes.

@atrick
Copy link
Contributor

atrick commented Sep 20, 2016

lib/SIL looks superficially ok to me. Question: Does SILGenBridging implicitly assume that the return value can't be reabstracted? If so, a comment would be helpful.

@jckarter
Copy link
Contributor Author

jckarter commented Sep 20, 2016

@atrick Yeah, it looks like SILGenBridging doesn't handle reabstraction (or address-only types, or indirect arguments, which @shajrawi is fixing). I can add that as a comment. Sorry, confused the optimizer and codegen layers. Yeah, we don't currently bridge anything via protocols that requires reabstraction.


/// Class of sentinel objects used to represent the `nil` value of nested
/// optionals.
@interface _SwiftNull : NSObject {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should'd probably be __attribute__((visibility("hidden")))

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea. We aren't making any of the runtime-private classes hidden yet. I think we should do that as a separate patch, though, just to keep the risk of unintentional breakage minimal for this as a 3.0.1 patch.

@implementation _SwiftNull : NSObject

- (NSString*)description {
return [NSString stringWithFormat:@"<_SwiftNull %u>", self->depth];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Usually NSObjects have the format more like:

[NSString stringWithFormat:@"<%@ %p depth = %u>", [self class], self, self->depth]

That way if there is ever a problem you have the pointer which can be used in a debugger if need be.

}

tests.test("collection of Optional") {
let holeyArray: [LifetimeTracked?] = [LifetimeTracked(0), nil, LifetimeTracked(1)]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does the reverse work? someNSArray as [LifetimeTracked?]

This pattern if ever used will never really be portable to swift-corelibs-foundation which is a bit unfortunate.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's true of all bridging conversions, but ideally the bridging is never necessary to begin with in corelibs.

static id getSentinelForDepth(unsigned depth) {
// For unnested optionals, use NSNull.
if (depth == 1)
return SWIFT_LAZY_CONSTANT([NSNull null]);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does this macro do?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It inlines a lambda that dispatch_onces the expression in the macro argument.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am fairly certain we have a full obligation from Foundation to maintain the return value as implemented:

+ (NSNull *)null {
    return (id)kCFNull;
}

So the dispatch_once here is not really needed at all.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea, I'll just use kCFNull directly.

if (!cached) {
auto sentinel = [[_SwiftNull alloc] init];
sentinel->depth = depth;
cached = sentinel;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Am I not reading this correctly? On cache miss you are not storing a value into the cache?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cached is a reference, so this will store the value into the cache.

@jckarter
Copy link
Contributor Author

@jrose-apple @phausler @atrick Thanks for the review comments! I've revised the patch in response, if you all don't mind taking another look.

@jckarter
Copy link
Contributor Author

@swift-ci Please test

@@ -754,6 +754,7 @@ static ValueDecl *getIsOptionalOperation(ASTContext &Context, Identifier Id) {
return builder.build(Id);
}


Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick: this is the only change in this file now.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, fixed.

@jrose-apple
Copy link
Contributor

LGTM

…ayload, or using a sentinel.

id-as-Any lets you pass Optional to an ObjC API that takes `nonnull id`, and also lets you bridge containers of `Optional` to `NSArray` etc. When this occurs, we can unwrap the value and bridge it so that inhabited optionals still pass into ObjC in the expected way, but we need something to represent `none` other than the `nil` pointer. Cocoa provides `NSNull` as the canonical "null for containers" object, which is the least bad of many possible answers. If we happen to have the rare nested optional `T??`, there is no precedented analog for these in Cocoa, so just generate a unique sentinel object to preserve the `nil`-ness depth so we at least don't lose information round-tripping across the ObjC-Swift bridge.

Making Optional conform to _ObjectiveCBridgeable is more or less enough to make this all work, though there are a few additional edge case things that need to be fixed up. We don't want to accept `AnyObject??` as an @objc-compatible type, so special-case Optional in `getForeignRepresentable`.

Implements SR-0140 (rdar://problem/27905315).
@jckarter
Copy link
Contributor Author

@swift-ci Please test

@jckarter jckarter merged commit 2bfe921 into swiftlang:master Sep 20, 2016
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants