Let's add #if available(Android ?, *)

Given a current very concrete issue where certain base-level functionality is only available from certain "development targets" onwards (Make Host NSUnimplemented for Android by ephemer · Pull Request #2439 · apple/swift-corelibs-foundation · GitHub), it'd be great to allow devs to specify the minimum Android platform level they intend to target with their apps.

The most natural dev-facing API for this would be to hook into the existing #if available API, i.e.:

#if available(Android 24, *)
doSomethingNice()
#else
NSUnimplemented()
#endif

I'm not 100% sure of the best way to achieve this, but it does need to be determined at compile time (to avoid non-existent symbols landing in the output binaries, which is impossible to work around on ELF). So it should probably end up as a compiler flag on swiftc to mirror iOS deployment target flags.

For example: swiftc -some -flags -mmin-android-sdk-version=21. That naming is based on how Android Studio calls it (minSdkVersion) but I'm not so much set on the name as much as I am on the functionality behind it.

Would be great to hear thoughts from any other Swift-Android devs on this! @ColemanCDA @compnerd @johnno1962 @Gonzalo_Larralde @janek

Also, because this pitch would be a change to swiftc, it'd be good to get some guidance and input from any interested core devs, @jrose?

7 Likes

Sounds great! I have been determining this at runtime while using JNI, but it should be built-in to the language.

Note that the syntax is:

if #available(Android 24, *) {
…
}

because this is checked at runtime, not at compile time. Is there a way to know the current SDK level at runtime?

Even then, that would not cover your use case, I believe, which is to avoid linkage entirely if your SDK target is too low. The way Darwin does it requires platform assistance and a bit of a compiler tweak, so our only option is to dlopen(), I believe — I do not think that a desirable end state would be that every Android developer compiles their Swift stack from scratch, even if it is the reality now, and any design choices that freeze us to that state of affairs look unpalatable to me.

The way s-c-f handles platform differences is by using the existing API surface wherever possible to return partial answers or appropriate errors if the relevant facilities are not available. In this case, the correct way to handle the exemplary patch would be:

  • check if the relevant API is there at runtime
  • invoke it if present
  • behave as NSHost does when an unresolvable address or host name is used if not

If the latter would fatalError, I’m open to figuring out experimental API that applies to Android only, but that’s basically a last resort.

Edited to add: Note if 'Android' is available as a platform identifier with the full @available/if #available featureset, it is perfectly fine to mark Host as @available(Android 24, *) to signal at compile time that you need to do a runtime check before use. It still has no bearing on linkage, or on the non-goal of different Swift runtimes for different Android SDK levels.

Hi @millenomi, thanks for your input on this!

If it's possible to "weakly link" and make a shim that only works in case the symbol exists, that'd be my preferred outcome as well of course. From what I understand thought that's only possible by using dlopen at runtime as you suggest, which might well be impossible on Android for this use-case:

  1. dlopening system libraries (anything from /system/lib(64)) is a fatal exception on Android 7.0+
  2. I actually have no idea which library we'd be dlopening here anyway (is getifaddrs provided by the C stdlib / runtime?). On Android > 5.0 Foundation "just works" without linking anything else at runtime.

So on platforms where the API doesn't exist, we could dlopen some system library (if you can tell me which one), but we already know that the symbol won't be there. And on platforms where we know it does exists, we can't use this method.

Is there no compile-time availability check at all, e.g. for Mac or for iOS? I still think that's the only way we'll be able to get around this, even if it has the unfortunate consequence of this API being unavailable unless Foundation is explicitly compiled for a higher Android platform level.

While if #available is usually a runtime check, perhaps we could have a compile-time-constant for the minimum supported target version too. For instance:

// the Android version (runtime check)
if #available(Android 24) {}

// the min version you want to support (known at compile-time)
if #available(AndroidTargetVersion 24) {}

Then conditionalize the availability of APIs as appropriate :

@available(AndroidTargetVersion 24, *)
func doThingsAvailableWhenTargetingVersion24()

@available(Android 24, *)
func doThingsAvailableWhenRunningOnVersion24()

So in essence when you don't have weak linking you can still use @available and #available like usual, but need to use the compile-time-constant for "min supported version" instead of the runtime one for "current running version". (This might be needed for Windows too).

You don't need dlopen. You can use just dlsym to check if a symbol is already loaded. It seemed to work in https://github.com/apple/swift-corelibs-foundation/pull/2228 for posix_spawn.

1 Like

Ok cool, this sounds like the way to go!

I am away for the rest of the week but can make a PR next week. The only thing I'm unsure about is the @available APIs - should I just leave these out for now?

Looks like we can do without the compile-time check after all, but the runtime check would still be great. Even better- it'd be ideal if we could set some @available attributes, e.g. on Host. @compnerd what do you think?

You will need to do a proper Swift Evolution pitch for that one, I think — we do not support platform tokens in @available/if #available other than the Darwin ones and swift right now.

(Also: note that @available is the compile-time feature, if #available the runtime check — I've seen them confused several times in this thread. They obviously interact, but they rely on the ability to know about version information at compile time for some uses of the former, and version information at runtime for all uses of the latter.)

We've had requests for this for Apple platforms too (SR-1778 and a handful of Radars). It's less important there because Apple does support adopting newer APIs while still targeting older OSs, but it still comes up during beta periods where someone wants to add features only available in (say) the iOS 13 beta, but still support building against the GM Xcode with the iOS 12 SDK from the same source code.

Since Android does not have weak linking, we can't support the runtime check if #available(Android 17, *) as a way to access newly-added APIs; it would at best be sugar for checking the OS version in ways you already can do. That in turn means there's no point in @available(Android 17, *) annotations either—while that restricts which clients can use something, the body of such a function is still compiled. We really do need something like what @Geordie_J started off with: a compile-time check with #if, if we want this at all.

What does this look like? It could be similar to the existing #available syntax, as shown in the original post:

#if sdk(Android 24, *)
// I am compiling against Android 24, something newer than Android 24, or a non-Android platform
#else
// I am compiling for Android, but the NDK I'm using is older than Android 24
#endif

#if sdk(iOS 13, tvOS 13, *)
// I am compiling against an iOS 13+ SDK, a tvOS 13+ SDK, or for a platform that is neither iOS nor tvOS
#else
// I am compiling for iOS or tvOS but with an older SDK
#endif

Or it could be similar to the os(…) check:

#if sdk(Android 24)
// I am compiling against Android 24 or newer
#else
// I am compiling against an old version of Android or another platform
#endif

#if sdk(iOS 13) || sdk(tvOS 13)
// I am compiling against an iOS 13+ SDK or a tvOS 13+ SDK
#else
// I am compiling against an old version of iOS or tvOS, or for another platform
#endif

We could even make it an error to check the SDK version if you're on the wrong platform.

#if sdk(Android 24)
// I am compiling against Android 24 or newer
#else
// I am compiling against an old version of Android
#endif

#if os(Android) && sdk(Android 24)
// I am compiling against Android 24 or newer
#else
// I am compiling against an old version of Android, or for another platform
#endif

I think given the Android (and Windows) development models, this is a worthwhile feature to add, and then we might as well add it for Apple SDKs too. It means the compiler has to know how to find an SDK's version, though, for each platform it supports.

cc also @compnerd, especially while we're debating the meaning of the word "SDK" in a Swift context in another thread.

Haha, this feature request came up in a conversation between @Geordie_J and I :slight_smile:

Although I had suggested #available to @Geordie_J originally, I am now leaning towards a variant of your second suggestion: #if platform(iOS 13). This neatly avoids the source compatibility issues and feels very much inline with the other checks:

#if platform(iOS 13) || platform(tvOS 14)
// I am compiling against iOS 13+ or tvOS 14+
#else
// I am compiling against iOS 12-, tvOS 13-, or another platform
#endif

If we are willing to add a bit more complexity, operators would be more clear I think (e.g. #if platform(iOS >= 13)) which would be inline with SE-0224.

It also conveniently sidesteps the confusions of what a SDK is. The concept of the platform level isn't tied to a "SDK" on any of the platforms - you can back deploy and forward deploy while building with a specific SDK version. It makes sense to tie this to the platform, so I think that the platform keyword is nicer here.

1 Like

Hmm. The reason I specifically went for "SDK" is to emphasize the compile-time nature of the thing. Because of Apple's backwards-deployment support, we (Apple) don't consider "iOS 12" and "iOS 13" to be separate platforms. More importantly, though, does "platform" check your SDK version or your min deployment target? (In the Java/Kotlin Android language, we're presumably talking about the "target SDK version" rather than either the "min SDK version" or the version of the NDK you downloaded.)

The feature is worthwhile and I look forward to a pitch!

I'll reiterate briefly that I wouldn't be able to accept a patch using it into s-c-f, though.

Edit: briefly: that's because I'm not going to split a build of Swift into 'Swift for Android pre-24' and 'Swift Android 24 and later'. I want a single s-c-f binary to behave reasonably on all SDK levels I and the community decide to support. #if os(Android) is fine, but per-SDK-level compile-time #ifs are not.

Yes, it would be the minimum deployment target, not the SDK version.

It's the minimum deployment target on Android because that's the only thing the NDK supports, but it's going to be the SDK version for Apple platforms because that's the interesting thing to check. Again, if Swift were interfacing with the Java runtime on Android, it'd be checking the target SDK version or compile SDK version, not the min SDK version, no?

(I'm getting this information from Device compatibility overview  |  Android Developers)

Hmm, am I messing up the terminology?

From that same page:
"The minSdkVersion attribute declares the minimum version with which your app is compatible"

That is the number that I was talking about.

Right, but that's only the number you want because Android doesn't have weak linking. The point is to pick the number that describes which APIs you're allowed to use at all. On Apple platforms, that's going to be much newer than the minimum deployment target.

I don't think you need weak linking at all with Java APIs though (or the JVM in general). Isn't everything weak linked there?

From a quick glance, it looks like the RequiresAPI annotation attached on declarations in the SDK would map well to @available, and the runtime check would map well to if #available. I also find this Xamarin document on API levels insightful, and it recommends checking availability like this (C# syntax) when using an APIs that may be unavailable:

if (Android.OS.Build.VERSION.SdkInt >= Android.OS.BuildVersionCodes.Lollipop)
{
    builder.SetCategory(Notification.CategoryEmail);
}

Can C code follow this model? From earlier in the thread it seems it was assumed not possible, but I'm confused because I though ELF supported weak linking. Perhaps the problem is is only about weak referencing a symbol when the library itself is not available? I don't really know.

I think the problem needs to be clarified. Perhaps showing currently working code in Java or C that work on Android and want to have a Swift equivalent would help focus the discussion.

@michelf, yes, ELF supports weak linking, but weak linking is not supported on all platforms. Ideally, we wouldn't design ourselves into a corner with a feature like this. That said, bionic's loader is also finicky, and has had some issues with loading certain constructs in the past (it may now be resolved though).

@jrose, I'm sorry, I don't follow the model you are describing (that is, what are the values in play and how does that work together). The model being suggested here is that you have a version V, which is the minimum version that you can deploy to. That matches the minimum version of the platform that supports the API. That is being used here to statically eliminate code paths. It like you want the dynamic behaviour where you fall back to runtime checking of the symbol? If that is correct, it sounds like what is needed is the same check, just in two different constructs:

#if platform(Android >= 21)
// statically known that the platform API level is 21 or newer
#else
if #available(Android 24, *) {
  // dynamically known to be 24 or newer
} else {
  // dynamically and statically known to be 21 or less.
}
#endif

(where Android, 21, 24, are replaced with iOS, 11, 13 respectively).

Or am I completely missing what you are suggesting?