[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: ClangImporter: Allow for structs with arc pointers to be constructed in objc mode by AdamCmiel · Pull Request #64381 · swiftlang/swift · GitHub. 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!

13 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.

1 Like

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

PR updated with a more complete implementation here: ClangImporter: Allow for structs with arc pointers to be constructed in objc mode by AdamCmiel · Pull Request #64381 · swiftlang/swift · GitHub

I see that there's a proposal PR now, so I'll bump this thread by asking if there were any interesting restrictions or corner cases you ran into while implementing this.

I would say the most interesting part of the implementation so far to me was the loadable-vs-indirect distinction with strongs-vs-weaks and tracing that back to the Clang's loadable decision in the llvm changes. Related but not the same (and in cxx interop mode): #93040

As well, and probably worth review is the bridging design. Right now I make the bridged types available as public computed properties on the structs where the original stored vars become private to Swift clients. Therefore the original stored objc type still defines the layout but is private where the Swift type becomes a public computed var with { get set } access.

1 Like

Two implementation surprises emerged around returns: ObjC methods must treat these non-retainable records as Owned: [SIL] Take ownership of non-retainable record returns from ObjC methods by AdamCmiel · Pull Request #93078 · swiftlang/swift · GitHub, while C functions must model them as indirect SIL results even when Clang returns them directly: [ClangImporter] Import C structs with strong ARC pointer members by AdamCmiel · Pull Request #64381 · swiftlang/swift · GitHub

The latter is surprising. We should model imported C types as loadable in SIL if Clang says they can be passed in registers; I can imagine that not being set correctly right now.

We already know how to actually pass/ return “direct” parameters/results indirectly if the C ABI says that’s how it works for a specific function type, like when the type is very large (or the psABI is inefficient about aggregates).

You know, you're right, we don't actually need the indirection here in SIL. I had pulled the indirect-result handling from an older, separate investigation and I don't think I pushed on the indirect at the time as I was targeting the generated clang ctor/dtor instead of generating a per-field VW.

For the supported strong-only records, the SIL result certainly should remain direct and @owned and IRGen already lowers it to either registers or sret according to Clang's ABI classification. I've updated the patch and added coverage for both paths, as well as nested records and C function pointers. Let me know what you think!

1 Like