-
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
Guarding calls to platform-specific APIs #33331
Comments
I feel like the method name needs something about the versioning. Like |
Could the two overloads be replaced by a single method taking a public static bool IsOSVersionAtLeast(OSPlatform osPlatform, Version minimumVersion); |
It'd be great if we could design this together with API for minimal version annotations. If we had both standardized someone could, for example, write missing version check analyzer which would work everywhere. The expanded version of the sample public void OnClick(object sender, EventArgs e)
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.iOS, 12, 0))
{
NSFizBuzz();
}
}
[IntroducedOn(OSPlatform.iOS, 12, 0)]
static void NSFizBuzz()
{
} I think the same can apply to libraries targetting Linux or Windows API which could be annotated when API minimal OS version is higher than the lowest version. Xamarin version can be explored at https://github.com/xamarin/xamarin-macios/blob/master/src/ObjCRuntime/PlatformAvailability2.cs |
What is the version that this will use on Linux? Is Android treated as a Linux flavor in these APIs?
The feature detection is a fine theory, but it is not an option in number of situations on Windows either. #32575 has recent example. |
Will this API work correctly in Windows too? |
The current OS version APIs in Windows (for which .NET uses) lie about the real Windows OS version due to how Windows OS uses EXE manifests to decide if reporting the actual version number would break an app or not. However, there are native Win32 APIs on Windows that will always return the true Windows OS version (including build number). Will this new .NET API use those Windows APIs? |
Why not have an overload that takes major version only? I would expect that people will check for major version only most of the time. |
Why use two separate overloads instead of one with optional parameter? I.e.: public static bool IsOSPlatform(
OSPlatform osPlatform, int majorVersion, int minorVersion, int revision = 0); (I'm assuming that @nil4 Using If the only common way of calling this method is with hard-coded version, then using |
@svick you were probably thinking of inline I was thinking along the lines of storing the class App {
static readonly Version ios12 = new Version(12, 0);
static void Main() {
if (RuntimeInformation.IsOSVersionAtLeast(OSPlatform.iOS, ios12))
NSFizBuzz();
// .. more stuff ..
if (RuntimeInformation.IsOSVersionAtLeast(OSPlatform.iOS, ios12))
NSFizBuzzML();
}
} Take the code given in #32575 as an example: Lines 144 to 146 in 3be5238
That could end up looking like this: static readonly Version quicMinVersion = new Version(10, 0, 19041, 0);
static MsQuicApi() {
IsQuicSupported = RuntimeInformation.IsOSVersionAtLeast(OSPlatform.Windows, quicMinVersion);
} The benefits I see are that |
macOS 10 is 19 years old and still going strong. |
Right, this overload would not apply to OSes that have decided to be stuck on the same major version. Both iOS and Android that are motivating this API have a more sane versioning story that increments the major version number. |
Why not just |
I'd be absolutely shocked if this API is in such a hot path that any allocations would be noticeable. |
I think that would depend on what kind of feature you are querying for. If this was something like a low level timer or a graphics/multimedia API, then it could be used in a hot path and constant folding/allocation free would basically be a must. |
If it's really that hot, then wouldn't the application itself want to cache the result? In your scenario I imagine they'd want to query the feature once upfront and then create a static function pointer to |
The API doesn't have to be fast, people can cache the value: class App {
static readonly bool ios12 = RuntimeInformation.IsOSVersionAtLeast(OSPlatform.iOS, new Version(12, 0));
static void Main() {
if (ios12)
NSFizBuzz();
// .. more stuff ..
if (ios12)
NSFizBuzzML();
}
} Although I agree that the faster the better of course. Personally I'm in favor of giving people the API that best suit their needs, and since it seems in this case we don't really know how people are going to use the API, we should provide all: public static bool IsOSVersionAtLeast(OSPlatform osPlatform, int majorVersion);
public static bool IsOSVersionAtLeast(OSPlatform osPlatform, int majorVersion, int minorVersion);
public static bool IsOSVersionAtLeast(OSPlatform osPlatform, int majorVersion, int minorVersion, int revision);
public static bool IsOSVersionAtLeast(OSPlatform osPlatform, Version version); A point to have in mind is that these two are not identical: var a = new Version (1, 0);
var b = new Version (1, 0, 0);
Console.WriteLine (a == b); this prints |
I think there are two scenarios to consider (much as we are considering for the hardware intrinsic The first is you are JIT compiled, in which case you know the exact machine you are targeting and the check can become a constant. This isn't readily applicable to iOS/Android, but it is to Desktop and some other scenarios. I imagine the default scenario for most people is to use cached lookups, rather than dynamic dispatch, but we should likely support users deciding to do both (and having a cached lookup doesn't preclude doing dynamic dispatch). I think in both cases, however, not using |
No, but that makes code analysis much harder. |
I agree that neither the property or parameter need the "OS" prefix on it. The type of platform is inferred from the type name. (The bare word "Name" wouldn't be good since it could be mistaken for a label for the attribute instance, vs the platform identifier.) If we wanted to change anything about it, I'd change the parameter and property to |
@terrajobst The attributes as we have them allow us to create a list of platforms with included support. Was there consideration for an attribute to indicate excluded support for scenarios like this one? On the surface, it seems like an |
* Add platform-specific attributes Spec #33331 * Convert to xml doc * Update src/libraries/System.Private.CoreLib/src/System/Runtime/InteropServices/MinimumOSAttribute.cs Co-authored-by: Jeremy Barton <jbarton@microsoft.com> * Update src/libraries/System.Private.CoreLib/src/System/Runtime/InteropServices/ObsoletedInPlatformAttribute.cs Co-authored-by: Jeff Handley <jeffhandley@users.noreply.github.com> * Address code review * Add to ref assembly, test and fix build errors * Fix spacing, revert unwanted changes * Namespace was wrong, updated Co-authored-by: Jeremy Barton <jbarton@microsoft.com> Co-authored-by: Jeff Handley <jeffhandley@users.noreply.github.com> Co-authored-by: Buyaa Namnan <bunamnan@microsoft.com>
* Add OSPlatform entries for iOS/tvOS/watchOS/Android Implements the non-controversial new OSPlatform members from #33331 * Add tests for new OSPlatform entries
@adamsitnik @buyaa-n During the Design Review meeting today, we talked about some details that affect the open PRs related to this work.
|
This makes this API even more expensive than it is today and increases chances that people will need to cache the result to avoid performance bottlenecks. Is the compat analyzer going to do the required data flow analysis to see through cached checks for this? |
No, we determined we couldn't feasibly handle platform checks being in helper/utility methods and therefore only direct calls to Let's recap the options we have for where this logic could exist:
Is that accurate? Are there any other options? |
Cache the result of the "is current platform check?" in the |
Interesting... Something like this? RuntimeInformation.Unix.cs (platform-specific, to be defined on Windows and Browser as well) public static partial class RuntimeInformation
{
internal static IsOSPlatformOrEquivalent(string osPlatform)
{
string name = s_osPlatformName ??= Interop.Sys.GetUnixName();
if (osPlatform.Equals(name)) return true;
if (name == "OSX") return osPlatform.Equals("MACOS");
return false;
}
} OSPlatform.cs public readonly struct OSPlatform : IEquatable<OSPlatform>
{
internal bool IsCurrentOSPlatform { get; set; }
private OSPlatform(string osPlatform)
{
if (osPlatform == null) throw new ArgumentNullException(nameof(osPlatform));
if (osPlatform.Length == 0) throw new ArgumentException(SR.Argument_EmptyValue, nameof(osPlatform));
_osPlatform = osPlatform;
IsCurrentOSPlatform = RuntimeInformation.IsOSPlatformOrEquivalent(_osPlatform);
}
} RuntimeInformation.cs (cross-platform) public static partial class RuntimeInformation
{
public static bool IsOSPlatform(OSPlatform osPlatform)
{
return osPlatform.IsCurrentOSPlatform;
}
} |
@jeffhandley GlobalFlowStateAnalysis API provided by roslyn-analyzers (Manish recently added) supports cached checks, so we have it for free (just by using GlobalFlowStateAnalysis) |
Thanks, @buyaa-n; I obviously didn't realize that made it in. Would that also cover caching within utility/helper methods or properties though, or is it limited to caching within the scope of the method? Would the following work? private _canUseIOS14 = RuntimeInformation.IsOSPlatformOrLater(OSPlatform.iOS, 14);
private void M1()
{
if (_canUseIOS14)
{
M2();
}
}
[MinimumOSPlatform("ios14.0")]
private void M2()
{
} |
I really think a set of That array given to a single |
I think it is limited within the scope of the method as the action registered to |
Currently it doesn’t support reading values in fields and properties, but we can add support. It would be much cheaper if we only cared about read only fields and properties. Otherwise, we would need to enable PointsTo/Alias analysis, which is implemented in the repo, but would obviously make the analysis more expensive. Analysis supports interprocedural analysis, but it is off by default. We can consider enabling it by default with a max level 1 of call chain (single utility/helper). Call chain analysis length is configurable for all DFA in the repo, but obviously makes it more expensive with longer call chain threshold. In short, all analyses requested here is/can be supported without too much implementation cost. We only need to be cautious about how much we want to enable by default considering the standard performance vs precision trade off for any DFA. |
Note that the analysis understands Debug asserts. I would personally recommend we take route similar to dataflow analysis for C# nullable reference types:
Users can choose any of the above two modes:
|
@mavasani I don't see how this is not better: [OSPlatformSupportAttribute(new MinSupportedPlatformAttribute(){ Platform, Version }, new NotSupportedPlatform(){ Platform, Version}), new MaxSupportedPlatformAttribute(){ Platform, Version)]
int SomeMethod(int param){ /**/ } This gives you more flexibility and the ability to have all the information in one place rather than several. public sealed class OSPlatformSupportAttribute: System.Attribute {
//This is not allowed I guess... https://sharplab.io/#gist:d062656f543144e19805a3b4d5d80ab8
PlatformSupportAttribute[] PlatformSupportAttributes {get; protected set;}
}
public abstract class PlatformSupportAttribute: System.Attribute
{
int m_Major, m_Minor;
public bool IsSupported {get; protected set;} = true;
public string Platform {get; protected set;}
public Version Version {get;} => return new Version(m_Major,m_Minor);
protected PlatformSupportAttribute(string platform, int major, int minor, bool isSupported = true)
{
if(string.IsNullOrWhiteSpace(platform) throw new InvalidOperationException($"{nameof(platform)} cannot be null or consist of only whitespace");
Platform = platform;
IsSupported = isSupported;
m_Major = major;
m_Minor = minor;
}
}
public sealed class MinSupportedPlatformAttribute: PlatformSupportAttribute
{
public MinSupportedPlatformAttribute(string platform, int major, int minor, , bool isSupported = true) : base(platform, major, minor, isSupported)
{
}
}
public sealed class MaxSupportedPlatformAttribute: PlatformSupportAttribute
{
public MaxSupportedPlatformAttribute(string platform, int major, int minor, bool isSupported = true) : base(platform, major, minor, isSupported)
{
}
}
public sealed class NotSupportedPlatformAttribute: PlatformSupportAttribute
{
public NotSupportedPlatformAttribute(string platform, int major, int minor) : base(platform, major, minor, false)
{
}
} This could be extended for when the OS is patched live or even for Please reconsider. |
@juliusfriedman my comment was not related to your suggestion on actual attributes to use for annotating platform dependent operations. It was regarding the kind of dataflow analysis to perform when detecting if a platform dependent operation was invoked in correct context, when user has performed the platform checks in a helper method or stored it into a field or property. |
@juliusfriedman You suggestion does not compile as written. |
@jkotas that's unfortunate, it can either be resolved with a custom version struct or more likley additional members which exspose a version property publicly. +protected int m_Major,m_Minor,m_Build,m_Patch;
+public Version => new Version (m_Major, m_Minor);//Not sure about build and patch here See also: https://sharplab.io/#gist:d062656f543144e19805a3b4d5d80ab8 If you really can't have arrays there as in my gist than have a method on the type which returns an array and that array should be able to be baked into the assembly if static. + IEnumerable<PlatformSupportAttributes> GetPlatformSupportAttributes() |
* Add platform-specific attributes Spec dotnet/runtime#33331 * Convert to xml doc * Update src/libraries/System.Private.CoreLib/src/System/Runtime/InteropServices/MinimumOSAttribute.cs Co-authored-by: Jeremy Barton <jbarton@microsoft.com> * Update src/libraries/System.Private.CoreLib/src/System/Runtime/InteropServices/ObsoletedInPlatformAttribute.cs Co-authored-by: Jeff Handley <jeffhandley@users.noreply.github.com> * Address code review * Add to ref assembly, test and fix build errors * Fix spacing, revert unwanted changes * Namespace was wrong, updated Co-authored-by: Jeremy Barton <jbarton@microsoft.com> Co-authored-by: Jeff Handley <jeffhandley@users.noreply.github.com> Co-authored-by: Buyaa Namnan <bunamnan@microsoft.com>
For iOS and Android in particular we want the developer to be able to do runtime checks for the OS version in order to guard method calls. We've steered people away from
Environment.OSVersion
in favor ofRuntimeInformation.IsOSPlatform()
. However, we (deliberately) omitted a way to detect version numbers because the guidance has been to move to feature detection instead. However, we've learned that version checks are a practical necessity. Also, they are the status quo on iOS and Android.We plan on combining these guards with a set of custom attributes that are used by an analyzer to flag code that isn't properly guarded. For more details, see this spec.
API Proposal
This design allows us to encapsulate the version comparison, i.e. the "and later" part.
Usage: Recording Project Properties
The SDK already generates a file called
AssemblyInfo.cs
which includes the TFM. We'll extend on this to also record the target platform and minium version (which can be omitted in the project file which means it's the same as the target platform):Usage: Guarding Platform-Specific APIs
NSFizzBuzz
is an iOS API that was introduced in iOS 14. Since I only want to call the API when I'm running on a version of the OS that supports it I'd guard the call usingIsOSPlatformOrLater
:Usage: Declaring Platform-Specific APIs
The
RemovedInPlatformAttribute
andObsoletedInPlatformAttribute
will primarily be used by the OS bindings to indicatewhether a given API shouldn't be used any more.
The
MinimumPlatformAttribute
will be for two things:Both scenarios have effectively the same meaning for our analyzer: calls into the assembly/API are only legal if the call site is from the given operating system, in the exact or later version.
The second scenario can also be used by user code to forward the requirement. For example, imagine the
NSFizzBuzz
API to be complex. User code might want to encapsulate it's usage in a helper type:As far as the analyzer is concerned,
NSFizzBuzzHelper
can only be used on iOS 14, which means that its members can call iOS 14 APIs without any warnings. The requirement to check for iOS is effectively forwarded to code that calls any members onNSFizzBuzzHelper
.@dotnet/fxdc @mhutch
The text was updated successfully, but these errors were encountered: