Background and Motivation
.NET 6 heavily relies on the platform compatibility analyzer, linker and operating system detection on the mobile platforms. Currently there's a parity between the values returned by OperatingSystem.IsXXX() APIs, the UnsupportedOSPlatform("xxx") and attributes. The target framework moniker (TFM) also uses the same XXX syntax for platform suffix. All the OperatingSystem.IsXXX() APIs are mutually exclusive and at most one of them returns true on a given platform.
Unlike most TFMs the Mac Catalyst has an implicit relationship with the iOS TFM. Application targeting net6.0-maccatalyst may consume library assets that were built with net6.0-ios TFMs. This creates a disparity where this relationship is not captured by the OperatingSystem.IsIOS/IsMacCatalyst APIs and the unavailable Mac Catalyst APIs have to include explicit UnsupportedOSPlatform("maccatalyst") annotations even though they don't target net6.0-maccatalyst directly. Failure to do so would currently be silently ignored and a transitive library consumption will not produce Platform Compatibility Analyzer warnings.
Additionally, libraries targeting net6.0 and including iOS specific logic can easily fall into a trap of guarding the code with OperatingSystem.IsIOS() when the correct condition is OperatingSystem.IsIOS() || OperatingSystem.IsMacCatalyst().
Similarly, in native C / Objective-C / Swift code the platform availability guards implicitly imply the Mac Catalyst as a variant of iOS.
Platform guard example in C
Consider the following C code:
#include <stdio.h>
int main()
{
#if __is_target_os(ios)
printf("__is_target_os(ios): true\n");
#endif
if (__builtin_available(iOS 16, *)) {
printf("__builtin_available iOS 16\n");
}
if (__builtin_available(iOS 10, *)) {
printf("__builtin_available iOS 10\n");
}
if (__builtin_available(iOS 16, macCatalyst 11, *)) {
printf("__builtin_available macCatalyst\n");
}
}
It can be compiled for Mac Catalyst by running clang -target x86_64-apple-ios13.0-macabi avail.c -o avail and it produces the following output:
__is_target_os(ios): true
__builtin_available iOS 10
__builtin_available macCatalyst
The interpretation is that __is_target_os(...) treats Mac Catalyst as iOS variant. __builtin_available uses the iOS <version> value on Mac Catalyst unless an explict check is specified.
Proposed solutions
Proposal A
OperatingSystem.IsIOS() would return true on both Mac Catalyst and iOS. In majority of the cases that is what the developer wants to check since Mac Catalyst is supposed to be a superset of iOS. Current runtime checks that do OperatingSystem.IsIOS() || OperatingSystem.IsTvOS() || OperatingSystem.IsMacCatalyst() would be shortened to OperatingSystem.IsIOS() || OperatingSystem.IsTvOS().
- Keep
UnsupportedOSPlatform("XXX") consistent with OperatingSystem.IsXXX, both in the Platform Compatibility Analyzer and in linker. Thus specifying UnsupportedOSPlatform("ios") would imply that an API is also unsupported on Mac Catalyst. Duplicate UnsupportedOSPlatform("ios") and UnsupportedOSPlatform("maccatalyst") attributes would coalesce into one.
- For the rare case where you actually want to behave differently on iOS and MacCatalyst you would use a combination of the checks / attributes. An iOS-only API would be decorated with
[UnsupportedOSPlatform("maccatalyst")] and runtime check would be !OperatingSystem.IsMacCatalyst(). A MacCatalyst-only API would be decorated with [UnsupportedOSPlatform("ios")] and [SupportedOSPlatform("maccatalyst")] (or similar). Code block guarding specifically for iOS and not Mac Catalyst would use OperatingSystem.IsIOS() && !OperatingSystem.IsMacCatalyst().
Proposal B
-
Add OperatingSystem.IsIOSOrMacCatalyst() API with appropriate UnsupportedOSPlatformGuard attributes. This would simplify the checks in code while keeping the OperatingSystem.IsXXX APIs more consistent. There's a potential error for the caller to keep using IsIOS where IsIOSOrMacCatalyst should have been used. Casual observation suggests that most of the IsIOS() API usages in .NET runtime itself would be replaceable with this alternate API since they do IsIOS() || IsMacCatalyst() check anyway.
-
Teach the Platform Compatibility analyzer about the additional TFM relationship and enforce additional rules when targeting net6.0-ios and not targeting net6.0-maccatalyst in a library code (ie. adding explicit supported/unsupported MacCatalyst annotations where iOS annotations are present; additional checks for use of the IsIOS() API). [TODO]
Additional design considerations
- Should
OperatingSystem.IsXXX map the TFM fallbacks in general?
- Should there be a relation to how RIDs are structured too?
- Should
IsLinux() return true on Android?
Likely not; the API surface is significantly different, there's prior art with Flutter:
This value is false if the operating system is a specialized version of Linux that identifies itself by a different name, for example Android (see isAndroid).
/cc @terrajobst @jeffhandley for design decisions
Kudos to @filipnavara for the write-up.
Background and Motivation
.NET 6 heavily relies on the platform compatibility analyzer, linker and operating system detection on the mobile platforms. Currently there's a parity between the values returned by
OperatingSystem.IsXXX()APIs, theUnsupportedOSPlatform("xxx")and attributes. The target framework moniker (TFM) also uses the sameXXXsyntax for platform suffix. All theOperatingSystem.IsXXX()APIs are mutually exclusive and at most one of them returnstrueon a given platform.Unlike most TFMs the Mac Catalyst has an implicit relationship with the iOS TFM. Application targeting
net6.0-maccatalystmay consume library assets that were built withnet6.0-iosTFMs. This creates a disparity where this relationship is not captured by theOperatingSystem.IsIOS/IsMacCatalystAPIs and the unavailable Mac Catalyst APIs have to include explicitUnsupportedOSPlatform("maccatalyst")annotations even though they don't targetnet6.0-maccatalystdirectly. Failure to do so would currently be silently ignored and a transitive library consumption will not produce Platform Compatibility Analyzer warnings.Additionally, libraries targeting
net6.0and including iOS specific logic can easily fall into a trap of guarding the code withOperatingSystem.IsIOS()when the correct condition isOperatingSystem.IsIOS() || OperatingSystem.IsMacCatalyst().Similarly, in native C / Objective-C / Swift code the platform availability guards implicitly imply the Mac Catalyst as a variant of iOS.
Platform guard example in C
Consider the following C code:
It can be compiled for Mac Catalyst by running
clang -target x86_64-apple-ios13.0-macabi avail.c -o availand it produces the following output:The interpretation is that
__is_target_os(...)treats Mac Catalyst as iOS variant.__builtin_availableuses theiOS <version>value on Mac Catalyst unless an explict check is specified.Proposed solutions
Proposal A
OperatingSystem.IsIOS()would returntrueon both Mac Catalyst and iOS. In majority of the cases that is what the developer wants to check since Mac Catalyst is supposed to be a superset of iOS. Current runtime checks that doOperatingSystem.IsIOS() || OperatingSystem.IsTvOS() || OperatingSystem.IsMacCatalyst()would be shortened toOperatingSystem.IsIOS() || OperatingSystem.IsTvOS().UnsupportedOSPlatform("XXX")consistent withOperatingSystem.IsXXX, both in the Platform Compatibility Analyzer and in linker. Thus specifyingUnsupportedOSPlatform("ios")would imply that an API is also unsupported on Mac Catalyst. Duplicate UnsupportedOSPlatform("ios") and UnsupportedOSPlatform("maccatalyst") attributes would coalesce into one.[UnsupportedOSPlatform("maccatalyst")]and runtime check would be!OperatingSystem.IsMacCatalyst(). A MacCatalyst-only API would be decorated with[UnsupportedOSPlatform("ios")]and[SupportedOSPlatform("maccatalyst")](or similar). Code block guarding specifically for iOS and not Mac Catalyst would useOperatingSystem.IsIOS() && !OperatingSystem.IsMacCatalyst().Proposal B
Add
OperatingSystem.IsIOSOrMacCatalyst()API with appropriateUnsupportedOSPlatformGuardattributes. This would simplify the checks in code while keeping theOperatingSystem.IsXXXAPIs more consistent. There's a potential error for the caller to keep usingIsIOSwhereIsIOSOrMacCatalystshould have been used. Casual observation suggests that most of theIsIOS()API usages in .NET runtime itself would be replaceable with this alternate API since they doIsIOS() || IsMacCatalyst()check anyway.Teach the Platform Compatibility analyzer about the additional TFM relationship and enforce additional rules when targeting
net6.0-iosand not targetingnet6.0-maccatalystin a library code (ie. adding explicit supported/unsupported MacCatalyst annotations where iOS annotations are present; additional checks for use of theIsIOS()API). [TODO]Additional design considerations
OperatingSystem.IsXXXmap the TFM fallbacks in general?IsLinux()returntrueon Android?Likely not; the API surface is significantly different, there's prior art with Flutter:
/cc @terrajobst @jeffhandley for design decisions
Kudos to @filipnavara for the write-up.