Weak linking of frameworks with greater deployment targets

I'm currently trying to figure out how to setup the deployment target of my SFSafeSymbols framework. While doing this, I came across a general Swift topic: Weak linking of frameworks. Before sharing my thoughts about this in general, I'm going to shortly explain the situation with my framework.

The issue with SFSafeSymbols

The framework provides both an enum making the SF Symbols in iOS 13 safely accessible and UIKit.Image and SwiftUI.Image convenience initializers leveraging this enum.

Although the SF Symbols are an iOS 13 feature, users may want to import the framework and then use it if iOS 13 is available. This is why I initially set the framework deployment target to iOS 10. So far, so good. Now, just recently, an initializer for SwiftUI.Image was added to my framework. As SwiftUI is only available for iOS 13+ (which must be ensured at compile time), I had to bump the deployment target to iOS 13. If an app e. g. targeting iOS 12+ wants to import the framework now, there's of course an error, as the framework requires iOS 13+ – that makes sense. However, it leaves me surprised to learn there's no way to "conditionally" import this framework.

Weak linking of frameworks

With Objective-C, it's possible to make frameworks optional, as described here. However, there doesn't seem a similar feature for Swift, at least I didn't find anything about it anywhere. A SO question about this has remained unanswered for over two years.

If my research was right and there's actually no way to do this in Swift, I propose to introduce a safe way to do this. I could imagine to always allow the import of a framework marked as Optional in Xcode:

import SFSafeSymbols // possible without availability check

However, when accessing types from this framework, it must be ensured that it was actually imported. It would be nice to infer this automatically based on an os version availability check, but there's probably a need for a dedicated check:

#if isAvailable(SFSafeSymbols)
_ = SFSafeSymbol.circle.toImage // Use framework
#endif

What do you think about such a feature? Are there other ways to achieve what I want (apart from splitting my framework into multiple sub frameworks)? Does such a feature fit Swift's design?

6 Likes

Experience showed that feature check based on presence of a framework is bad. You should always check based on system version.
The imported framework may be available on older system, but with different symbols or different behaviours.

That said, I agree that it should be possible to import module from the target system, but only use it inside availability check.
This is possible for single symbol, so why not support it for whole module.

And by the way, I did just try on macOS, and it mostly works as expected.

You must set you deployment target to the min OS version, and your SDK to the latest version. Then, fix all the compiler errors by adding @availability declaration and #availability check, make sure the SwiftUI framework is weak linked, and the resulting binary run flawlessly on 10.14 where it fallbacks to using my NSView based code, and on 10.15 where it uses my new SwiftUI code.

I said mostly work because I can't figure out how to tell Xcode to specify that SwiftUI should be weak linked (even adding SwiftUI as an optional framework explicitly in the link step does not work), so I had to edit the resulting binary to fix that.
But at this point, this is an Xcode issue, not a Swift issue.

Right. Like (modern) Objective-C, Swift infers which things to weak-link based on availability and deployment target. You should be able to do that just fine in your framework: always load it, but mark the new stuff as guarded with availability.

3 Likes

Looks like it does not work at the framework level. While it work fine with symbols, some tests with Xcode 11 shows that Xcode does not properly infer that the whole SwiftUI framework must be weak-link.

No, no. Weak-linking the whole framework does take some extra work (which I'd have to go look up, or which you can brute-force by adding -weak_framework MyFramework to all your clients' "Other Linker Flags"), but my point was that you shouldn't have to weak-link the whole framework for the situation you described:

  • Your deployment target should be iOS 12
  • You should build against the iOS 13 SDK
  • You should mark any uses of SwiftUI with @available.
  • If this fails on iOS 12, it means that some part of SwiftUI is missing its own availability annotations.
1 Like

Yeah, it doesn’t work with tests at all. You have to guard availability inside the test methods for it to be effective.

I'll be surprise it works by just declaring symbols as weak.
dyld try to unconditionally load any frameworks that is declared on the load commands at launch time and abort if one is missing.

I have a dynamic library containing the following code:


int test() {
	return 0;
}

I build it using clang -shared -o libFoo.dylib lib.c

I then create a simple app with no reference on libFoo symbols:


#include <stdio.h>

int main() {
	printf("hello world\n");
	return 0;
}

Built it: clang libFoo.dylib -o app app.c

Now, remove libFoo.dylib and try to launch the tool:

$ ./app
dyld: Library not loaded: libFoo.dylib
  Referenced from: ./app
  Reason: image not found
fish: './app' terminated by signal SIGABRT (Abort)

Which is the exact same error that I have trying to launch an app using SwiftUI on previous OS, dyld abort as it can't found SwiftUI framework image.

That should definitely work as long as the framework is indeed weak-linked. If it's not, then either the SwiftUI folks have missed some availability (more likely), or there's something in Xcode (or in your project specifically?) that's causing it to be strong-linked. It's supposed to Just Work™. Please file a bug at https://feedbackassistant.apple.com!

Feel free to have a look at it: FB6170959

1 Like

I also filed a similar issue FB6187287 where I could not get weak linking of SwiftUI to work when using UIViewControllerRepresentable. Ideally I want to allow compilation with both Xcode 10 and Xcode 11, with a deployment target of iOS 12.

My motivation here is to enable a development workflow with Xcode Previews that is backwards compatible to iOS 12.

@jrose So, if I get things correctly, I should be able to simply write import SwiftUI without problems, even if my app's deployment target is iOS 12, and only insert availability check for types of SwiftUI, right? And if that doesn't work that means there's an issue with SwiftUI, right?

Yep, that's the intent. There's also a chance there's a bug in the compiler itself if it uses a strong symbol reference instead of a weak one for a declaration that does have proper availability annotations.

Wouldn't that also mean that checks like those would be unnecessary:

#if os(OSX)
import AppKit
#else
import UIKit
#endif

and one could just write

import AppKit
import UIKit

?

That can't be, right? I'm totally confused... :neutral_face:

Weak-linking says "this must be present at link-time, but does not need to be present at run time". It's also a linker consideration; the compiler always needs things to be present (so it knows how to use them).

1 Like

Got it! Thanks for your patience ;)

This seems to be a bug still (Xcode 11.2.1) in Swift package manager projects at least. In my case I was not using SwiftUI but was importing CryptoKit and running on macOS 10.14.6 (CryptoKit is macOS 15+ only).

FB7471728

This is a significant bummer, needless to say.

@haikuty This bug should be fixed in swift-5.1-branch (and master, for that matter). Can you see if a Swift 5.1 development snapshot toolchain from swift.org works for you? You won't be able to submit to the App Store with that compiler, but it will at least confirm that the fix will work when it ships.

1 Like

Thank you. Unfortunately, the November 7, 2019 toolchain does not seem to have fixed the issue I'm seeing.

Seems like I should make a simple test package that demonstrates the issue and file a bug somewhere…

A reproducer would be very helpful. You may want to try a “trunk” (master branch) snapshot too—it’s possible that I’m mistaken about it being fixed in swift-5.1-branch.

Edit: Actually, I think you may have downloaded the 5.1.2 release at the top of the page. You should try the one under the “Swift 5.1 Development” heading, which is dated December 3, 2019 (or later).