-
Notifications
You must be signed in to change notification settings - Fork 4.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
OperatingSystem.IsIOS API is problematic #53084
Comments
I couldn't figure out the best area label to add to this issue. If you have write-permissions please help me learn by adding exactly one area label. |
Tagging subscribers to this area: @tannergooding Issue DetailsBackground 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 Unlike most TFMs the Mac Catalyst has an implicit relationship with the iOS TFM. Application targeting Additionally, libraries targeting 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 CConsider 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
The interpretation is that Proposed solutionsProposal A
Proposal B
Additional design considerations
/cc @terrajobst @jeffhandley for design decisions Kudos to @filipnavara for the write-up.
|
My vote would be for Proposal A since it matches the underlying platform model. |
/cc @buyaa-n @adamsitnik |
@terrajobst Was there any traction on this? |
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 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: namespace System
{
public partial class OperatingSystem
{
[SupportedOSPlatformGuard("ios")]
public static bool IsMacCatalyst();
}
} This would mean that code that checks for namespace System
{
public partial class OperatingSystem
{
[SupportedOSPlatformGuard("ios", MapVersions = true)]
public static bool IsMacCatalystVersionAtLeast(int major, int minor = 0, int build = 0);
}
} Please note that @jeffhandley @buyaa-n, any thoughts? |
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). |
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. |
I do not completely understand their relation, is
Interesting solution, makes sense for above relation, but it is not directly comply with |
MacCatalyst works kinda like a superset of iOS API (with very rare exception where some APIs are missing). There's TFM fallback that allows managed iOS code to be consumed by application targeting MacCatalyst. Effectively you can think of MacCatalyst as child of iOS even if it doesn't quite reflect the real heritage.
Yeah, we want to check for |
Here's my understanding of the desired experience:
@filipnavara does that match your expectations? Note that the Android/Linux behavior has already been changed in #53034. |
Yes |
I guess the same apply to the attributes: |
@buyaa-n Correct. Note that there could be some APIs (none in dotnet/runtime AFAIK; few in Xamarin bindings) which exist on iOS and don't exist in Mac Catalyst. In that case I would expect an annotation like this to work:
This is mostly covering frameworks that don't make sense outside of the phone environment (CoreTelephony, CoreNFC, ARKit, VisionKit, HealthKit, HealthKitUI, AddressBookUI, CarPlay, ...). It's feasible that some of them may appear at later time if Apple decides to do so. |
@buyaa-n Would this have to happen inside the analyzer itself, or can you think of a way for this to work such that the analyzer doesn't need special handling of these platforms? |
Yes its need analyzer update, same for removing coverage by:
No, nothing |
Have you looked at my comment? Ideally we'd make this data driven and don't hard code compat rules. |
I agree on making it data-driven so that we don't have to hard-code the compat rules into the analyzer. But the guard attributes as illustrated wouldn't cover the cases of: [SupportedOSPlatform("ios")]
[UnsupportedOSPlatform("maccatalyst")] or [SupportedOSPlatform("maccatalyst")]
[UnsupportedOSPlatform("ios")] We could potentially have the SDK automatically apply |
Yes, that makes sense, but that doesn't look like could cover all scenarios, especially for handling the relation between called API attributes and the call-site attributes |
I'm not entirely following. What I'm proposing is that we use the guard attributes on the Does this make sense? |
So the trick is how to add the support to the analyzer to understand the platform relationships. I'd prefer for that to be data-driven and not have those specific relationships coded into the analyzer. Are you proposing the analyzer would read the guard attributes in the |
Agreed, that's inescapable IMHO.
Agreed
|
Gotcha; thanks, @terrajobst. OK, I'm doing a proof of concept of the guard attributes being applied to the methods to make sure the analyzer will respect those as implemented now. I'll follow up once I have those findings and we can explore what it would look like for the analyzer to consume that data (during initialization) to understand platform aliases/relationships. One question though... Should these relationships be defined within the runtime, or within the tooling? In other words, if the analyzer gets the data from the |
That seems fine because if you target .NET 5 you can't see |
I've been exploring options for this and I have an updated proposal. I found that the Platform Compatibility Analyzer does not currently respect Acceptance CriteriaTo consider the proposal of using
There are a few other design considerations as well, although they are not required goals.
Proposal Summary
Illustration of Acceptance CriteriaThe following code simulates this behavior by using custom guard properties with // Guard methods that simulate the OperatingSystem members
[SupportedOSPlatformGuard("ios")]
[SupportedOSPlatformGuard("maccatalyst")]
bool IsIOS => OperatingSystem.IsIOS() || OperatingSystem.IsMacCatalyst();
[SupportedOSPlatformGuard("maccatalyst")]
bool IsMacCatalyst => OperatingSystem.IsMacCatalyst();
// This would be a new member added to OperatingSystem, but it could be
// private or Obsolete as an error with EditorBrowsable: Never
[SupportedOSPlatformGuard("macos")]
[SupportedOSPlatformGuard("osx")]
private bool IsOsx() => OperatingSystem.IsMacOS();
[SupportedOSPlatformGuard("macos")]
[SupportedOSPlatformGuard("osx")]
bool IsMacOs => OperatingSystem.IsMacOS();
// APIs for the scenarios
[SupportedOSPlatform("ios")]
// The following attribute would not exist in the code
// but it would be inferred by the analyzer.
[SupportedOSPlatform("maccatalyst")]
void SupportedOnIOS() { }
[SupportedOSPlatform("ios")]
// The following attribute would exist in code, but
// would be canceled out by the analyzer because
// "maccatalyst" would be inferred as supported,
// and then the unsupported attribute cancels it out.
// [UnsupportedOSPlatform("maccatalyst")]
void SupportedOnIOS_ButNotMacCatalyst() { }
[SupportedOSPlatform("maccatalyst")]
void SupportedOnMacCatalyst() { }
[SupportedOSPlatform("ios")]
[SupportedOSPlatform("maccatalyst")]
void SupportedOnIOSOrMacCatalyst() { }
[UnsupportedOSPlatform("ios")]
// The following attribute would not exist in the code
// but it would be inferred by the analyzer.
[UnsupportedOSPlatform("maccatalyst")]
void UnsupportedOnIOS() { }
[UnsupportedOSPlatform("maccatalyst")]
void UnsupportedOnMacCatalyst() { }
[SupportedOSPlatform("macos")]
// The following attribute would not exist in the code
// but it would be inferred by the analyzer.
[SupportedOSPlatform("osx")]
void SupportedOnMacOS() { }
[SupportedOSPlatform("osx")]
// The following attribute would not exist in the code
// but it would be inferred by the analyzer.
[SupportedOSPlatform("macos")]
void SupportedOnOsx() { }
// Test cases
void Main()
{
// This block is reachable on both IOS and MacCatalyst but not macOS/OSX
if (IsIOS)
{
SupportedOnIOSOrMacCatalyst(); // Scenario 1a - No warning
SupportedOnMacCatalyst(); // Scenario 1b - Warning
SupportedOnIOS(); // Scenario 1c - No warning
SupportedOnIOS_ButNotMacCatalyst(); // Scenario 1d - Warning
UnsupportedOnIOS(); // Scenario 1e - Warning
UnsupportedOnMacCatalyst(); // Scenario 1f - Warning
SupportedOnMacOS(); // Scenario 1g - Warning
SupportedOnOsx(); // Scenario 1h - Warning
}
// This block is reachable on MacCatalyst but not IOS or macOS/OSX
if (IsMacCatalyst)
{
SupportedOnIOSOrMacCatalyst(); // Scenario 2a - No warning
SupportedOnMacCatalyst(); // Scenario 2b - No warning
SupportedOnIOS(); // Scenario 2c - No warning
SupportedOnIOS_ButNotMacCatalyst(); // Scenario 2d - Warning
UnsupportedOnIOS(); // Scenario 2e - Warning
UnsupportedOnMacCatalyst(); // Scenario 2f - Warning
SupportedOnMacOS(); // Scenario 2g - Warning
SupportedOnOsx(); // Scenario 2h - Warning
}
// This code block is reachable on IOS but not MacCatalyst or macOS/OSX
if (IsIOS && !IsMacCatalyst)
{
SupportedOnIOSOrMacCatalyst(); // Scenario 3a - No warning
SupportedOnMacCatalyst(); // Scenario 3b - Warning
SupportedOnIOS(); // Scenario 3c - No warning
SupportedOnIOS_ButNotMacCatalyst(); // Scenario 3d - No warning
UnsupportedOnIOS(); // Scenario 3e - Warning
UnsupportedOnMacCatalyst(); // Scenario 3f - No warning
SupportedOnMacOS(); // Scenario 3g - Warning
SupportedOnOsx(); // Scenario 3h - Warning
}
// This code block is reachable on macOS/OSX but not IOS or MacCatalyst
if (IsMacOs)
{
SupportedOnIOSOrMacCatalyst(); // Scenario 4a - Warning
SupportedOnMacCatalyst(); // Scenario 4b - Warning
SupportedOnIOS(); // Scenario 4c - Warning
SupportedOnIOS_ButNotMacCatalyst(); // Scenario 4d - Warning
UnsupportedOnIOS(); // Scenario 4e - No warning
UnsupportedOnMacCatalyst(); // Scenario 4f - No warning
SupportedOnMacOS(); // Scenario 4g - No warning
SupportedOnOsx(); // Scenario 4h - No warning
}
// This code block is reachable on IOS, MacCatalyst, and macOS/OSX
if (IsMacOs || IsIOS)
{
SupportedOnIOSOrMacCatalyst(); // Scenario 5a - Warning
SupportedOnMacCatalyst(); // Scenario 5b - Warning
SupportedOnIOS(); // Scenario 5c - Warning
SupportedOnIOS_ButNotMacCatalyst(); // Scenario 5d - Warning
UnsupportedOnIOS(); // Scenario 5e - Warning
UnsupportedOnMacCatalyst(); // Scenario 5f - Warning
SupportedOnMacOS(); // Scenario 5g - Warning
SupportedOnOsx(); // Scenario 5h - Warning
}
} This approach and design meets all of the acceptance criteria defined above.
The other design considerations can all be achieved as well.
The lengthy discovery process that led to this proposalSupportedOSPlatformGuard Attribute BehaviorI 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 [SupportedOSPlatform("android")]
void DoWork() { }
void Main()
{
if (OperatingSystem.IsAndroid())
DoWork();
} Abstracting away the OperatingSystem Check [SupportedOSPlatformGuard("android")]
bool IsSupported => OperatingSystem.IsAndroid();
void Main()
{
if (IsSupported)
DoWork();
} Adding a Second PlatformIn this case, [SupportedOSPlatform("android")]
[SupportedOSPlatform("ios")]
void DoWork() { }
[SupportedOSPlatformGuard("android")]
[SupportedOSPlatformGuard("ios")]
bool IsSupported => OperatingSystem.IsAndroid() || OperatingSystem.IsIOS();
void Main()
{
if (IsSupported)
DoWork();
} Differentiating Between PlatformsMaking 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 [SupportedOSPlatform("android")]
void SupportedOnAndroid() { }
[SupportedOSPlatform("ios")]
[SupportedOSPlatform("maccatalyst")]
void SupportedOnApplePlatforms() { }
[SupportedOSPlatformGuard("ios")]
[SupportedOSPlatformGuard("maccatalyst")]
bool IsApplePlatform => OperatingSystem.IsIOS() ||
OperatingSystem.IsMacCatalyst();
[SupportedOSPlatformGuard("android")]
[SupportedOSPlatformGuard("ios")]
[SupportedOSPlatformGuard("maccatalyst")]
bool IsSupported => IsApplePlatform ||
OperatingSystem.IsAndroid();
void Main()
{
if (IsSupported)
{
if (IsApplePlatform)
SupportedOnApplePlatforms();
else if (OperatingSystem.IsAndroid())
SupportedOnAndroid();
}
} In this scenario:
This scenario illustrates why the guards must be treated with OR logic. MacCatalyst or IOSHere is the behavior we desire to allow a single check for either MacCatalyst or IOS:
In this table, [SupportedOSPlatformGuard("ios")]
public static bool IsIOS();
[SupportedOSPlatformGuard("maccatalyst")]
[SupportedOSPlatformGuard("ios")]
public static bool IsMacCatalyst(); However, this is the inverse of what was actually intended. If we want [SupportedOSPlatformGuard("ios")]
[SupportedOSPlatformGuard("maccatalyst")]
public static bool IsIOS(); // returns true on either ios or maccatalyst
[SupportedOSPlatformGuard("maccatalyst")]
public static bool IsMacCatalyst(); // returns true only on maccatalyst With those annotations, Current Guard BehaviorThe current guard behavior for this configuration can be observed by using custom guard methods with the illustrated annotations. [SupportedOSPlatform("ios")]
[SupportedOSPlatform("maccatalyst")]
void SupportedOnIOSOrMacCatalyst() { }
[SupportedOSPlatform("ios")]
void SupportedOnIOS() { }
[SupportedOSPlatform("maccatalyst")]
void SupportedOnMacCatalyst() { }
[SupportedOSPlatformGuard("ios")]
[SupportedOSPlatformGuard("maccatalyst")]
bool IsIOS => OperatingSystem.IsIOS() ||
OperatingSystem.IsMacCatalyst();
[SupportedOSPlatformGuard("maccatalyst")]
bool IsMacCatalyst => OperatingSystem.IsMacCatalyst();
void Main()
{
if (IsIOS)
{
// Correct: No warning
SupportedOnIOSOrMacCatalyst();
// Correct: Reachable on 'ios', only supported on 'maccatalyst'
SupportedOnMacCatalyst();
// INCORRECT: Reachable on 'maccatalyst', only supported on 'ios'
SupportedOnIOS();
}
if (IsMacCatalyst)
{
// Correct: No warning
SupportedOnIOSOrMacCatalyst();
// Correct: No warning
SupportedOnMacCatalyst();
// INCORRECT: Reachable on 'maccatalyst', only supported on 'ios'
SupportedOnIOS();
}
}
The proposal above included changing the behavior of the second point such that when Recognizing the Platform RelationshipWe asserted above that we could glean the relationship between "maccatalyst" and "ios" by finding an annotation of Two alternatives to illustrate based on the proposal are:
Inverting the AnnotationsIf we invert the annotations in the sample, we would achieve the correct results within the [SupportedOSPlatformGuard("ios")]
bool IsIOS => OperatingSystem.IsIOS() ||
OperatingSystem.IsMacCatalyst();
[SupportedOSPlatformGuard("maccatalyst")]
[SupportedOSPlatformGuard("ios")]
bool IsMacCatalyst => OperatingSystem.IsMacCatalyst();
void Main()
{
if (IsIOS)
{
// Correct: No warning
SupportedOnIOSOrMacCatalyst();
// Correct: Reachable on 'ios', only supported on 'maccatalyst'
SupportedOnMacCatalyst();
// Correct: No warning
SupportedOnIOS();
}
if (IsMacCatalyst)
{
// Correct: No warning
SupportedOnIOSOrMacCatalyst();
// INCORRECT: Reachable on 'ios', only supported on 'maccatalyst'
SupportedOnMacCatalyst();
// INCORRECT: Reachable on 'maccatalyst', only supported on 'ios'
SupportedOnIOS();
}
} 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:
This would have the effect of applying AND logic between the platforms, which would be equivalent to changing the code above to: [SupportedOSPlatformGuard("ios")]
bool IsIOS => OperatingSystem.IsIOS() ||
OperatingSystem.IsMacCatalyst();
[SupportedOSPlatformGuard("maccatalyst")]
// [SupportedOSPlatformGuard("ios")] this would exist
bool IsMacCatalyst => OperatingSystem.IsMacCatalyst();
void Main()
{
if (IsIOS)
{
// Correct: No warning
SupportedOnIOSOrMacCatalyst();
// Correct: Reachable on 'ios', only supported on 'maccatalyst'
SupportedOnMacCatalyst();
// Correct: No warning
SupportedOnIOS();
}
// Simulating the _AND_ logic that would be inferred from the
// extra "ios" guard annotation on IsMacCatalyst
if (IsMacCatalyst && IsIOS)
{
// Correct: No warnings
SupportedOnIOSOrMacCatalyst();
// Correct: No warnings
SupportedOnMacCatalyst();
// Correct: No warnings
SupportedOnIOS();
}
} 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 Glean the relationship from the annotations on
|
Scenario | Behavior |
---|---|
1 | 1. Guarded by a property with multiple platforms indicated. |
2. Reachable on ios and maccatalyst, supported on ios and maccatalyst. | |
3. No warning is produced. This is the expected result. | |
2 | 1. Guarded by a property with multiple platforms indicated. |
2. Reachable on ios and maccatalyst, supported on maccatalyst. | |
3. Iterate over the supported platforms (in this case, maccatalyst). | |
4. Look for a corresponding OperatingSystem.IsMacCatalyst() method. |
|
5. Collect the SupportedOSPlatformGuard attributes on the method. |
|
6. Look for all reachable platforms in the list (in this case, ios). | |
7. No match is found. | |
8. A warning is produced. This is the expected result. | |
3 | 1. Guarded by a property with multiple platforms indicated. |
2. Reachable on ios and maccatalyst, supported on ios. | |
3. Iterate over the supported platforms (in this case, ios). | |
4. Look for a corresponding OperatingSystem.IsIOS() method. |
|
5. Collect the SupportedOSPlatformGuard attributes on the method. |
|
6. Look for all reachable platforms in the list (in this case, maccatalyst). | |
7. A match is found for maccatalyst, so the warning is eliminated. | |
8. No warning is produced. This is the expected result. | |
4 | 1. Reachable on maccatalyst, supported on maccatalyst and ios. |
2. All reachable platforms are supported. | |
3. No warning is produced. This is the expected result. | |
5 | 1. Reachable on maccatalyst, supported on maccatalyst. |
2. All reachable platforms are supported. | |
3. No warning is produced. This is the expected result. | |
6 | 1. Reachable on maccatalyst, supported on ios. |
2. Iterate over the supported platforms (in this case, ios) | |
3. Look for a corresponding OperatingSystem.IsIOS() method. |
|
4. Collect the SupportedOSPlatformGuard attributes on the method. |
|
5. Look for all reachable platforms in the list (in this case, maccatalyst). | |
6. A match is found for maccatalyst, so the warning is eliminated. | |
7. No warning is produced. This is the expected result. |
Generalizing the Behavior
In order for this behavior to work, we would introduce behavior into the analyzer such that:
- Any time an API call is reachable on an unsupported platform
- Iterate over all platforms identified as reachable but not supported
- Check for a corresponding
OperatingSystem.Is{Platform}()
method - Inspect the method's
SupportedOSPlatformGuard
attributes for other platforms - Eliminate warnings for platforms included in the guard attributes
An alternate approach to the implementation could be done at the time of identifying an API's supported/unsupported platforms.
- For each platform marked as supported/unsupported
- Check for a corresponding
OperatingSystem.Is{Platform}()
method - Inspect the method's
SupportedOSPlatformGuard
attributes - Set the API's supported platforms to include all of the platforms indicated by the attributes
We 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.
Here's the plan to get this implemented:
|
Modified Illustration of Acceptance CriteriaI 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). public class OperatingSystem
{
// Guard methods that simulate the OperatingSystem members
// [SupportedOSPlatformGuard("ios")] this annotation is not needed as the method name express that it is iOS guard
[SupportedOSPlatformGuard("maccatalyst")]
public static bool IsIOS() => IsIOS() || IsMacCatalyst();
// [SupportedOSPlatformGuard("maccatalyst")] for same reason annotation doesn't need
public static bool IsMacCatalyst() => IsMacCatalyst();
...
}
public class AnnotationScenarios // APIs for the scenarios
{
// The [SupportedOSPlatform("maccatalyst")] attribute would not exist in the code
// but it would be inferred by the analyzer.
[SupportedOSPlatform("ios")]
void SupportedOnIOSAndMacCatalyst() { }
// Having the [SupportedOSPlatform("maccatalyst")] explicitly
// has no effect, it is same as above annotation
[SupportedOSPlatform("maccatalyst")]
[SupportedOSPlatform("ios")]
void SupportedOnIOSAndMacCatalyst() { }
[SupportedOSPlatform("ios")]
// The following attribute could be used
// to cancel the inferred "maccatalyst" support
[UnsupportedOSPlatform("maccatalyst")]
void SupportedOnIOS_ButNotMacCatalyst() { }
// Unlike the [SupportedOSPlatform("ios")]
// this will not infer iOS support, only support maccatalyst
[SupportedOSPlatform("maccatalyst")]
void SupportedOnMacCatalyst() { }
[UnsupportedOSPlatform("ios")]
// The following attribute would not exist in the code
// but it would be inferred by the analyzer.
// that also [UnsupportedOSPlatform("maccatalyst")]
void UnsupportedOnIOSAndMacCatalyst() { }
// only unsupported on maccatalyst
[UnsupportedOSPlatform("maccatalyst")]
void UnsupportedOnMacCatalyst() { }
void Main()
{
// This block is reachable on both IOS and MacCatalyst
if (OperatingSystem.IsIOS())
{
SupportedOnIOSAndMacCatalyst(); // Scenario 1a - No warning
SupportedOnMacCatalyst(); // Scenario 1b - Warning
SupportedOnIOS(); // Scenario 1c - No warning
SupportedOnIOS_ButNotMacCatalyst(); // Scenario 1d - Warning
UnsupportedOnIOS(); // Scenario 1e - Warning
UnsupportedOnMacCatalyst(); // Scenario 1f - Warning
}
// This block is reachable on MacCatalyst but not IOS
if (OperatingSystem.IsMacCatalyst())
{
SupportedOnIOSAndMacCatalyst(); // Scenario 2a - No warning
SupportedOnMacCatalyst(); // Scenario 2b - No warning
SupportedOnIOS(); // Scenario 2c - No warning
SupportedOnIOS_ButNotMacCatalyst(); // Scenario 2d - Warning
UnsupportedOnIOS(); // Scenario 2e - Warning
UnsupportedOnMacCatalyst(); // Scenario 2f - Warning
}
// This code block is reachable on IOS but not MacCatalyst
if (OperatingSystem.IsIOS() && !OperatingSystem.IsMacCatalyst())
{
SupportedOnIOSAndMacCatalyst(); // Scenario 3a - No warning
SupportedOnMacCatalyst(); // Scenario 3b - Warning
SupportedOnIOS(); // Scenario 3c - No warning
SupportedOnIOS_ButNotMacCatalyst(); // Scenario 3d - UPDATE: NO WARNING
UnsupportedOnIOS(); // Scenario 3e - Warning
UnsupportedOnMacCatalyst(); // Scenario 3f - No warning
}
}
} |
@jeffhandley as part of testing step 3 found that we left out the guard attributes scenario, most likely the when API is [UnsupportedOSPlatformGuard("ios")] // is this should imply [UnsupportedOSPlatformGuard("macCatalyst")] too?
// [UnsupportedOSPlatformGuard("macCatalyst")] or do we want to require the attribute explicitly
[UnsupportedOSPlatformGuard("tvos")]
public static bool IsDSASupported => !OperatingSystem.IsIOS() && !OperatingSystem.IsTvOS(); |
That is correct. However, you should be able to override that behavior if needed. [UnsupportedOSPlatform("ios")]
[SupportedOSPlatform("maccatalyst")]
public void WorksOnMacCatalyst_ButNotStandardIOS() { }
[SupportedOSPlatform("ios")]
[UnsupportedOSPlatform("maccatalyst")]
public void WorksOnStandardIOS_ButNotMacCatalyst() {} |
Sorry -- I misread -- you were asking about the guard attributes... Re-reviewing... |
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. |
Here's a representation of what I think can be inferred for the guard attributes: [SupportedOSPlatformGuard("ios")] // specified
// [SupportedOSPlatformGuard("maccatalyst")] cannot be inferred, because
// it's possible for IsIOS() to be true while IsMacCatalyst() is false.
public bool WorksOnIOS => OperatingSystem.IsIOS();
[SupportedOSPlatformGuard("maccatalyst")] // specified
[SupportedOSPlatformGuard("ios")] // can be inferred, because
// if IsMacCatalyst() returns true, then IsIOS() will also return true.
public bool WorksOnMacCatalyst => OperatingSystem.IsMacCatalyst();
[UnsupportedOSPlatformGuard("ios")] // specified
[UnsupportedOSPlatformGuard("maccatalyst")] // can be inferred, because
// if IsIOS() returns false, then IsMacCatalyst() will also return false.
public bool DoesNotWorkOnIOS => !OperatingSystem.IsIOS();
[UnsupportedOSPlatformGuard("maccatalyst")] // specified
// [UnsupportedOSPlatformGuard("ios")] cannot be inferred, because
// it's possible for IsMacCatalyst() to be false while IsIOS() is true.
public bool DoesNotWorkOnMacCatalyst => !OperatingSystem.IsMacCatalyst(); |
Thanks, @jeffhandley, all looked right initially, but now I think the section covering the [SupportedOSPlatformGuard("ios")] // specified
[SupportedOSPlatformGuard("maccatalyst")] // both specified
// The guard attributes would produce OperatingSystem.IsIOS() || OperatingSystem.IsMacCatalyst() logic
public bool WorksOnIOSMacCatalyst => true;
// And because the guard method OperatingSystem.IsIOS() now has [SupportedOSPlatformGuard("maccatalyst")]
// OperatingSystem.IsIOS() is now infer OperatingSystem.IsIOS() || OperatingSystem.IsMacCatalyst();
// therefore [SupportedOSPlatformGuard("ios")] snould inferl [SupportedOSPlatformGuard("maccatalyst")] too
// Further as OperatingSystem.IsMacCatalyst() has no attribute and it only infer OperatingSystem.IsMacCatalyst()
[SupportedOSPlatformGuard("maccatalyst")] // specified
// [SupportedOSPlatformGuard("ios")] // can not be inferred, therefore to guard ios user need to add the attribute explicitly
public bool WorksOnMacCatalyst => OperatingSystem.IsMacCatalyst(); |
Makes sense, @buyaa-n; thanks. I forgot that the guards produce OR logic. |
For step 4 filed an issue, this one can be closed now |
Thanks everyone! |
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 sameXXX
syntax for platform suffix. All theOperatingSystem.IsXXX()
APIs are mutually exclusive and at most one of them returnstrue
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 withnet6.0-ios
TFMs. This creates a disparity where this relationship is not captured by theOperatingSystem.IsIOS/IsMacCatalyst
APIs and the unavailable Mac Catalyst APIs have to include explicitUnsupportedOSPlatform("maccatalyst")
annotations even though they don't targetnet6.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 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 avail
and it produces the following output:The interpretation is that
__is_target_os(...)
treats Mac Catalyst as iOS variant.__builtin_available
uses theiOS <version>
value on Mac Catalyst unless an explict check is specified.Proposed solutions
Proposal A
OperatingSystem.IsIOS()
would returntrue
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 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 appropriateUnsupportedOSPlatformGuard
attributes. This would simplify the checks in code while keeping theOperatingSystem.IsXXX
APIs more consistent. There's a potential error for the caller to keep usingIsIOS
whereIsIOSOrMacCatalyst
should 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-ios
and not targetingnet6.0-maccatalyst
in 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.IsXXX
map the TFM fallbacks in general?IsLinux()
returntrue
on 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.
The text was updated successfully, but these errors were encountered: