[swift-evolution] [Review] SE-0058: Allow Swift types to provide custom Objective-C representations

Russ Bishop xenadu at gmail.com
Mon Apr 4 23:54:34 CDT 2016


> On Apr 4, 2016, at 9:22 PM, Brent Royal-Gordon via swift-evolution <swift-evolution at swift.org> wrote:
> 
>> 	https://github.com/apple/swift-evolution/blob/master/proposals/0058-objectivecbridgeable.md
> 
> There are a number of things I'm not really clear on.
> 
> * * *
> 
> Consider this Objective-C API:
> 
> 	ObjCFizzer* myFizzer;
> 
> Which of these represents how it is imported?
> 
> 	var myFizzer: ObjCFizzer
> 	var myFizzer: Fizzer

The latter. The idea is that the importer sees the bridged type is available and substitutes it on all imported signatures. The actual mechanics of that will involve some generated code (thunk) to call the protocol. I could update the proposal to include what the body of that thunk might look like but it didn’t seem terribly interesting.

> 
> Suppose there is also a subclass (say, ObjCMutableFizzer), and we have this Objective-C API:
> 
> 	ObjCMutableFizzer* mutableFizzer;
> 
> Which of these represents how it is imported?
> 
> 	var myMutableFizzer: ObjCMutableFizzer
> 	var myMutableFizzer: Fizzer

The intention there is that it imports as the bridged type so the latter.


> 
> On the basis of NSArray and friends, I assume they come across like this:
> 
> 	var myFizzer: Fizzer
> 	var myMutableFizzer: ObjCMutableFizzer
> 
> Is that correct?

No


> 
> * * *
> 
> I assume that you can use casts to implicitly cross the bridge in both directions, even when Objective-C is not involved. That is, you could write something like this:
> 
> 	ObjCFizzer() as Fizzer
> 
> Is that correct?
> 
> If you can cross the bridge purely in Swift, does the object type actually have to be @objc? Why?
> 
> If it does not have to be @objc, is this perhaps better thought of as an `ObjectBridgeable` protocol which allows cast-based conversion of any type to an equivalent class, and which also does Objective-C bridging if the class happens to be @objc? (This looser definition might help us if we ever interoperate with other object systems on different platforms.)

In theory you could do this, though it would be a useless waste of CPU cycles :)

My thinking was that we may need similar bridging protocols in some hypothetical world where we can import C++ objects but the shape of the protocol (and certainly the constraints on the associated type) would be quite different.


> 
> * * *
> 
> Suppose you have a value of type `ObjCMutableFizzer`:
> 
> 	let mutableFizzer: ObjCMutableFizzer = ...
> 
> Can you write `mutableFizzer as! Fizzer`? In other words, if a type is bridged to a particular class, is it also bridged to its subclasses?
> 
> Based on the examples from Foundation, I suspect the answer is "yes”.

Yes, you can invoke as? or as! casts which will call the respective initializers, though the default implementation of unconditional bridging (as!) will call the conditional initializer.


> 
> * * *
> 
> Foundation classes can sometimes be bridged using an upcast (a plain `as`), which cannot crash. Is this possible with ObjectiveCBridgeable? If so, how? If not, will Foundation classes lose that ability?
> 
> If this feature can't be expressed with ObjectiveCBridgeable, is this seen as a shortcoming we should try to overcome, or the proper design? I worry about the unnecessary proliferation of exclamation points, especially since many style guides strongly discourage them, which will turn this into an unnecessary proliferation of unnecessary `if let`s.

This would not be possible. This sort of bridging only works with special magic types because they are known to always succeed. There is no condition under which Swift will fail to convert String to NSString. The compiler/runtime can’t prove that about any arbitrary type.

For bridging an Objective-C library into Swift, ideally all the APIs will be annotated with SWIFT_BRIDGED so on import the Swift code won’t even be aware the Objective-C type exists. All you’ll see in Swift is the appropriate Swift types. This gives a library (say Photos.framework or UIKit) the chance to provide truly native Swift types by shipping a module with combined Swift and Objective-C code.

Similarly, going the other direction (an app with Objective-C and Swift code) this proposal eliminates the need to deal with the Objective-C types in Swift.

The only situation where casting might be required is interop with things like performSelector, context objects, or when SWIFT_BRIDGED annotations are missing.


> 
> * * *
> 
> I'm confused by a statement in the "Ambiguity and Casting" section:
> 
> 	2. A Swift type may bridge to an Objective-C base class then provide different subclass instances at runtime, but no other Swift type may bridge to that base class or any of its subclasses.
> 		i. The compiler should emit a diagnostic when it detects two Swift types attempting to bridge to the same ObjectiveCType.
> 
> Does this mean that each bridged class must have exactly one corresponding Swift type? Or does it mean that if a bridged type has more than one corresponding Swift class, an explicit cast is always needed? Or merely that it may sometimes be needed?

As of right now, the proposal says that is an error to define in Swift code and the compiler will generate an error.



> 
> There are examples in the frameworks of many types bridging to a single class. For instance, a whole hoard of numeric types bridge to NSNumber; Swift handles this by exposing Swift types like `[Int]` as `NSArray<NSNumber*>*`, but doesn't do any converting when going in the opposite direction. Is that how this new protocol works, or does it do something else? 

I’ve gone back and forth on the ambiguity section (In one version of the proposal, ambiguity just disabled automatic thunk generation and required you to perform manual casting). NSNumber brings up an interesting case. Several Swift types should adopt NSNumber as their bridging target, but wouldn’t be able to under the proposed rule. However allowing this for the general case may be problematic. There is already special compiler magic for importing things like NSInteger and Int bridges to NSInteger or NSNumber depending on whether it is inside a collection. I’m curious what other people’s thoughts are on this.



> 
> * * *
> 
> I'm confused by the SWIFT_BRIDGED() macro. Why does it exist? Doesn't the ObjectiveCBridgeable conformance provide all the information needed? What happens if you don't include it? (Perhaps you get the one-way bridging behavior seen with `NSNumber`?)

For an Objective-C framework, the definitions of the @objc types will be in Objective-C headers, not an auto-generated Swift bridging header. The macro is both for the auto-generated header but also for library writers to use in their Objective-C headers.


> 
> * * *
> 
> The "Resilience" section says:
> 
> 	Adding or removing conformance to ObjectiveCBridgeable, or changing the ObjectiveCType is a fragile (breaking) change.
> 
> Why is this? In particular, why is adding a conformance a breaking change? That isn't the normal rule for protocols.

Because it changes the shape of the API of a client importing it. 

For similar reasons requiring the bridging to be in same-named modules is to prevent the shape of an imported API changing because you imported a separate module that added some bridging conformance.


> 
> * * *
> 
> Probably a stupid question, but I want to be certain since the example does something else: There would not be some kind of circularity problem with defining `ObjCFizzer` in terms of `Fizzer`, would there? For instance:
> 
> 	class ObjCFizzer: NSObject {
> 		fileprivate var fizzer: Fizzer
> 
> 		fileprivate init(_ fizzer: Fizzer) {
> 			self.fizzer = fizzer
> 		}
> 				
> 		var fizzyString: String? {
> 			guard case .case1(let string) = fizzer else { return nil }
> 
> 			return string
> 		}
> 		
> 		var fizzyX: Int? {
> 			guard case .case2(let x, _) = fizzer else { return nil }
> 			return x
> 		}
> 		
> 		var fizzyY: Int? {
> 			guard case .case2(_, let y) = fizzer else { return nil }
> 			return y
> 		}
> 	}
> 	extension ObjCFizzer {
> 		convenience init(string: String) {
> 			self.init(.case1(string))
> 		}
> 		
> 		convenience init(x: Int, y: Int) {
> 			fizzer = .case2(x, y)
> 		}
> 	}
> 
> I have a place in my code where I would like to use ObjectiveCBridgeable, but it uses a pattern like this, so I'd like to make sure I'll be able to adopt the feature.
> 

You can certainly “wrap” the underlying Swift type and expose accessors for it as you’ve done here. There are no issues with circularity at all. 

I realize that the proposal doesn’t explicitly state it but generally a type that is @objc can’t conform to ObjectiveCBridgeable. (Even if it did, the conformance would be useless.)



> * * *
> 
> In general, I feel like this proposal is written in a way which is accessible to the people who participated in the original discussion, but not to others.
> 
> This is a proposal which contains seven uses of the word "thunk" and zero uses (as opposed to conformances) of the proposed protocol in examples. It seems to imply that existing bridging of Foundation types might change, but does not explain what those changes will mean for user code. It is fundamentally an Objective-C bridging feature, but it contains no examples of Objective-C.

The intent is for existing foundation types to continue bridging just as they do today with no changes. 



Russ



More information about the swift-evolution mailing list