[swift-evolution] [Idea] ObjectiveCBridgeable

Douglas Gregor dgregor at apple.com
Mon Mar 7 12:36:36 CST 2016


Hi Russ,

Sorry for the long delay in replying. This is a topic of interest to me.

> On Feb 25, 2016, at 11:02 AM, Russ Bishop via swift-evolution <swift-evolution at swift.org> wrote:
> 
> I wanted to float a proposal and see what the community thinks. If there is interest I’m happy to submit an official proposal. This comes from real problems we’re encountering with a large mixed codebase. Every time our nice clean Swift design hits an Objective-C boundary we end up having to go back and dumb-down the Swift API significantly because manually converting at every touch point is just too painful.
> 
> 
> # Introduction
> 
> Provide an ObjectiveCBridgeable protocol that allows a type to control how it is represented in Objective-C by converting into and back from an entirely separate type. This frees an API designer to create truly native Swift types like structs, generics, enums with associated values, protocols with associated types, etc without having to compromise to allow interop with Objective-C.

There may be some limitations that are inherent to the bridging process, so it’s not clear that we can achieve everything you mention above. As an example, check out my proposal to bridge Objective-C’s lightweight generics into Swift:

	http://thread.gmane.org/gmane.comp.lang.swift.evolution/2886

The content of that proposal isn’t as important as the scope of implementation effort to bridge between two similar-looking but very-differently-implemented features across the languages. It’s likely that other features you’ve mentioned above have similar complexities. That said, I do think it’s possible to extend the bridging mechanism to help alleviate some of the issues you’re describing.

> # Motivation
> 
> When working in a mixed codebase, we find ourselves unable to use generics, enums with associated values, structs, and various other Swift features because there are chunks of Objective-C code that we can’t stop and rewrite. This leads to a catch-22 situation where your new APIs (or refactored APIs) have a compromised design,  which has a cascading effect on new Swift code that uses the compromised API. When the last vestiges of Objective-C have been swept away you’re left with a mountain of Swift code that essentially looks like a direct port of Objective-C code and doesn’t take advantage of any Swift-ish features. 
> 
> 
> 
> # Proposed Solution
> 
> Today, you can adopt the private protocol _ObjectiveCBridgeable and if your parameters or return types are inside an Array, Swift will automatically call the appropriate functions to let you control the way the type bridges. This allows you to define a completely different Objective-C compatible API for the type and round-trip it appropriately.

The other bridged collection types (Set/Dictionary) also work this way as well.

> My proposal is simple: expose the existing protocol as a public protocol ObjectiveCBridgeable: _ObjectiveCBridgeable, and have the compiler generate the appropriate Objective-C bridging outside of the existing Array support.
> 
> 
> 
> # Detailed Design
> 
> 1. Expose the existing protocol as a public protocol 
> 2. When generating an Objective-C interface for a type that is itself bridgeable to Objective-C:
>  a. When a function contains parameters or return types that are @nonobjc but those types adopt ObjectiveCBridgeable:
>    i) Create @objc overloads that call the Swift functions but substitute the corresponding ObjectiveCType.
>    ii) The overloads will automatically call the appropriate protocol functions to perform the conversion.

This is *roughly* what the compiler does today for the various bridged types (Array/Dictionary/Set/String): the function with the Swift calling convention uses the Swift-native types, and the compiler emits an @objc thunk that translates the Objective-C-visible types into the Swift-native types and back.


>  b. If any @nonobjc types do not adopt ObjectiveCBridgeable, the function itself is not exposed to Objective-C (current behavior).

Right.

> 3. It is up to the API designer to decide if they want to allow construction of wholly new values from Objective-C and how to handle convertibility back to the Swift type in that case

I’m not sure what you mean by this?

> An example enum that adopts the protocol and bridges by converting itself into an object representation.
> Note: The ways you can represent the type in Objective-C are endless; I’d prefer not to bikeshed that particular bit :)
> 
> enum Fizzer: ObjectiveCBridgeable {
>    case Case1(String)
>    case Case2(Int, Int)
> 
>    static func getObjectiveCType() -> Any.Type {
>        return ObjCFizzer.self
>    }

FWIW, this method is an anachronism. You can leave it out, because the runtime can now recover this information directly. However, I do suggest that you show

	typealias ObjectiveCType = ObjCFizzer

in your example, so its clear what the mapping is.

>    static func isBridgedToObjectiveC() -> Bool {
>        return true
>    }

>    func bridgeToObjectiveC() -> ObjCFizzer {
>        let bridge = ObjCFizzer()
>        switch self {
>        case let .Case1(x):
>            bridge._case1 = x
>        case let .Case2(x, y):
>            bridge._case2 = (x, y)
>        }
>        return bridge
>    }

Okay.

>    static func conditionallyBridgeFromObjectiveC(source: ObjCFizzer, inout result: Fizzer?) -> Bool {
>        _forceBridgeFromObjectiveC(source, result: &result)
>        return true
>    }

The above isn’t a correct implementation, because it will fatalError return than returning “nil” if the source cannot be bridged.

>    static func forceBridgeFromObjectiveC(source: ObjCFizzer, inout result: Fizzer?) {
>        if let stringValue = source._case1 {
>            result = Fizzer.Case1(stringValue)
>        } else if let tupleValue = source._case2 {
>            result = Fizzer.Case2(tupleValue)
>        } else {
>            fatalError("Unable to bridge")
>        }
>    }
> }
> 
> class ObjCFizzer: NSObject {
>    private var _case1: String?
>    private var _case2: (Int, Int)?
> }

I had expected ObjCFizzer to be defined in Objective-C. If it can also be defined in Swift, that’s a nontrivial expansion of what bridging currently does.

Along these lines, I think we’ll need a Clang-side attribute on the Objective-C type to specify what Swift type it bridges to. That’s important for Swift’s Clang importer, because when it sees the Clang type it needs to know whether it’s importing that type directly or bridging to a different type.


> # TBD / Questions:
> 
> Should the shape of the protocol change?

I think the protocol is still basically the right interface, except that getObjectiveCType is no longer necessary. How Objective-C-centric should this protocol be? Should it be possible for (e.g.) the NSInteger-to-Int mapping to be expressible this way? Can a simple C struct be bridged?

> It may not make sense for the public protocol to support both conditional and force bridging; maybe we only provide a throwing equivalent of force bridge and let the implementer throw if the conversion is invalid? 

The distinction between forced and conditional bridging is fairly important for performance. Forced bridging is used for “as!” while conditional bridging is used for “as?”; when we know we’re in the former case, we can sometimes delay validation work.

From a proposal perspective, I think it’s important to show the ObjectiveCBridgeable protocol you’re proposing and say what each requirement does. You can grab them directly from the comments, of course, but I suspect that most people haven’t dug into bridging at all.

If it’s not obvious already, I think this is definitely worthy of a proposal.

	- Doug



More information about the swift-evolution mailing list