Why would a framework w/Swift module built with Xcode 15 Beta / Swift 5.9 not be importable by previous versions of the compiler?

Silly me I decided to jump both feet into Xcode 15 Beta + Swift 5.9 because if/switch expressions were too good to pass, and because incremental build times seem to be back to normal.
Unfortunately we build and distribute a framework with our software. The framework includes a Swift module to export various Obj-C/C APIs renamed-for-Swift, and actual Swift-based APIs.

I thought that module stability meant that previous versions of the compiler could continue linking against the framework as long as I didn't expose any new language features. But sure enough, when I try to import the framework built with Xcode 15 / Swift 5.9 in an Xcode 14 / Swift 5.8 project, I get the dreaded error:

Compiled module was created by a different version of the compiler; rebuild 'XYZ' and try again:

What am I missing? Thanks for any insight!

Someone knowledgeable can explain fully, but as I understand it, Swift's module stability guarantees forward compatibility but not backward compatibility. That is, as you've found, newer compilers can emit stable interfaces that can't be parsed by older compilers, while the interfaces emitted by older compilers are guaranteed to be parseable by future versions. And unfortunately there's no way to get a compiler to emit an interface compatible with older versions, so you need to keep compiling your stable modules with the oldest version of the compiler you support, and you can't move forward without dropping that version. This is particularly troublesome now that Apple specifically breaks old versions of Xcode in new versions of macOS, so you need to keep a compatible environment around, or pay for access to a remote CI system.

1 Like

Thanks for the clarification... As fas as you know, is the problem limited to compilation or does it extend to linker behavior at runtime? In other words, if I shipped this framework/Swift-module built with 5.9, will it break existing software that was linked against the same framework/module built by Swift 5.8?

I hope there is a window to get this working, even with some compromises, as otherwise the Obj-C/C approach seems like the only sensible approach for framework development. Considering that (AFAIK) Swift also lacks the ability to export/import symbols weakly, this is just too darn impractical.

The Swift standard library (and OS frameworks built in Swift) shipped on new iOS versions is built with the new version of Swift but is backwards compatible with apps compiled with an older version of Swift, so that's a use-case I'd expect to be well tested and work.

2 Likes

I'm still in the process of testing it out, but it seems the solution lies in the BUILD_LIBRARY_FOR_DISTRIBUTION build settings.
This is discussed in Library Evolution, and I assume it requires a bit of extra work on the part f the compiler thus it was made opt-in rather than enabled by default for all dynamically loadable target you may be building. :crossed_fingers:

Update: that build setting wasn't enough. Now Xcode 14 complains as follows:

Failed to build module 'XYZ'; this SDK is not supported by the compiler (the SDK is built with 'Apple Swift version 5.9 (swiftlang-5.9.0.114.6 clang-1500.0.27.1)', while this compiler is 'Apple Swift version 5.8.1 (swiftlang-5.8.0.124.5 clang-1403.0.22.11.100)'). Please select a toolchain which matches the SDK.

...which begs the question: what does it mean by SDK? the macOS SDK, or "I'm calling your library an SDK"?

Much of the work of Library Evolution is on the part of the author rather than the compiler. Publishing a stable API and ABI is a nontrivial ongoing effort. It may also reduce performance without careful use of @frozen and @inlineable, which have their own tradeoffs.

1 Like

I'm not even trying to expose an actual Swift-based API... all I need is a few symbols to C functions that have been beautified to have nicer Swift names via the NS_SWIFT_NAME macro. The SDK error has me stumped...

I was not able to get past the "this SDK is not supported by the compiler" problem in reasonable time. Maybe there is a way to build a framework with latest Xcode and macOS SDK that is still usable from prior versions of Xcode and macOS SDK...but I just gave up.

For posterity, if you are a third-party framework developer and you are exposing C or Obj-C APIs only, while privately your framework also has Swift sources, you might want to read on for ideas.

When a framework contains Swift sources Xcode automatically creates an entry in your .modulemap file and a .swiftmodule directory to "spell out" what exactly is being exported, along with the metadata that is used to verify compatibility between said Swift module and the exact version of the compiler + SDK you happen to be using. And in private builds of your software it will always make sense to keep this information there, just so your various targets compile and are able to access the full spectrum of C, Obj-C and Swift APIs.
But when it comes to distribution, you can wipe out the .swiftmodule directory and remove the module entry associated with Swift content in the .modulemap file. You end up with a .modulemap file containing only the entry for your framework, i.e.

framework module XYZ {
    umbrella header "XYZ.h"
    export *

    module * { export * }
}

Whoever attempts to link to your framework will naturally only be able to access C and Obj-C APIs, but you can still embellish them at will with NS_SWIFT_NAME and you get lots of stuff "for free". You don't have to worry about any performance issues that derive from BUILD_LIBRARY_FOR_DISTRIBUTION (which in fact can remain disabled). Your clients can link your framework weakly and conditionally/safely access weak-linked APIs from Swift: simply write inline C functions in the header that allow your Swift code to check if a weak symbol is nil before calling it.... While the Swift compiler AFAIK doesn't have provisions for dealing with weakly linked symbols, it has no trouble using an inlined C function to do that job. Above all, if you stick to C and Obj-C for the public API, the C calling conventions and Obj-C dynamic dispatch give your framework immunity from some of the peskier problems described by the Library Evolution docs.