Skip to content

OperatingSystem.IsIOS API is problematic #53084

@marek-safar

Description

@marek-safar

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.

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions