Hi all,
In a follow up to SE-0384, we would like to propose another improvement to Swift - Objective-C interop. Specifically, we would like to improve upon the ClangImporter
's ability to tie an @class Foo
or @protocol Bar
declaration in imported Objective-C, to a Swift definition @objc class Foo {}
or @objc protocol Bar {}
defined in Swift.
Problem:
First a brief illustration of the problem we wish to solve:
Consider Swift module Foo
with following source:
@objc public class Foo {}
An Objective-C Clang module FooObjCWrapper
with source:
@class Foo;
void takeAFoo(Foo* foo);
Foo* giveAFoo();
And a Swift client using both these modules like so:
import Foo
import FooObjCWrapper
let myStraightFromSwiftFoo = Foo() // This works!
_ = giveAFoo() // This doesn't!
test.swift:5:5: error: cannot find 'giveAFoo' in scope
_ = giveAFoo()
^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
FooObjCWrapper.h:3:1: note: function 'giveAFoo' not imported
Foo* giveAFoo();
^
FooObjCWrapper.h:3:1: note: return type not imported
Foo* giveAFoo();
^
FooObjCWrapper.h:3:1: note: interface 'Foo' is incomplete
Foo* giveAFoo();
^
FooObjCWrapper.h:3:1: note: interface 'Foo' forward declared here
@class Foo;
^
Even though we have imported the definition of Foo, it is not visible from in the ClangAST, so giveAFoo
and takeAFoo
are dropped by the ClangImporter
. If Foo
was defined in an imported Clang module instead of a Swift module, this would work.
Motivation:
We briefly enumerate the motivating factors below:
- Make Objective-C APIs wrapping Swift usable from Swift without unnecessarily bloating headers
The only solution to use this API from Swift is to #import
the generated -Swift.h
header for the Swift module Foo in the FooObjCWrapper
's headers. This unnecessarily bloats headers, making both Swift and Objective-C clients parse more content, to only the benefit of Swift clients. As with all header includes, it makes the build graph more connected and harder to parallelize. As Swift adoption grows, while attempting to reuse existing Objective-C, these kinds of Swift -> Objective-C -> Swift cases become more and more common.
- Consistency with Clang modules
As mentioned above, if the definition were in an imported Clang module, not Swift module, this already works. This can be quite confusing. Users are told that if forward declared types are causing them problems to import the defining module. When this only works for Clang modules, it complicates things and increases the minimum understanding of interop implementation users need to operate effectively.
- Required for SE-0384 implementation
Unfortunately, I did not know it at the time, but this is required for SE-0384 to land in a reasonable fashion. If SE-0384 were implemented without this, Swift would see two conflicting interpretations of the type Foo
. One the complete definition directly from the defining Swift module, the other an opaque placeholder synthesized to represent the @class Foo
whose definition "we can't see".
Precedent:
We already do this kind of @class Foo
-> @objc class Foo {}
resolution to an extent to support mixed modules: swift/ImportDecl.cpp at main · apple/swift · GitHub . In implementing this change, we would be expanding the search from an overlay module, or owners of the bridging header to all imported Swift modules. A draft implementation can be seen here: Link imported ObjC forward declarations to their Swift definition by NuriAmari · Pull Request #63398 · apple/swift · GitHub .
Source Compatibility:
The usual, we are importing new declarations, possibly introducing name conflict based source compatibility breakage with ClangImporter changes applies.
Risks / Factors to Consider:
We would like to know of any edge cases or risks we are not thinking of. One hurdle to be called out is the potential for multiple Swift declarations visible from the main module with the same @objc
provided name. It is my impression that writing this kind of code already introduced "undefined behavior" but could use confirmation.
We appreciate any input and feedback. Thanks!