The adoption of `import FoundationEssentials` throughout the package ecosystem

With the new official Android SDK, binary sizes are becoming a bigger issue for Swift and may be blocking some adoption, and one of the reasons is especially the use of import Foundation throughout the package ecosystem. This is due to the size of the ICU package in Foundation, and there are ongoing discussions on how we could improve that situation on platforms, such as Android.

Several popular packages, that people would like to use on Android, still make use of import Foundation. From what I could tell, library authors generally want to use import FoundationEssentials, but because of Swift leaking the public API of imports, this cannot be done in a non-source-breaking way. Therefore, requiring the release of a new major version.

Also, improving this situtation would of course also have benefits for other platforms, such as Linux, not just Android. But it is especially important for Android adoption imo.

Here are some of the issues/PRs I could find of this issue in popular repositories:

I am just creating this thread to bring the situation to the surface again and opening a discussion. Particularly, I am curious if the authors of the above libraries would be OK with cutting new major releases because of this change. And if not, why not?

Thanks! :smiley:

11 Likes

Making a dependency conditional with traits also has an issue currently because you can't override traits from a transitive dependency.

So in my case, by using Hummingbird-Elementary, I'm locked into using swift-configuration with the default traits (which depends on Foundation) through Hummingbird: Can't use package with traits disabled. Ā· Issue #769 Ā· hummingbird-project/hummingbird Ā· GitHub

(Obviously if this was for a project I'm getting paid for to work on and maintain, forking everything would be a given and then making those changes myself would be easy)

Thanks for surfacing this again, Mads. As you mention, the linked thread does discuss some potential solutions to getting the ICU dependency size down (externalizing the ICU data so it can be thinned and not duplicated across the Android architectures, or adding a FoundationAndroidICU substitute library that uses JNI upcalls for its i18n needs in order to share the system ICU data, etc.) But it isn't something that is going to be solved in the short term.

You are right that this is hampering adoption: it is the number one reason our users cite for sticking with Skip's "Lite" mode (where a transpiled Hello World apk is 17MB) rather than the "Fuse" mode that uses the Swift SDK for Android (where a compiled Hello World apk is 124MB). Solving this is a high priority for us.

But in the short term, eliminating unnecessary dependencies on FoundationInternationalization would be a great help. I understand why this causes a source-breaking change (and thus forces burning a major release under the policy of many projects):

#if canImport(FoundationEssentials)
import FoundationEssentials
#else
import Foundation
#endif

… however, I wonder if perhaps this might pass muster:

#if os(Android) // and os(WhateverElse), like MUSL…
import FoundationEssentials
#else
import Foundation
#endif

It might require the call site to add an extra import sometimes, but given that this would only be for platforms that weren't (officially) supported until now, maybe this is a change that wouldn't need to be considered "source breaking"? After all, adding Android support sometimes requires adding extra imports anyway (see Exploring the Swift SDK for Android | Swift.org), so adding Android platform support to a package isn't expected to always just work "out of the box".

3 Likes

Related discussion here - FoundationEssentials on Darwin?

I think the goal here is the right one. I think it’s going to be very difficult to achieve without the tooling in place to make it a usable developer experience

1 Like

A smart way of using the Unicode data of the host system either for Android, Linux, or Windows and so reducing the usage of the ICU data and so bringing down the deployment size of Swift programs would be great. Maybe someone can explain whether this is not possible or just difficult or not efficient enough?

The problem is, when you need one thing from Foundation that is not in FoundationEssentials, then you have it. The division of packages would need to be more fine-grained to help in any circumstances, up to the point that maintenance and usage becomes difficult. A better solution in many cases might be link-time optimization (LTO). To my understanding there is some functionality, but not ready for prime time, so maybe needs more focus?

For Android, the problem is that Android's icu4c is quite minimal and doesn't expose nearly enough API for FoundationInternationalization's needs. There's no date or number formatting or parsing, no calendar logic, etc. Plus, it is only available from Android 12 (API level 31), which is on only 75% of devices. And the ICU data file format is unstable and isn't forward or backward compatible, so even if we could locate it and try to load it directly from the file system, we wouldn't be able to reliably parse it. Different API levels use different versions of ICU.

That's why I suggested making a shim library that passes the icu calls through JNI to the Java ICU4J packages (like android.icu.text and android.icu.number), which is much more complete than icu4c and is available since API 24 (95% of devices). However, there is no guarantee that it would have sufficient API coverage for Foundation's needs, it wouldn't contain Apple-specific ICU functions that are relied on, and it could very well introduce unacceptable performance bottlenecks. It might work, but it would take a lot of work to find out.

We aren't the only ones facing this issue on Android. Flutter apps also need to bundle their own i18n data files, but they include tooling to thin out their data files for just the locales the app supports, which greatly reduces the size of the data file. We'd need to create our own equivalent tool (perhaps based on the ICU Data Build Tool | ICU Documentation). I had a draft PR to explore the possibility of externalizing the data file, but it never garnered much attention (and then got accidentally closed).

Anyway, there's your answer for Android :slight_smile: Other platforms might have similar reasons.

3 Likes

FWIW, I personally think we should take those source-breaks and live with the potential of breaking a few builds. Releasing new majors of packages is an even more breaking change as we have seen with the gRPC 2.0.0 release. We already have a few packages in the ecosystem that made the transition from import Foundation to import FoundationEssentials. I will bring the topic to the Ecosystem Steering Group in our next meeting to discuss further.

11 Likes

I completely agree. I’m convinced projects that prioritise importing FoundationEssentials will happily accept a source break in return. Conversely, projects that don’t care will almost certainly already have an ā€œimport Foundationā€ somewhere in their code and therefore won’t even notice the source break.

1 Like

I don't really understand the Android issue. Don't Java/Kotlin apps have a way to do localization related things? If so, why not just delegate that task to the host system? I mean not in Foundation using the system ICU but the developer using the system ICU like they would in a plain Java/Kotlin app and then not using the whole Foundation, just FoundationEssentials.

And then it's the same problem as other platforms where some common packages import Foundation for no reason and then don't remove that import because that would be a breaking change, so the developer has to maintain their forks with Foundation removed.

I’d actually also push on the side of ā€˜just do it’ for these… It’s a looming problem ever since the Foundation split was introduced but we’ve not benefitted from it as much as we could have because these compatibility concerns.

It is true that the problem of relying on a ā€œleakedā€ Foundation import is likely to hit people who were doing so unknowingly, but on the other hand, the solutions are not that hard of a lift… I’d personally be supportive to take a lanient approach to guaranteed source compatibility here and just go for it, and address the breaks wherever necessary.

That absolutely does seem like the most useful medium/long term goal to strive for - hope we can figure out the when and how to get progress towards that. If this is a high priority as you say, we could figure out a road towards this solution I’m sure.

1 Like

Of course, but as Marc explained above, Foundation relies on ICU for other APIs also, which Android doesn't really provide as a system C library:

For those who want to use the full Foundation beyond Essentials, that choice then bloats up Swift apps on Android, as we then ship the entire ICU data array to support that. We need to do some work to thin that out, but it will always add some weight.

1 Like

I recently did an experiment using OpenAPIGenerator in an ElementaryUI wasm web app.
Even in full Swift, a "hello world" can be stripped down to <2 MB (which may be workable in certain situations where Embbeded Swift is too limiting). Adding the OpenAPIRuntime (which uses Foundation) gets you a 19 MB binary (optimized, compressed) - which is essentially unusable on the web.

Now, I do not know how much of this is due to Foundation, or what effect depending on FoundationEssentials instead would have, but I want to echo the concern that binary sizes matter - this is a serious issue for Swift adoption.

I am very much in favor of this. The way I see this:

  • leaving it as it is -> every package that uses Foundation remains a toxic binary size killer
  • having entire package ecosystems make a major jump -> very disruptive and realistically not going to happen
  • "just do it" -> a few people might need to add an import Foundation in their code that they should have had anyway, but the ecosystem can start "healing"

We can't keep ignoring this issue forever.

6 Likes

That would be great Franz! I definitely think the community should consider biting the bullet.

Out of curiosity, when is the next Ecosystem Steering Group meeting?

Yeah, at least something like this would be nice for the Android platform.

2 Likes

@FranzBusch When is the next Ecosystem Steering Group meeting? Or what was the result of the discussion if it already happened?

Hey everyone,

We had discussed this issue over the past few weeks in the Ecosystem Steering Group together with the Foundation Workgroup to come up with guidance here. It took us a bit longer since there is some complexity with this adoption. Below is our guidance to the ecosystem.


The Ecosystem Steering Group thinks the adoption of FoundationEssentials is an important ecosystem-wide problem to tackle to achieve our goal of a flourishing Swift package ecosystem across all use cases. As discussed in this thread, FoundationEssentials provides a subset of the Foundation APIs which result in significantly reduced binary size. This is important for many use cases such as WASM or Android. However, to take advantage of this reduced binary size, every single package in the graph needs to ensure it only imports FoundationEssentials.

The current primary problem with adopting FoundationEssentials broadly is Swift's leaky import behavior. Changing from an import Foundation to an import FoundationEssentials is potentially source breaking as shown by this example:

// Module A
import Foundation

// Module B
import ModuleA

let s = "foo"
print(s.lengthOfBytes(using: .utf8))

Today, this builds because the import Foundation in Module A leaks public extensions on stdlib types into clients of Module A (i.e. Module B). In this case, func lengthOfBytes(using:) is a Foundation extension on String. If Module A imports FoundationEssentials instead, Module B fails to build.

Strictly speaking, changing from Foundation to FoundationEssentials would require a new major version. Due to this, we discussed three different options:

  1. Wait for the natural next major version of packages to change their imports.
  2. Recommend packages to change the import and release a new major version.
  3. Change the import and release a new minor while accepting the small potential for a source break.

We ruled out option 1 and 2 since new major versions are even more disruptive for the ecosystem as we have seen with the grpc-swift 2 major release. Therefore, we landed on option 3 since we believe the upsides heavily outweigh the potential downsides here. Importantly, this recommendation only applies to packages that only depend on Foundation APIs that are available in FoundationEssentials. Packages that use APIs that are only available in full Foundation are not expected to make a change here. Concretely, our recommendation for packages is:

  1. Where applicable, change Foundation imports to this:
#if canImport(FoundationEssentials)
import FoundationEssentials
#else
import Foundation
#endif
  1. Setup CI that tests your package across platforms since FoundationEssentials is not available everywhere and it is important to ensure that your package builds with either of the two imports.
  2. Release a new minor version of the package with release notes clearly indicating this expected potential source break.

In addition to the adoption of FoundationEssentials, we also discussed the broader problem of leaky imports with regards to the larger ecosystem. We believe that Swift's leaky import behavior is a bug and that most package authors are not even aware that changing an import can result in a source break. Hence, many popular packages in the ecosystem are not providing source stability guarantees for this. To combat this problem actively, we encourage package maintainers to adopt the two upcoming language features InternalImportsByDefault and MemberImportVisibility proactively. Similar to the adoption of FoundationEssentials, the adoption of these upcoming features can result in potential source breaks. However, since almost no package is guaranteeing this stability in the first place, we strongly believe it is better to adopt those features quickly throughout the ecosystem to remove the potential of any unintended source breaks.

We hope that this will allow the ecosystem to transition to FoundationEssentials relatively quickly to unlock the binary size savings and that the upcoming feature adoption will provide a healthier and stabler ecosystem.

23 Likes

Thanks for posting the update, now it's time to go around all repos and apply this systematically. At least we can move on doing these updates pretty quickly :+1:

3 Likes

Great news! Thank you for the update.

That's great! Thanks for listening and following up on this @FranzBusch and the rest of the workgroup

Is there further upcoming guidance on leaky imports for Swift as a whole? Specifically, is it expected that the upcoming InternalImportsByDefault/MemberImportVisibility features plug most of the holes, or are there more significant ones that need to be stopped too?

As Swift devs try to transition their code, it would help to have doc on the scope of these import problems and some kind of compiler flag that helps hunt them down, eg -list-all-resolved-imports or something that dumps every symbol's resolved module, so package authors can see exactly what is being imported from where. Perhaps this doc and flag already exists and I'm just unaware, if so, please just point me at it.

1 Like

MemberImportVisibility specifically carves out an exception for re-exports (either through Clang modules' export * or Swift's @_exported import), and Foundation re-exports FoundationEssentials, so I don't believe that alone would help matters? Are there also plans to have Foundation stop re-exporting FoundationEssentials and FoundationInternationalization in a future major version?

To plug all the holes, you would need something like Clang's "include what you use" to ensure that you import the precise module for every declaration you reference and remove any unused imports. My team has built something like that for Swift (internal-only, unfortunately) and it's absolutely riddled with special cases (protocol conformances are probably the worst). And Apple's recent SDKs have made this even harder. They've split frameworks like SwiftUI into SwiftUI and SwiftUICore, with many of the lower-level fundamental data types moved into the latter. So you'd think if someone uses one of those types, they could just import SwiftUICore, right? No—when the module is compiled, there is a fixed set of modules that are allowed to import it, and any others will fail. So if we write tooling that detects a reference to a declaration in SwiftUICore, we have to manually switch it over to SwiftUI instead. (And what if the "Core" module was extracted to allow multiple modules to re-export it? Then it's even trickier.)

So this is a Very Hard Problemā„¢ in general, but the situation would be helped if Foundation goes into this aware of the nastier cases and with the intent of avoiding them.

5 Likes