Hi everyone, we would like to propose some improvements to Swift - Objective-C interop, specifically with respect to Objective-C forward declarations. A draft PR of the proposed change can be found here: Import objc forward declarations by NuriAmari · Pull Request #61606 · apple/swift · GitHub
Introduction:
This proposal seeks to improve the usability of existing Objective-C libraries from Swift by reducing the negative impact forward declarations have on API visibility from Swift. We wish to start synthesizing placeholder types to represent forward declared Objective-C interfaces and protocols in Swift.
Motivation:
Forward declarations are very common in many existing Objective-C code bases, used often to break cyclic dependencies or to improve build performance. Unfortunately, when it comes to "importability" into Swift they are quite detrimental.
As it stands, the ClangImporter will fail to import any declaration that references a forward declared type in many common cases. This means a single forward declared type can render larger portions of an Objective-C API unusable from Swift. For example, the following Objective-C API from the proposal PR is empty from the Swift perspective (Swift textual interface generated via swift-ide-test):
Objective-C
#import <Foundation/Foundation.h>
@class ForwardDeclaredInterface;
@protocol ForwardDeclaredProtocol;
@interface IncompleteTypeConsumer1 : NSObject
@property id<ForwardDeclaredProtocol> propertyUsingAForwardDeclaredProtocol1;
@property ForwardDeclaredInterface *propertyUsingAForwardDeclaredInterface1;
- (id)init;
- (NSObject<ForwardDeclaredProtocol> *)methodReturningForwardDeclaredProtocol1;
- (ForwardDeclaredInterface *)methodReturningForwardDeclaredInterface1;
- (void)methodTakingAForwardDeclaredProtocol1:
(id<ForwardDeclaredProtocol>)param;
- (void)methodTakingAForwardDeclaredInterface1:
(ForwardDeclaredInterface *)param;
@end
ForwardDeclaredInterface *CFunctionReturningAForwardDeclaredInterface1();
void CFunctionTakingAForwardDeclaredInterface1(
ForwardDeclaredInterface *param);
NSObject<ForwardDeclaredProtocol> *CFunctionReturningAForwardDeclaredProtocol1();
void CFunctionTakingAForwardDeclaredProtocol1(
id<ForwardDeclaredProtocol> param);
Swift
class IncompleteTypeConsumer1 : NSObject {
init!()
}
We wish to make the experience of consuming an Objective-C API with forward declarations from Swift more consistent with the experience of consuming the API from Objective-C.
Proposed Solution
As a first iteration, we propose the following representation for forward declared Objective-C interfaces and protocols in Swift:
// @class Foo turns into
@available(*, unavailable, message: “This Objective-C class has only been forward declared; import its owning module to use it”)
class Foo : NSObject {}
// @protocol Bar turns into
@available(*, unavailable, message: “This Objective-C protocol has only been forward declared; import its owning module to use it”)
protocol Bar : NSObjectProtocol {}
The idea is to introduce the minimal change that will make Objective-C APIs usable in a predictable safe manner.
You will be able to use Objective-C and C declarations that refer to these types without issue. You will be able to pass around instances of these incomplete types from Swift to Objective-C and vice versa. You’ll also be able to call the set of methods provided by NSObject or NSObjectProtocol on instances. The aforementioned Objective-C API with this change looks like this from Swift:
@available(*, unavailable, message: "This Objective-C class has only been forward-declared; import its owning module to use it")
class ForwardDeclaredInterface {
}
@available(*, unavailable, message: "This Objective-C protocol has only been forward-declared; import its owning module to use it")
protocol ForwardDeclaredProtocol : NSObjectProtocol {
}
class IncompleteTypeConsumer1 : NSObject {
var propertyUsingAForwardDeclaredProtocol1: ForwardDeclaredProtocol!
var propertyUsingAForwardDeclaredInterface1: ForwardDeclaredInterface!
init!()
func methodReturningForwardDeclaredProtocol1() -> ForwardDeclaredProtocol!
func methodReturningForwardDeclaredInterface1() -> ForwardDeclaredInterface!
func methodTakingAForwardDeclaredProtocol1(_ param: ForwardDeclaredProtocol!)
func methodTakingAForwardDeclaredInterface1(_ param: ForwardDeclaredInterface!)
}
func CFunctionReturningAForwardDeclaredInterface1() -> ForwardDeclaredInterface!
func CFunctionTakingAForwardDeclaredInterface1(_ param: ForwardDeclaredInterface!)
func CFunctionReturningAForwardDeclaredProtocol1() -> ForwardDeclaredProtocol!
func CFunctionTakingAForwardDeclaredProtocol1(_ param: ForwardDeclaredProtocol!)
More usage examples can be found in these tests introduced here: Import objc forward declarations by NuriAmari · Pull Request #61606 · apple/swift · GitHub
However, that is the limit of the functionality. You will not be able to directly use the synthesized type itself in Swift in any manner. This is to keep the impact of the change small and to prevent unsound declarations, such as declaring a new Swift class that inherits from or conforms to such a type. You will also not be able to create new instances of these types in Swift.
If the complete definition for one of these types is imported, either in the Objective-C library or the consuming Swift environment, these changes will have no effect. That is, no new declaration will be synthesized at all.
Source Compatibility
This change may introduce name conflicts for users that have created accessible declarations with the same name as any newly imported Clang declarations. Thus we suggest keeping the change off by default initially.
Alternatives Considered
The unavailable attribute attached to synthesized declarations might be more restrictive than necessary. We considered the introduction of a new attribute to denote an incomplete type. This attribute would be used to prevent operations that are not suitable for an incomplete type (ie. declaring a Swift type that conforms to an incomplete protocol).
Allowing more use of the type however increases the complexity of the problem, and the likelihood of source compatibility issues.
Feedback
We are looking for input on if these might be reasonable stand-in types and any possible edge cases we might not be thinking of. In particular, input from any of @allevato, @xwu, @hborla, @beccadax and other members of the language workgroup would be great!