How to disable implicit Foundation imports?

Although it contains a lot of useful functionality, Foundation is a frequent source of unexpected bugs and unexpectedly large binary size on non-Darwin and resource-restricted platforms (e.g. Android, WASI).

The latest issue I ran into is that Foundation gets implicitly imported – maybe by SwiftPM? – when using certain APIs. In particular, I just spent the last 90mins git bisecting three of our git repos to find out when our WASI bundle size had ballooned from 3MB to 26MB due to this issue. While I'm aware we could prevent this in future with CI hooks and automated tests on our bundle size, that is not the point.

The point is, how can we permanently disable the automatic import of Foundation, simply because we wrote "string".trimmingCharacters(in: .whitespaces) in a random Swift file somewhere? (That was the first culprit, the second was the String(format: "") initializer).

To be honest, I find this automatic behaviour pretty outrageous given the multi-platform state of Swift in 2022. I can understand that it was implemented with good intent, but wouldn't a much better default be to just throw a compiler error stating "import Foundation to use this API"?

12 Likes

+1 because i am running into a similar issue

Are you referring to the Xcode feature where code completion adds an import to the top of your file? If so, that’s no longer the case in the current Xcode 14 beta. Quoting the release notes:

Code completion no longer automatically imports modules. (78136559)

Share and Enjoy

Quinn “The Eskimo!” @ DTS @ Apple

2 Likes

Thanks for your reply @eskimo but that’s not it.

I’m talking about the compiler (or maybe SwiftPM) automatically importing Foundation silently at compile time without changing any of the source files (that’s why it took so long to track the issue down!)

That sounds disgusting, and I've never heard of the compiler doing anything like that before. Can you reproduce this on older compilers?

Do you have any files in the module which do import Foundation explicitly? Do you have any dependencies which depend on Foundation (and may be re-exporting the interface)? It'd be good to rule out where the API is getting picked up from.

edit: And if you're able to whittle this down into a minimal, reproducible test case, that would be excellent to see; as described, this behavior doesn't sound reasonable.

I don't think the compiler does this. What does happen is that one of your dependencies may be importing Foundation without @implementationOnly, which can potentially "leak" the import into your code, at least partially.

This suggestion is really interesting.

I’m not making it up though: I add myString.trimmingCharacters(in: .whitespace) to my code and Foundation gets linked at the cost of > 20 MB in binary size; I remove it again and Foundation doesn’t get linked.

There are absolutely no files in our modules that import Foundation directly (if I do then Foundation is linked even if I don’t use it). I can sift through my dependencies to see if one of them is adding @implementationOnly import Foundation if I get a chance today, but I kind of doubt it. I haven’t come across that syntax before – what is the purpose of doing that?

Edit: I just noticed you said without @implementationOnly so that’s even less likely. But I’m still curious about the syntax?

3 Likes

That seems like an awkward behaviour: does it reproduce if you compile with a single file using swiftc?

It prevents you from using any of the types or methods declared in the imported module in your own public interface: essentially, you're saying that users who import your module do not need to import that one to understand the interface.

Otherwise, it's assumed that they will need to. As an example, if you return ByteBuffer from one of your functions, users can only understand what that means if they also get the API definition of NIOCore (which defines ByteBuffer). As a result, the user sort of imports NIOCore when they import your module. However, if you use ByteBuffer but never expose it to the user, then you can import NIOCore as @implementationOnly and users no longer need the implicit NIOCore import.

1 Like

I only have some guesses for you here, so I'm sorry if these suggestions turn out to be useless or irrelevant. Could this be something to do with bridging String/NSString? At some point, you are being careful to avoid using Foundation API extensions or what have you, but however the link stage is happening Foundation is being supplied unconditionally and then being optimized out by something at the backend. Once you refer to bridged Foundation API reference, this can't be optimized down any more and your binary size increases.

1 Like

I appreciate you don't need Foundation at all in your case and hope you'll find the solution soon. Indeed, having a reproducible minimal example with a set of steps would be useful to resolve this.

However, assuming the cases when Foundation is needed I found it quite a big bundle size increase, if you use just a fraction of it. Is it possible to link Foundation statically and then strip it down to what's actually used? (I appreciate there might be other complications, like third party dependencies insisting on dynamic linking or somesuch, so let's assume for a moment there are no extra obstacles of such nature.)

Basically, everything in Wasm-land is statically linked, which is why the bundle size balloons so much. I don't know why it's impossible to strip the unused code though. There are numerous tools for the Wasm ecosystem that do that, but they don't seem to help that much here (to be clear: we are already using them).

I suspect this is exactly what's happening: that "something" is always requesting -lFoundation, but it's being optimized away unless it's specifically used. I guess what I'd like to find out is why Foundation is being supplied unconditionally like that – I just want to get rid of it and "suffer" the error if we use its APIs, like I would with any other module.

I think a smallest reproduce-able demo (in either standalone Swift files or a Swift package, with build command) could largely help us look into the problem. From your description we can see many tools involved in the process, including SwiftPM, driver, compiler and swift-autolink-extract (and maybe carton?). We’d better narrow our vision to find out the specific one that goes wrong.

2 Likes

How does it pass compilation? If I write:

"hello".trimmingCharacters(in: .capitalizedLetters)

I am getting a compilation error: "Value of type 'String' has no member 'trimmingCharacters'"
unless I import Foundation

I found this in the Carton GitHub repo and it looks very suspicious to me. I don't have the full picture as to how it works, but it seems to match the behaviour I'm seeing. In particular, this reply.

Assuming this is it, I'd like to thank everyone for their help so far! Really appreciate the support from the community about this. Sorry for the false alarm.

We haven't been using destination files for quite some time, see this note:

--destination option is no longer needed when using latest SwiftWasm toolchains.
This option no longer has any effect and will be removed in a future version of carton.
You should be able to link with Foundation/XCTest without passing this option.

In my understanding, Foundation is linked by the compiler, specifically the driver with swift-autolink-extract I guess? As the rest of the people mentioned upthread, we'd really appreciate a reproducible self-contained example code that allows us to reproduce and fix this. Thanks!

Just to double check is this happening when you build on Linux or Darwin?

Building on Darwin.

As mentioned here the reproducible test case is quite simple.

What I'm unsure about though is the linking behaviour on Darwin generally with SwiftPM.

$ swift build
$ otool -L .build/arm64-apple-macosx/debug/test

also shows me

/System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation (compatibility version 150.0.0, current version 1858.112.0)
/usr/lib/libobjc.A.dylib (compatibility version 1.0.0, current version 228.0.0)
/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1311.100.3)
/System/Library/Frameworks/Foundation.framework/Versions/C/Foundation (compatibility version 300.0.0, current version 1858.112.0)
/usr/lib/swift/libswiftCore.dylib (compatibility version 1.0.0, current version 5.6.0)
/usr/lib/swift/libswiftCoreFoundation.dylib (compatibility version 1.0.0, current version 14.0.0, weak)
/usr/lib/swift/libswiftCoreGraphics.dylib (compatibility version 1.0.0, current version 2.0.0, weak)
/usr/lib/swift/libswiftDarwin.dylib (compatibility version 1.0.0, current version 0.0.0, weak)
/usr/lib/swift/libswiftDispatch.dylib (compatibility version 1.0.0, current version 11.0.0, weak)
/usr/lib/swift/libswiftFoundation.dylib (compatibility version 1.0.0, current version 72.105.0)
/usr/lib/swift/libswiftIOKit.dylib (compatibility version 1.0.0, current version 1.0.0, weak)
/usr/lib/swift/libswiftObjectiveC.dylib (compatibility version 1.0.0, current version 3.0.0, weak)
/usr/lib/swift/libswiftXPC.dylib (compatibility version 1.0.0, current version 1.1.0, weak)

without import Foundation (or import *anything* for that matter). I'm not sure if that's expected behaviour.

Are you using macOS Ventura? :thinking: