[Pitch] Import C structs with ARC pointer members

Happy Wednesday everyone! We’d like to propose an extension to the ClangImporter for Swift <> Objective-C Interop.

See a draft PR for importing Automatic Reference Counting (ARC) pointers in C structs in Objective-C mode here: https://github.com/apple/swift/pull/64381/files. Note that the draft does not do type bridging for strong references yet, as that involves more SILgen work and I'd like some feedback before venturing down that route.

Introduction:

Today Swift can import C structs whose members are Swift-importable value types such as int, BOOL, and __unsafe_unretained ARC pointers, which are all trivially construct- and destructable. Building on Akira’s work enabling strong and weak references in LLVM, we should be able to import these types and declarations.

Motivation:

Since 2018, these pointer types have been available to use in Objective-C/Objective-C++ (and before that in Objective-C++) and Swift has, to date, not been extended to import them. This is a necessary part of Swift <> C++ interop, and we should be able to port default and member-wise construction and destruction of reference type members of value types, including ARC pointers in structs in Objective-C language mode. Puyan has already merged some code for constructing these types using C++ interop here.

Proposed Solution:

#import <Foundation/Foundation.h>

typedef struct WithArcPointers {
    __unsafe_unretained NSString *usUR;
    __strong _Nonnull NSString *sStr;
    __weak _Nullable NSString *wStr;
    NSInteger count;
} WithArcPointers;

should import to Swift as

struct WithArcPointers {
  init(usUR: String!, sStr: String, wStr: String?, count: Int)
  var usUR: Unmanaged<NSString>!
  var sStr: String
  weak var wStr: NSString?
  var count: Int
}

The various arc storage types should be preserved, with the expected type bridging and nullability annotations respected where possible. Weak members must be Optional on the Swift side, and __unsafe_unretained as Unmanaged of an unbridged type. These structs should be member-wise constructable, and if they contain no nonnull members, trivially constructable as well with an available init().

Nonnull weak references in structs, while legal in Clang, will not be imported and the compiler will emit an appropriate diagnostic, as Swift requires weak references to be Optional. The C declaration should probably have a diagnostic as well, in line with ObjC classes that appropriately error, but that’s out of scope of this proposal.

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, and possibly enabling by default for Swift 6.0

Alternatives Considered

Because rewriting those C structs types as Objective-C types will involve non-trivial amount of metadata and more code generation (effectively turning each member access into a computed var with a getter and a setter), such a rewrite may be less desirable than importing the types unbridged. There are binary size tradeoffs to consider here as well as ergonomics of using the bridged Swift types.

More codegen for type bridging should happen in the SIL, but I haven’t gotten to that part yet. If there’s a better place to implement that (either in the ClangImporter or IRgen or all three), I’d appreciate the feedback.

Feedback

We are looking for input on if the proposal can be implemented as described, and any possible edge cases we might not be thinking of. In particular, any member of the community that works with Objective-C interop, or any people working with the Objective-C++ Interop would be great!

12 Likes

This is certainly something we (Realm) would like to have. It hasn't been a big pain point, but we currently have a handful of internal obj-c wrappers for structs that exist purely to work around the lack of this, which is both some boilerplate and is bad for compiled library size.

If this is off by default then it would be a requirement for us that enabling it pre-Swift 6 be doable in a SPM-compatible way.

It hasn't been a big pain point, but we currently have a handful of internal obj-c wrappers for structs that exist purely to work around the lack of this, which is both some boilerplate and is bad for compiled library size.

This is our primary motivation at Meta as well, enabling direct interop instead of (adding) massive numbers of ObjC classes.

What is the opposite of “by default”? Would this be enabled in Swift 5 only by a compiler flag, or could it be enabled in the ObjC code with an attribute?

I struggle with this a little bit. How will the init behave in the 3rd case? The init will instantiate an instance of NSString, storing it in a weak pointer - and then immediately release the only strong reference in the epilogue?

Correct. Probably an "upcoming feature" compiler flag: swift/Features.def at 7064e18c0435648df17e01fdef77651650c6d1d9 · apple/swift · GitHub

@stuchlej , good point, the weak and unsafe_unretained params for this struct should both be NSString. I imagine for a possibly bridged type that the strong reference (as it is the default) would be by far the most common.

What I'm more looking for in this pitch is if the interface for the strong member should be bridged, or if we should just expose the un-bridged type. That would significantly simplify the implementation, but may be awkward at the API.

My gut feeling is that they should in fact be bridged, but I need to dig more into the SILGen part of the implementation then.

@jayton , yep, as @drodriguez said, this availability would be behind a feature flag. I'll add that on the PR as well.

1 Like

Feature request: make it possible to opt-in on the C/ObjC side using an __attribute__((__swift_attr__(importable_struct))) or similar.

Is there another example in the Clang Importer that uses that pattern?

Does your proposal to have the feature flag work over all the structs and have that attribute work when the feature flag is not enabled?

Feature request: make it possible to opt-in on the C/ObjC side using an __attribute__((__swift_attr__(importable_struct))) or similar.

I'd rather opt-in with the feature flag and then if a given struct would like to opt-out of Swift visibility, could use the existing __attribute__((availability(swift, unavailable, message))) with the convenient NS_SWIFT_UNAVAILABLE if linking Foundation