Why the new features in Swift 5.7 needs the latest OS version support?

I’d like to point out that this problem is not unique to Swift, neither in theory nor in practice. For example, you can’t build C++21 apps for systems that only ship with C++17 runtimes. You can’t have two C++ runtimes in the same process. (Well, you can try, but it usually blows up spectacularly.) System libraries use C++ under the hood, and sometimes expose C++ interfaces (e.g. DriverKit), so you can’t just statically link libc++. It’s just that Swift evolves faster than C++ nowadays.

4 Likes

Discussions about specific technical decisions Apple has made and how they impact the Swift development experience are undoubtedly on-topic for these forums (though they may not always receive the most satisfactory response). Indeed, the topic of the inflexibility of OS upgrades has come up before and I think there's been valuable points raised.

I agree with @itaiferber, though, that generalized grievances and speculation about Apple's company priorities do not seem like a particularly productive topic of discussion.

3 Likes

C++ (in practice) has a different runtime/standard library split from Swift, which makes it mostly a non-problem for developers interested in supporting older OSes. Supporting backdeployment merely limits what portions of the standard library you can use and not which language features you can use, and because the standard library is much less special in C++ than it is in Swift, you can just use a third-party implementation of the portions of the standard library that are unavailable. Most new standard library things also happen to just work due to being header-only too. We write C++20 targeting iOS 9 (soon to be 11...), and it's really just not a problem.

The reason the MacPorts packagers are having to do complicated things are because they're trying to backport software to older OS versions than the upstream developers support.

Parameterized protocol types introduce a new kind of runtime type metadata which describes their layout and is distinct from all other kinds of type metadata (because it's a distinct type). The runtime has to know all the possible kinds of type metadata so that you can do things like put type metadata inside of an 'Any', bind it to an unconstrained generic parameter, or print it out.

Not all usages of parameterized protocol types require instantiating the metadata at runtime, so the type checker has a diagnostic pass to attempt to detect the usages that would be unsupported on an old OS.

It would be very difficult to backward-deploy a new kind of metadata without shipping large parts of the runtime with the application, so that's why it was implemented in a way that requires a new OS.

11 Likes

As a counterpoint to some of the negatives here, I’m generally impressed with the effort the Swift team goes to in making most features compatible without runtime support. It would be easy to just say “new swift features require a compatible OS” but a fair amount of this stuff actually works. It’s sad it doesn’t all work, but everything is a trade off and I’m thankful for the efforts put in.

15 Likes

C++ does have language features that are heavily supported by the language runtime, including dynamic casts and exceptions. That support is not any better able to be patched to support future language evolution than Swift's runtime is; it's actually quite a bit less patchable in practice. The main reason C++ programmers don't have frequent back-deployment issues around these features is simply that C++ has largely abandoned the idea of evolving them at all, and most C++ programmers pretend that they do not exist.

There are a handful of other C++ features that technically require support from the language runtime, like aligned operator new, but which are less integrated into other support. Formally these features also have back-deployment problems, but by their nature, I think programmers have found it easier to work around those limits. We've been able to do similar things in Swift for similar sorts of features, but not every feature cleanly fits that bill.

Dynamic casts are, not coincidentally, one of the biggest reasons we made the decision not to try to back-deploy the reflective / type metadata parts of parameterized protocol types. We could not find an acceptable way to back-deploy dynamic cast support for these types, so if we didn't do anything, casts were doomed to misbehave on old OSes. Intentionally introducing behavior that varied so wildly by target OS seemed much worse than locking down the things you could do with these types. And there's no way to prevent dynamic casts if you allow these types as generic arguments.

16 Likes

I'd consider std::any an example of C++ successfully evolving dynamic casting in a backwards-compatible way even though it does not involve the C++ dynamic_cast feature. "Will the runtime have to be updated to support boxing this new thing in std::any?" is not even a particularly meaningful question to ask, while for Swift's Any it is.

There are massive downsides to C++'s approach of doing everything that can be done in a library in a library, and I certainly don't think that Swift should do the same. I think Swift has successfully shown that baking things which could be "just libraries" into the language results in a much better language, and on the whole the tradeoff of some things needing runtime updates rather than building a much less usable feature that doesn't is very worth it. I just think it is disingenuous to claim that backdeployment is an actual problem that comes up in practice when writing C++, and therefore Swift hasn't made a tradeoff.

1 Like

This is a very good point. All the use-cases I had are for applications that don't really integrate with the OS common IO (e.g. web servers).

I don't have much experience with Windows, but presumably lots of their system APIs are exposed in C#. Could someone from that side of the curtain chime in with how do they pull that off?


Coincidentally, I was listening to some old interviews with Anders Hejlsberg (lead architect of C) and ran into this bit which sounds remarkably similar (at 17:50) https://youtu.be/5hiJElu8jpo?t=1070

... probably the biggest influence was when we added generics, which was from .NET 1.0 to .NET 2.0. That was a big change and it had lots of new op codes in the runtime and new metadata formats and so forth, but then we made what I think in retrospect was probably a mistake, in saying that the .NET runtime is a Windows component and it ships with the Windows operating system and it cannot run side-by-side with other versions of .NET.

And that in a sense, put handcuffs on evolving anything in the runtime, and there was a number of years there where really, we sort of bent over backwards to do everything in the compiler instead of in the runtime.

But now with with .NET core, where we've finally gotten to where I think we should have been in the first place, which is: you can install runtimes in a sub-directory, they can run side-by-side you could delete it all again, and it's gone it's gone, like it never was on your machine. At that point you can have different capabilities in the runtime, and we're starting to look at at things now that we couldn't have done before.

This is a tangent for sure, but std::any uses existing C++ typeid (a feature that does require runtime support), and does not support downcasting. The comparable feature isn't adding new types to put in std::any, but adding downcasting support to any_cast. If we're lucky, the info provided by typeid would already have inheritance information in it, but if it doesn't, there'd be no way to backwards-deploy that feature, and you would indeed get behavior that "varied wildly by target OS".

EDIT: to clarify, by downcasting I mean "I put a UIView equivalent into a std::any and tried to get it out as a UIButton".

DOUBLE EDIT: I just thought of a way to do it without relying on the type info for the value inside the Any having inheritance information! This is what happens when the Swift compiler/runtime engineers say "we're not sure whether we can backwards-deploy this" and later on say "good news, we've figured out how to backwards-deploy this".

9 Likes

They don’t. Windows APIs are exposed as either C interfaces or COM interfaces. (C++/WinRT is COM under the hood.)

And for what it’s worth, for decades Windows apps written in C++ shipped with the C++ runtime they linked against. This is what caused DLL hell; bad installers would overwrite newer runtimes with older ones, breaking apps.

In Windows 10, Microsoft decided to adopt the same approach as macOS, and now ships a single “universal” C++ runtime with the operating system.

8 Likes

I've met the Regex error requesting to use iOS 16, but "any" worked just fine with oldest installed iOS 14.5 and target 13.0 :thinking:

The language runtime on older operating systems is fully able to support the any keyword — the issue being referred to here is that the older runtimes cannot fully support parameterized protocol types. So this code can compile for older OSes:

let x: any Sequence = 0..<10
let y = x as? any Collection

But this code can't:

let x: any Sequence<Int> = 0..<10
let y = x as? any Collection<Int> // error: Runtime support for parameterized protocol types is only available in iOS 16.0.0 or newer
4 Likes

you should have to try let variable: [any YourProtocol<T>] = [...], then enumerate & use it

1 Like

As mentioned up-thread, there's a straightforward workaround for almost all such use-cases, which is to wrap the type in a struct, like:

struct MyProtocolWrapper<T> {
  var value: any MyProtocol<T>
}

It's unfortunate that you have to do this in the short term, but it's also not that much of a problem in practice.

4 Likes

I agree that a newer version of Swift should work on older versions of macOS. Several older versions of Swift up to the most recent 5.7 work on Linux Ubuntu 18.04 or 20.04 and on Windows 10, no newer version of Linux or Windows needed.

It's a fact that it's been different on macOS for several years but in my opinion that doesn't justify that it should stay like that for years to come. Please Apple / Swift Team consider more backward compatibility.

A good example of a C++ feature that depended on runtime support and didn't get timely support on older OSes was thread_local. Apple OSes didn't support it for a good few years after upstream clang and libc++ added support, in Xcode 8, and you still needed to target iOS 9 or OS X El Capitan to use it.

On the Swift front, we are continuing to work on functionality that will make it easier for platform vendors to back-deploy new APIs:

It would be interesting to see this back deployment feature extend to other declarations such as new types in the future too.

13 Likes

The first problem is that the OS itself has frameworks using a specific version of Swift, and your application is dependent on the frameworks which ship in a specific version of the operating system. Multiple versions of swift in a single process would mean incompatibilities - for instance, the String instance your application creates may have a different binary format in memory than the ones the Swift library inside the framework was built to accept.

It could be possible to ship a new OS minor version with tweaks to support new features (see: concurrency), but the assumption of applications running on the system is that breaking changes will not be added as a surprise in minor/maintenance releases.

The second problem is that the OS will sometimes gain new API at the native library/kernel level to better support features being added to newer Swift versions. For things like concurrency, there was a separate implementation (with different performance and scheduling) that needed to be written to support the back port.

3 Likes

Of course the implementation and optimization of Swift varies between platforms. Taking advantage of kernel and other advancements on a particular OS is a given. However, this should not prevent deployment on some earlier versions of macOS.

I assume that Swift 5.8 will be deployed on Linux 20.04 and Windows 10, we won't need to ask our customers to update their Linux or Windows. But on macOS we will have to upgrade to macOS 14 and we will have to ask our customers to upgrade to macOS 14 as well.

It is desirable at some point to take a step forward at the OS level for multiple reasons. However, forcing our customers to update macOS every year to use the latest Swift features is very restrictive.

1 Like

Important to note, neither Linux nor Windows are ABI-stable WRT Swift, and as consequence of that none of their system frameworks can expose Swift APIs.

3 Likes

swift on linux and windows is like the wild west. you have a lot of freedom, and you can evolve faster because you are a pioneer and there is not a lot of existing support infrastructure. but that is also a drawback: there is not a lot of existing support infrastructure, so you have to do most things yourself.

swift on macOS is like the settled east. you don’t have as much freedom and you basically have to wait for the surrounding society to upgrade before you can advance. but you benefit from the settled and mature ecosystem of that world.

so “very restrictive” is very much point-of-view dependent. you could also argue that the lack of existing frameworks on linux (and windows especially) is also “very restrictive” in a different way.

2 Likes