Cross-platform Objective-C Interop with GNUStep feasibility discussion

Hello Swift community,

Over the past few weeks I have been looking into what it would take to bring cross-platform Objective-C interop using the GNUStep runtime to Swift.

Although the GNUStep community is small, there is interest in having cross-platform Swift Objective-C interop. Another developer and I would be willing to work on it, so I’d like to get some feedback on my findings and discuss potential blockers.

I have written down some of my notes on what I think it would take to port different ways the Swift and Apple’s objc4 runtime interact to libobjc2 (the GNUStep runtime). Ideally, our goal would be to get as close to existing interop as possible, though I understand if we have to make some concessions.

As a first step, I would like to focus on the “@implementation extension interop” from SE-0436, since it does not require the Objective-C runtime to handle Swift objects and will likely be easier to implement. I’ll refer to this as “extension-interop” (as opposed to “header-interop”, where the bridging header is compiler generated).

Swift-GNUStep Feasibility

1. Metadata

Swift’s IRGen would have to be able to emit libobjc2 compatible metadata for classes, methods, protocols, etc. We might be able to make use of Clang’s CodeGen here to emit these for us to reduce the burden on Swift.

As stated in the original SE-0436 proposal, with extension-interop all bridged types are pure Objective-C types, and therefore Swift does not need to emit Swift class metadata for interfaces which have been implemented via Swift extension. This makes many things easier, since the Objective-C runtime does not need to be aware of Swift objects. With header-interop it gets trickier:

Swift’s class metadata was designed to be compatible with objc4’s metadata structure. When interop is enabled, Swift classes are slightly larger so the first 5 pointer-sized fields match objc4’s layout and contain objc interop reserved fields. Since this is not the case on non-Apple platforms where interop is disabled, we would need to find a way to add objc metadata to Swift classes without an ABI break. I don’t know how to solve this problem. Would it be possible to add a pointer “Immediate Member” (Which would be non ABI and could be added only to bridged types), which could then point to an additional record that stores the libobjc2 metadata?

2. Type Bridging

As I understand, there are three different ways objects can be bridged. Verbatim: Reference types are just forwarded across the language boundary. Bridged: Swift types that conform to _ObjectiveCBridgeable define a conversion method, which the runtime detects and calls. Many Foundation types do this. Boxed: Some value types are wrapped in __SwiftValues to make them usable from Objective-C. This generally seems to be portable aside from missing SPIs.

Here, my major blocker is bridging. GNUStep has its own Foundation with a different ABI, therefore the Swift Foundation bridging won’t work. The simplest approach (And frankly the only approach I can think of at the moment) to solve this problem would be to not bridge GNUStep types at all, and require explicit conversions.

3. Object Model differences (objc4 / libobjc2)

Objc4 supports three different ISA modes: Raw pointer, tagged and indexed/Nonpointer. Libobjc2 only knows raw pointers, so we would need to disable nonpointer ISAs. For header-interop, libobjc2 will have to support the “Swift bits” that indicate a Swift type in the ISA pointer.

Both objc4 and libobjc2 support tagged object pointers. This is an optimization, which allows embedding small objects (such as small NSNumber or very short strings) into the object pointer. Initially, we should be able to just disable these features for now, but this is a notable example where Swift directly accesses objc4 internals during bridging of built-in types. Eventually we might want an optimized codepath in the Swift runtime that supports libobjc2 “small objects”.

4. Memory Management

Objc4 uses side tables to map objects to their reference counts / weak tables, while libobjc2 has a slightly larger heap object, that stores the refcount after the ISA pointer. During interop, Swift and objc4 forward retain / release calls to the other runtime respectively, if they detect a foreign object. If libobjc2 is to support Swift objects, this mechanism will need to be implemented there as well.

5. Missing SPIs

Generally, I think the preferred approach to handle missing SPIs is to implement them in libobjc2. Most notably we are missing:

  • objc_constructInstance / objc_destructInstance should be easy to shim

  • _objc_empty_cache, this is just a constant for initializing the objc4 cache

Various SPIs related to runtime initialization (See section 6)

  • _objc_realizeClassFromSwift, _objc_setClassCopyFixupHandler, objc_readClassPair

6. Loading and Initialization

This is probably going to be tricky to get right. Swift’s metadata initialization is very complex and I don’t fully understand how it works and integrates with the Objective-C runtime yet.

There are a few SPIs missing that Swift uses to initialize the Objective-C side of a class, or to insert fixup callbacks. Most of the complexity seems to stem from the fact that Swift builds Metadata dependency graphs and may defer Metadata initialization using stub classes. Objc4 is aware of Swift’s model, and we would have to replicate the same behavior in libobjc2. Again, I believe most of these problems only affect header-interop, since the Objective-C runtime does not need Swift object awareness for extension-interop.

Conclusion

The question of GNUStep interop has been raised several times on the Swift Forums, but previous discussions have been inconclusive about whether there is enough community interest to justify the added complexity in Swift.

It seems to me that most of the effort in adding GNUStep interop would be confined to Swift’s IRGen and a few additional branches and ifdefs in the runtime, though I would need a prototype to say this with more confidence. I also want to clarify that I am not asking for the Swift maintainers to work on this, but rather I hope to answer the following questions:

  • Would you (the Swift maintainers) be willing to review and accept patches to implement GNUStep interop support?

  • If so, do you expect a feature-complete implementation before any upstreaming, or can we work on this gradually, for example by working on the extension-interop first?

  • Would be possible to add a pointer to the metadata of bridged classes on non-Apple platforms without an ABI break?

  • Do you have any additional concerns or ideas, for example regarding the type bridging problem I mentioned?

Thanks.

  • Hendrik
8 Likes

I haven't finished reading the post yet, but my understanding is that Swift isn't ABI-stable on Linux or Windows, so it doesn't matter if this is an ABI break?

Edit: Just finished reading the post. I don't have a lot else to say; I would love to see this happen but my ability to contribute to this effort would be limited.

1 Like

I wish I could help. I’ve tried to learn the compilers enough to get a grasp of the issues and tasks needed but I just lack the time. Regardless, I am 100% willing to donate money towards such efforts. I would not mind donating directly to someone or more preferably towards a github page. I want to see this working, and have a major interest in it working.

1 Like

I want to say they’re likely not interested, but I want my voice out there noting that I, personally, am VERY interested in this being a part of swift. I am 100% ok with using a fork if not accepted. I want it that much.

Yes, you are right. The ABI on non-Apple platforms is not stable yet. I think we still want to avoid ABI breaks whenever possible, however. Perhaps appending a pointer-sized field to the Swift class metadata entry would be acceptable, since this would be an ABI-additive change.

My compiler engineering experience is limited to a few non-ABI changes in Clang, so in any case this is something I would like an expert’s opinion on.

1 Like

Do you know if there is a more complete (or updated) version of this swift/docs/ObjCInterop.md at main · swiftlang/swift · GitHub

On Darwin, Swift and Objective-C share enough of their basic object representation that (1) arbitrary Swift object references can be passed around in Objective-C as values of type id and (2) arbitrary Objective-C object references can be passed around in Swift as values of type AnyObject.[1] We also bridge Swift values to Objective-C object references and back again, and the efficiency (and "obviousness") of bridging relies on having a common representation, especially when bridging complex data structures. But we don't have to do some or all of that just to allow basic Objective-C interoperation; Swift also has the ability to work with "foreign" class models that stay distinct from its native types. So I think the first question is what we actually want at the language level for non-Darwin platforms.

I see three options for how this could work:

  1. exactly like it does on Darwin, and enabled in all Swift code;
  2. exactly like it does on Darwin, but you have to build Swift in some special mode to enable it; or
  3. treating Objective-C object references as a foreign class model. (@objc class hierarchies known to be defined in Swift might be able to straddle both worlds.)

I don't think there's anything technical about GNUStep that would prevent us from embracing a fully Darwin-like model of ObjC interoperation using the GNUStep runtime. It's mostly a question of if we actually want to. Tight interoperation has a bunch of real costs:

  • Arbitrary AnyObjects are not necessarily Swift objects, so reference counting, weak references, etc. are all slower than they are when we know we have a Swift object. We can dynamically optimize some of that runtime code for Swift objects, but doing so relies on assumptions about the ObjC runtime that might be reasonable in OS-distributed code (as on Darwin) but maybe not in a package ecosystem where the ObjC runtime is an independent peer.
  • Similarly, dynamic introspection and casting are slower because they have to check for an ObjC object.
  • Swift's class objects have to function as ObjC class objects, so the static memory overhead of each class is substantially higher. On Darwin, this includes three extra words in the class object, but also a class_ro_t, a metaclass object, a class_ro_t for the metaclass object, and whatever the ObjC runtime has to allocate dynamically to realize both classes (which itself is an extra dynamic execution overhead). I think most of these costs have parallels on other runtimes.
  • The need to handle bridged representations adds complexity and dynamic checks to a bunch of core data structures. This can be avoided by eagerly bridging, which is to say, creating a completely new data structure with the values of the old one, but of course that makes some kinds of interoperation both (1) much more expensive and (2) potentially lossy if e.g. a particular NSArray subclass has more information than is captured the superclass interface.
  • The ObjC runtime and Foundation both become core dependencies that must be linked and loaded in every process that uses Swift.

These costs are worth paying on Darwin because of Objective-C's importance to the platform API. Some of them are also minimized on Darwin; for example, there isn't much cost to linking the ObjC runtime and Foundation, because those libraries are loaded into most Darwin userspace processes anyway, and the OS optimizes for that. But I wouldn't want to force these costs on other platforms just in case something in the process uses Objective-C; among other things, it'd feel a little weird to privilege Objective-C that way. So, personally, I don't think I can support option #1.

Option #2 would basically be treating Swift+GNUStep as a platform variant; you'd need to build + distribute special binaries for it whenever you wanted to use it. This feels like it might be a configuration nightmare because you wouldn't be able to just use a standard Swift distribution, you'd need one specifically built with this support. But if GNUStep folks are willing to bear the primary burden of maintaining this, and accept that new things might be implemented without initial support for GNUStep,[2] I do think it's manageable.

Option #3 would make ObjC work substantially differently from how it does on Darwin, which might be its own little nightmare in the short term for both users and the language. For example, we'd need to introduce some sort of AnyObjCObject to import id as. But in the long term, if we can make sure that the support doesn't require linking the ObjC runtime unless ObjC is actually used, I think this is probably tenable.[3]

(Edited the first paragraph to clarify that there's a sort of spectrum of possible behavior, especially around bridging.)


  1. which is also aligned with AnyObject generic requirements ↩︎

  2. I'll note that this is already true in clang, e.g. direct methods are not currently supported on non-Darwin runtimes. ↩︎

  3. Especially if it doesn't require hard-coding details of the ObjC runtime into the Swift runtime, so that you could pick an arbitrary ObjC runtime dynamically. Darwin has a platform ObjC ABI, but there are alternatives to GNUStep on other platforms. ↩︎

16 Likes

Personally, I prefer option #1 and #2. But option #3 is also acceptable for me. (Background: trying to port some CF based low level Darwin library into Linux without a full rewrite)

Another issue I concern is GNUStep + Swift's CoreFoundation module support.

I was wondering what if we need to use #import <CoreFoundation/CoreFoundation.h> in the ObjC code. It is supported on Darwin, and on some platform, we can workaround by adding search path to the SDK since CF header is indeed exist.

Is CoreFoundation import in C/ObjectiveC part of this proposal's story?

1 Like

We ought to be able to recognize and manage CF types without specifically needing ObjC interop. The foreign reference support we’ve done for C++ is probably most of the way there.

3 Likes

Thank you for for sharing your thoughts.

My goal would be to get as close to Apple native interop as possible, but I agree the additional runtime costs that Objective-C interop incurs should be opt-in and not enabled by default, given that the number of interop users is likely going to be small.

The ObjC runtime and Foundation both become core dependencies that must be linked and loaded in every process that uses Swift.

Could you clarify why this is necessary for a Swift process that does not use Objective-C? My assumption was that any GNUStep dependency can be loaded lazily at runtime, only when the user needs them.

Another idea would be to let the user decide whether to enable Objective-C interop in the runtime using some sort of flag like an environment variable, or by having the runtime detect whether anything in the Swift process is going to require Objective-C. A single global “ObjC interop enabled” flag guarding all additional interop specific checks would minimize CPU overhead on hardware with branch prediction.

I mentioned this in my post as well: libobjc2’s class metadata model differs from objc4. Would it be acceptable to extend the class size on non-Apple plattforms by one word, and use this word as a pointer to an optional metadata record (similar to class_ro_t and class_rw_t), which is only present for Objective-C types?

I’m unsure if anyone is willing to put in the effort to distribute special swift binaries. This would likely require users to build all their dependencies from source with interop enabled. I think your third option is also viable. As I mentioned in my post, type bridging is another big blocker. I understand if we have to divert from native Apple interop here as well and require the user to convert types explicitly, similar to how its done with C++ interop.

1 Like

It's possible that we could make that happen, yes. But it's not a small amount of work. For example, currently the Swift standard library contains Objective-C code to define the classes needed for passing Swift objects off as Objective-C. Objective-C compilers do assume that you're hard-linking against the Objective-C runtime.

Minimize but not reduce it to nothing. We guard a lot of runtime logging with similar checks, and that's fine in relatively cold operations, but I wouldn't want to put something like that in the core reference-counting functions.

Stepping back, there's also a separate-compilation vs. whole-program conflict here. If ObjC support is needed in the process, every compilation that defines or works with classes potentially needs to work to support that. But whether ObjC support is actually needed in the process is whole-program (maybe even dynamic) information. Like, the Objective-C object model requires each class to have both a class object and a metaclass object; that's just how the language works. If we want to pass a Swift object off as an Objective-C object, those objects had better both exist and be properly linked to each other, to the root class, etc. That's code the compiler has to emit whenever we emit a class unless we know that ObjC interoperation is definitively not needed for objects of that type. There are solutions to this, but not ones that don't require compromise and/or extensive changes to the ObjC runtime.

At the compiler level, bridging mostly just involves calling some Swift code from the bridging conformance. You can implement that however you want. It just gets very questionably acceptable to do all this implicit bridging if both directions of bridging require deep copies every time, so yeah, if that's the only option, you probably shouldn't bridge anything. But then the basic user experience is going to be radically different from how it is on Darwin.

2 Likes

I think this option (#2) is not only the correct one but it compromises well with the other two options. From an infrastructure standpoint, I understand it could be nightmarish. I am assuming what is required with option 2 is to distribute versions of Swift with GNUStep interop enabled on a variety of platforms, say Ubuntu 24 arm64, Ubuntu 24 x86_64, FreeBSD 15 x86_64, et cetera.

Other than building it for each of these platforms. Are there any cots involved? I would like to know what that looks like monetarily.

I imagine distribution to be similar to how the Swift wasm project evolved. For years I used their binary builds on GitHub. Which was fine.

I don’t imagine I would want to build it myself each time. But that isn’t totally unreasonable in the beginning.

Lastly, I wonder if in such a version the libobjc2 runtime could be bundled with GNUStep+Swift to lower the burden ld having to make sure the correct runtime is installed.

I would also really like to see this and have the full capabilities of Swift available everywhere eventually. I guess Apple sort of considers Obj-C on the way out but there are situations where it is more comfortable/nicer to use Obj-C than Swift. Or situations where you want the dynamicity of Obj-C and Swift doesn’t suffice at all without a lot of boilerplate code. Or situations where you might want to use a huge Obj-C framework that is infeasible to be rewritten in pure Swift. And the fact that you have to pick one or the other unless you are specifically compiling for Apple and they are incompatible otherwise even though they are both OPENSTEP-ish Foundation is extremely unfortunate.

What I would really like to see eventually is cross-platform Obj-C Foundation implemented on top of the cross-platform Swift one, for essentially “the de-facto” Foundation. And then being able to build GNUstep AppKit or whatever on top of that. But clearly that’s a ways off if it is ever going to happen at all.

FWIW there have been previous efforts into interop on the GNUstep side, for example hmelder was looking into it a while ago which is the most recent effort I’ve seen: objc4 class structure incompatibilities and Swift interop considerations · Issue #306 · gnustep/libobjc2 · GitHub

I think compiling Swift with this enabled vs disabled should not be classified as an “ABI break” in terms of having to worry about it on the Swift compiler side. It should be treated similarly to building C/C++ code with libstdc++ vs libc++, or glibc or musl, or whatever else that is usually a OS-wide decision that is not generally changed IMO. Unless you’re using an OS that lets you pick freely like Gentoo but with that the user is responsible for that anyway. I assume you can also build macOS Swift without Obj-C interop too, for example, in which case you’d hit the same problem, right? This should be up to distros to decide in case of Linux (though I guess that also assumes system-supplied/managed Swift similar to how it is on Apple platforms and would sorta fall over on Windows, but there I assume precompiled stuff would generally ship its own Swift runtime anyway).

On the other hand, making the Obj-C feature truly additive would be desirable, if it’s possible to make this a property of the runtime. For example Swift should allow an OS to supply binaries compiled without Obj-C without those binaries breaking when Obj-C is added to the system for something that requires it, it would be useful to be able to compile Swift so that it does reserve space for whatever Obj-C runtime compatibility is intended, while having that essentially unused until running with a Swift runtime that has Obj-C enabled. Though I don’t know how feasible that is to do when it comes to the support code required in the built executables.

In terms of package manager packages, I guess you would have some swift-app-foo package depending on either the Swift runtime either without or with Obj-C, and package for the Swift runtime with Obj-C would itself depend on libobjc2 and whatever else, and both runtime packages would block each other so you can have one or the other. And a swift-app-bar that uses Obj-C would depend on the Swift runtime with Obj-C exclusively, of course. So if you install both swift-app-foo and swift-app-bar on the same system you’d get the runtime with Obj-C and both apps would load the Obj-C runtime even if swift-app-foo isn’t using it. I think that would be the cleanest situation that would make this comfortable to use without outright hard-depending on the Obj-C runtime.

That said Option 2 as @John_McCall suggested is the one I would go for too, with this being a SDK/runtime build-time switch between “no Obj-C support in built binaries ever”, “GNUstep support stub”, and “GNUstep support enabled”, with the latter two having the same ABI and same codegen for binaries the Swift compiler produces, and the former two not depending on a (functional; stub functions might perhaps be needed) Obj-C runtime. I don’t think Swift should special-case GNUstep as “the one” non-Apple Obj-C runtime, which means you should at least be able to turn support for it off at SDK build time.

4 Likes

Hey Hendrik,

I am the lead of the GNUstep project. Please join the gnustep discussion group, find out how to join here:

The list to join is discuss-gnustep. I and others would love to work with you on this.

Yours, GC

5 Likes

I don't believe it is possible to build Swift for any Apple OS (other than MacOS 9) without Objective-C support. It's embedded too deep in the ABI.

  • Would you (the Swift maintainers) be willing to review and accept patches to implement GNUStep interop support?

I would hope so. This is critical to the whole idea of interoperability with GNUstep. GNUstep’s runtime and frameworks were created before the Swift compiler was conceived. It was, however, created with clang compatibility in mind. I am not sure if this helps us at all with respect to swift interoperability.

The challenge will be compatibility with the implementations in GNUstep. The first hurdle is, obviously, libs-base/Foundation. The issue here is this… while these classes are API / source-compatible, they are not ABI-compatible… but I am not sure they really need to be. What we are focused on here is the ability to build Swift source on other platforms.

Having done some research into this… if I recall correctly, there is a define that is used to turn off Objective-C interop in the Linux version of the Swift compiler. It may be helpful to look into this to see if we can implement those parts for GNUstep. I may be well off base. I know GNUstep’s code base very well, I implemented a lot of it. I know next to nothing about Swift’s code.

I am glad to provide any insights I can into helping make interoperability happen.

Yours, GC

4 Likes

I hope we can keep this discussion as much as possible here where swift engineers can chime in easily.

1 Like

Hi Gregory,

I’m happy to see that you are also interested in this. I’ve spent some time looking into Swifts compiler internals already before starting this thread. You are right, there is a macro guarding much of the interop specific code but there is still a lot to be done and some APIs that Swift expects are not implemented in libobjc2. Your help with these would certainly be appreciated.

Right, thanks for clarifying. I would prefer the second option, since my goal is to enable users to create cross-platform interoperable Swift-ObjC applications with minimal headaches. This still seems feasible, assuming we can bridge GNUstep and Swift Foundation types.

My understanding of the bridging mechanism is the following:

Bridging is enabled for types which conform to the _ObjectiveCBridgeable protocol. Shared mutable references of bridged types are not possible (E.g. A NSArray and a bridged Swift Array behave as separate copies, even if the underlying data is shared).

Some types are copied immediately - this is what you referred to as eager bridging, correct? Other types use Copy-on-Write optimization and may share the same underlying storage until modified (lazy bridging). I have not looked at each type in detail but it seems like the main way to do this is to create wrapper objects around the data structure from the source language. In some cases this might be feasible only because of some shared binary layout between Swift and Apple’s Objective-C implementation, which is not guaranteed to be compatible with GNUstep. Initially, we could bridge everything eagerly since this would not require any shared data. I agree that bridging everything eagerly could be detrimental to performance due to unnecessary copies, but users still have the options to avoid implicit casts in performance sensitive contexts by not using bridging alltogether. Later, we can add lazy bridging optimizations where possible.

The code that implements bridging mostly lives in Swift Corelibs Foundation. For GNUstep interop, we would have to add an optional "GNUStep compatibility mode" in the form of a compile time flag here as well, which includes a dependency on GNUStep's base libraries / Foundation.

I would be willing to start working on this, but we should clarify what the development process for a feature like this looks like. I assume you will want to see a prototype before any serious code reviews happen. Would demonstrating basic interoperability without type bridging be enough?

I had also hoped some other Swift maintainers would be willing to comment on this thread - especially if the question “Do we even want this upstream?” has not yet been fully answered. Perhaps I should make a second post with a more formal proposal?

1 Like

Right, so _ObjectiveCBridgeable is essentially just a pair of functions that turn a e.g. NSArray<NSString> into an Array<String> and vice-versa. You can, of course, always implement these by e.g. just constructing a Swift Array value with the same apparent information content as the original NSArray object (and vice-versa), and that's what we call "eager" bridging. There are a variety of "lazier" implementation techniques — lazy in the sense of delaying (hopefully avoiding) work at runtime, of course, not in terms of implementation effort.

One simple approach is that Array could simply store either an NSArray or a native buffer reference. This is essentially what Swift did on Darwin at first. Bridging into Swift just constructs an Array in the bridged case. Bridging an Array in the bridged case back into Objective-C just returns the NSArray object. Bridging an Array in the native case could be eager, but a smarter approach is to just make Swift's native buffer objects subclass NSArray, implementing methods like -objectAtIndex: by accessing and bridging the element value. Of course, you would then want to make sure that native buffer objects that have been bridged to Objective-C get re-bridged into Swift in the native case.

We've explored going a step further in Apple's Foundation by using Swift's Array buffers as the standard implementation of NSArray and NSMutableArray, so that bridging an NSArray always falls into that native case unless it's some non-standard subclass. That would change the bridging calculus by quite a bit, to the point that we could consider eagerly bridging those non-standard implementations to avoid the runtime burden of checking cases. Someone who works on the standard library would have to fill you in on the exact details, and I don't know where it actually stands.

6 Likes