Repository navigation
OperatingSystem.IsIOS API is problematic #53084
Description
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 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().- Keep
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. - 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 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
- Should
OperatingSystem.IsXXXmap the TFM fallbacks in general? - Should there be a relation to how RIDs are structured too?
- Should
IsLinux()returntrueon 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.
Activity
jkotas commentedon May 21, 2021
My vote would be for Proposal A since it matches the underlying platform model.
jeffhandley commentedon May 21, 2021
/cc @buyaa-n @adamsitnik
filipnavara commentedon Jun 16, 2021
@terrajobst Was there any traction on this?
terrajobst commentedon Jun 16, 2021
While I think that's true today this wasn't an explicit design goal. In fact, we said before that returning true for multiple operating systems should be allowed because it allows us to add "virtual" operating systems like browser where we want to be able to add more specific checks later, such as
IsBrowserChromium().I think modelling MacCatalyst as a variant of iOS makes sense because that's how Apple positioned it. So my preference would be proposal A but instead of teaching the platform compat analzer about this relationship I propose we explicitly declare the model using attributes:
This would mean that code that checks for
IsMacCatalyst()never has to check for iOS -- this check is now always implied. I'm not sure how this plays for version numbers. If iOS is supposed to map to MacCatalyst versions 1:1 we can add a property toSupportedOSPlatformGuardto indicate that:Please note that
SupportedOSPlatformGuardis a feature we added for .NET 6. It was meant to only decorate user-providedIsXxxxmethods. We'd extend the semantics when being applied to the nativeOperatingSystem.IsXxxAPIs to model an actual compat relationsip between platforms such that[UnsupportedOSPlatform("ios")]also implies that it's unsupported on MacCatalyst.@jeffhandley @buyaa-n, any thoughts?
filipnavara commentedon Jun 16, 2021
It is. There are multiple versions available on MacCatalyst (Darwin, macOS, iOS) but this API and Evironment.OSVersion operates on the iOS one (consistent with Apple and documentation).
jeffhandley commentedon Jun 26, 2021
This is still on my radar to try to field for Preview 7. I expect we'll be able to look into it middle of next week.
buyaa-n commentedon Jul 8, 2021
I do not completely understand their relation, is
iOSis the parent ofMacCatalys?iOSsupportsMacCatalystbutMacCatalystnot supportiOS?Interesting solution, makes sense for above relation, but it is not directly comply with
Proposal Aexpectation:OperatingSystem.IsIOS() || OperatingSystem.IsTvOS() || OperatingSystem.IsMacCatalyst()would be shortened toOperatingSystem.IsTvOS() || OperatingSystem.IsMacCatalyst()notOperatingSystem.IsIOS() || OperatingSystem.IsTvOS().14 remaining items
terrajobst commentedon Jul 8, 2021
That seems fine because if you target .NET 5 you can't see
OperatingSystem.IsMacCatalyst()anyway.jeffhandley commentedon Jul 11, 2021
I've been exploring options for this and I have an updated proposal. I found that the Platform Compatibility Analyzer does not currently respect
SupportedOSPlatformGuardattributes on theSystem.OperatingSystemmethods; it identifies this type specifically and infers the method's platform guard. I also found flaws in the proposal of adding[SupportedOSPlatformGuard("ios")]toIsMacCatalyst, but the updated proposal is similar and addresses the flaws.Acceptance Criteria
To consider the proposal of using
SupportedOSPlatformGuardattributes on theOperatingSystemmethods to achieve a relationship of MacCatalyst as a superset of IOS, we will utilize the following acceptance criteria.OperatingSystem.IsIOS()matches both IOS and MacCatalystOperatingSystem.IsIOS() && !OperatingSystem.IsMacCatalyst()matches only IOS but not MacCatalystOperatingSystem.IsMacCatalyst()matches only MacCatalyst but not IOS[SupportedOSPlatformGuard]attributes is consistent with existing behavior[UnsupportedOSPlatform]and[UnsupportedOSPlatformGuard]attributes must continue to work as expected[UnsupportedOSPlatform]attributes must respect the platform relationship definedThere are a few other design considerations as well, although they are not required goals.
OperatingSystemmethods should be treated like any other method with[SupportedOSPlatformGuard]attributesProposal Summary
OperatingSystem.IsIOSto returntruewhen the platform is "ios" OR "maccatalyst"[SupportedOSPlatformGuard]attributes onSystem.OperatingSystemOperatingSystem.IsIOSwith[SupportedOSPlatformGuard("ios")]and [SupportedOSPlatformGuard("maccatalyst")]OperatingSystem.Is{Platform}methods with[SupportedOSPlatformGuard("{Platform}")]attributes[SupportedOSPlatformGuard]attributes on that platform'sIs{Platform}methodIsOsxmember onOperatingSystem(either private or[Obsolete])Illustration of Acceptance Criteria
The following code simulates this behavior by using custom guard properties with
[SupportedOSPlatformGuard]attributes and showing where[SupportedOSPlatform]and[UnsupportedOSPlatform]attributes would be inferred by the updated logic.This approach and design meets all of the acceptance criteria defined above.
IsIOS())[SupportedOSPlatformGuard]attributes is consistent with existing behavior[UnsupportedOSPlatform]and[UnsupportedOSPlatformGuard]attributes must continue to work as expectedSupportedOSPlatformGuardannotations during initializationThe other design considerations can all be achieved as well.
OperatingSystemmethods should be treated like any other method withSupportedOSPlatformGuardattributesOperatingSystemimplementations to augment/override built-in behaviorIsOSX()method is added toOperatingSystem(private or[Obsolete])The lengthy discovery process that led to this proposal
SupportedOSPlatformGuard Attribute Behavior
I needed a refresher for how the guard attributes are applied. Let's start a basic single-platform scenario where the guard attribute is useful.
Single Platform without the Guard Attribute
Abstracting away the OperatingSystem Check
Adding a Second Platform
In this case,
DoWorkis supported on both "android" and "ios" andIsSupportedasserts that the current platform is either of those through an OR condition.Differentiating Between Platforms
Making the scenario more complex, we can expand this out such that there's one implementation for the Apple platforms and a different implementation for Android.
To accomplish this, we will keep
IsSupportedas the higher-level check, but use additional guard methods to identify the Apple platforms.In this scenario:
IsApplePlatformistruefor "ios" OR "maccatalyst"IsSupportedistrueif the current platform is "android" OR "ios" OR "maccatalyst"This scenario illustrates why the guards must be treated with OR logic.
MacCatalyst or IOS
Here is the behavior we desire to allow a single check for either MacCatalyst or IOS:
In this table,
IsIOS()should returntruefor either "ios" or "maccatalyst", butIsMacCatalyst()should only returntrueon "maccatalyst". In earlier comments on this issue, we were proposing the following:However, this is the inverse of what was actually intended. If we want
IsIOS()to returntruefor either "ios" or "maccatalyst", then our annotations should instead be:With those annotations,
IsIOS()would match the behavior of the multi-platform guard methods illustrated above, returningtruefor either of the platforms. TheIsMacCatalyst()method is annotated to only returntrueif the platform is "maccatalyst" (but not "ios").Current Guard Behavior
The current guard behavior for this configuration can be observed by using custom guard methods with the illustrated annotations.
Within the
if (IsIOS)guard, the code is understood to be reachable on either "ios" OR "maccatalyst". That implies that all code within the block needs to be supported on both of those platforms, and that's not the case.Within the
if (IsMacCatalyst)guard, the code is understood to be reachable only on "maccatalyst" (and not traditional "ios")--which is the accurate interpretation of asserting that we are on the superset platform. As is, that implies code within the block must be supported on "maccatalyst", butSupportedOnIOSis not.The proposal above included changing the behavior of the second point such that when
IsMacCatalyst()returnstrue, references to APIs supported only on "ios" should succeed, with the analyzer recognizing a relationship between these platforms.Recognizing the Platform Relationship
We asserted above that we could glean the relationship between "maccatalyst" and "ios" by finding an annotation of
[SupportedOSPlatformGuard("ios")]onIsMacCatalyst(). As is illustrated here though, that annotation would be backwards.Two alternatives to illustrate based on the proposal are:
SupportedOSPlatformGuardannotationsIsIOS()insteadInverting the Annotations
If we invert the annotations in the sample, we would achieve the correct results within the
if (IOS)block, but we still don't achieve the correct results within theif (IsMacCatalyst)block.On the surface, this result seems close enough to the desired effect that the analyzer could be updated to understand the relationship between "maccatalyst" and "ios" and suppress the two incorrect warnings. The logic would be:
OperatingSystemmethod, check that method forSupportedOSPlatformGuardannotationsThis would have the effect of applying AND logic between the platforms, which would be equivalent to changing the code above to:
Everything in this example is correct, but there is a flaw in this approach when we look back at the early examples of how guards are interpreted with OR logic: this behavior would have to be special-cased to apply only for methods on the
OperatingSystemclass and behave differently elsewhere. This violates the acceptance criteria of the newSupportedOSPlatformGuardattributes being handled consistently with existing behavior.Glean the relationship from the annotations on
IsIOS()insteadLet's look back at the previous example to see how we could check the
IsIOS()method for annotations about additional platforms.Given the numbered scenarios, the behavior would need to be:
OperatingSystem.IsMacCatalyst()method.SupportedOSPlatformGuardattributes on the method.OperatingSystem.IsIOS()method.SupportedOSPlatformGuardattributes on the method.OperatingSystem.IsIOS()method.SupportedOSPlatformGuardattributes on the method.Generalizing the Behavior
In order for this behavior to work, we would introduce behavior into the analyzer such that:
OperatingSystem.Is{Platform}()methodSupportedOSPlatformGuardattributes for other platformsAn alternate approach to the implementation could be done at the time of identifying an API's supported/unsupported platforms.
OperatingSystem.Is{Platform}()methodSupportedOSPlatformGuardattributesWe can simulate this approach by explicitly applying the attributes that would be inferred. This example adds scenarios for the other acceptance criteria as well, including the scenario of an API being marked as supported on IOS but not MacCatalyst.
jeffhandley commentedon Jul 13, 2021
Here's the plan to get this implemented:
OperatingSystem.IsIOSreturn true when on MacCatalyst, and apply the minimal guard attributes (.NET 6, Preview 7)4. [ ] Optionally make another change to the runtime to model the macOS/OSX alias through the guard attributes (this would likely be during .NET 7)An issue filed for 7.0buyaa-n commentedon Jul 20, 2021
Modified Illustration of Acceptance Criteria
I have modified the proposal with exact use case scenarios with have agreed to add within 6.0 (like excluding macOS/OSX scenario we moved to 7.0).
buyaa-n commentedon Jul 27, 2021
@jeffhandley as part of testing step 3 found that we left out the guard attributes scenario, most likely the when API is
[UnsupportedOSPlatformGuard("ios")]it should also imply[UnsupportedOSPlatformGuard("macCatalyst")], but just want to make sure (same for[SupportedOSPlatformGuard("ios")]and[SupportedOSPlatformGuard("macCatalyst")]):jeffhandley commentedon Jul 27, 2021
That is correct. However, you should be able to override that behavior if needed.
jeffhandley commentedon Jul 27, 2021
Sorry -- I misread -- you were asking about the guard attributes... Re-reviewing...
terrajobst commentedon Jul 27, 2021
Do guard methods provide the same stacking behavior as the supported attributes do? If so, then @jeffhandley's logic should apply to guard methods too.
In fact, they probably should be mirrors because the guard methods are supposed to be used before calling the annotated APIs, so I'd say it would be odd if guard attributes can't be stacked like this.
jeffhandley commentedon Jul 28, 2021
Here's a representation of what I think can be inferred for the guard attributes:
buyaa-n commentedon Jul 29, 2021
Thanks, @jeffhandley, all looked right initially, but now I think the section covering the
SupportedOSPlatformGuardpart needs update because multipleSupportedOSPlatformGuardattributes produce OR logicjeffhandley commentedon Jul 29, 2021
Makes sense, @buyaa-n; thanks. I forgot that the guards produce OR logic.
buyaa-n commentedon Aug 11, 2021
For step 4 filed an issue, this one can be closed now
filipnavara commentedon Aug 11, 2021
Thanks everyone!