-
Notifications
You must be signed in to change notification settings - Fork 4.7k
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
[API Proposal]: Attribute model for feature APIs #96859
Comments
Tagging subscribers to this area: @dotnet/area-system-runtime-compilerservices Issue DetailsBackground and motivation.NET has feature switches which can be set to turn on/off areas of functionality in our libraries, with optional support for removing unused features when trimming or native AOT compiling. Feature switches suffer from a poor user experience:
This document proposes an attribute-based model for feature switches that will significantly improve the user experience, by removing the need for this XML and enabling analyzer support. More detail and discussion in dotnet/designs#305. API Proposalnamespace System.Diagnostics.CodeAnalysis;
[AttributeUsage(AttributeTargets.Property, Inherited = false)]
public sealed class FeatureCheckAttribute : Attribute
{
public Type FeatureType { get; }
public FeatureCheckAttribute(Type featureType)
}
[AttributeUsage(AttributeTargets.Property, Inherited = false, AllowMultiple = true)]
public sealed class FeatureGuardAttribute : Attribute
{
public Type FeatureAttributeType { get; }
public FeatureGuardAttribute(Type featureAttributeType)
}
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
public sealed class FeatureSwitchDefinitionAttribute : Attribute
{
public string FeatureName { get; }
public FeatureSwitchDefinitionAttribute(string featureName)
} API Usage
namespace System.Runtime.CompilerServices;
public static class RuntimeFeature
{
[FeatureCheck(typeof(RequiresDynamicCodeAttribute))]
public static bool IsDynamicCodeSupported { get; } = AppContext.TryGetSwitch("System.Runtime.CompilerServices.RuntimeFeature.IsDynamicCodeSupported", out bool isDynamicCodeSupported) ? isDynamicCodeSupported : true;
[FeatureGuard(typeof(RequiresDynamicCodeAttribute))]
public static bool IsDynamicCodeCompiled => IsDynamicCodeSupported;
}
namespace System.Diagnostics.CodeAnalysis;
[FeatureSwitchDefinition("System.Runtime.CompilerServices.RuntimeFeature.IsDynamicCodeSupported")]
public sealed class RequiresDynamicCodeAttribute : RequiresFeatureAttribute
{
// ...
} This allows them to guard calls to annotated APIs, so that the callsites don't produce analyzer warnings: if (RuntimeFeature.IsDynamicCodeSupported)
APIWhichRequiresDynamicCode(); // No warnings
if (RuntimeFeature.IsDynamicCodeCompiled)
APIWhichRequiresDynamicCode(); // No warnings
[RequiresDynamicCode("Does something with dynamic codegen")]
static void APIWhichRequiresDynamicCode() {
// ...
} When the app is trimmed with the feature switch Analyzer warnings will initially be limited to Alternative DesignsSeparate type to represent featureThere could be a level of indirection, so that the feature is represented not by the attribute type, but by a separate type that is linked to the attribute type: [FeatureAttribute(typeof(RequiresFeatureAttribute))]
[FeatureSwitchDefinition("MyLibrary.Feature.IsSupported")]
class Feature {
[FeatureCheck(typeof(Feature))]
public static bool IsSupported => ...;
[RequiresFeature(typeof(Feature))]
public static void DoSomething() { ... }
}
class RequiresFeatureAttribute : Attribute { ... } String-based APIThe attributes could instead use strings, making the usage slightly more analogous to preprocessor symbols. The difference is that callsites or code blocks within a method can't be annotated directly, so the class Feature {
[FeatureCheck("MY_LIBRARY_FEATURE")]
public static bool IsSupported => ...;
[RequiresFeature("MY_LIBRARY_FEATURE")]
public static void DoSomething() { ... }
}
class Consumer {
static void Main() {
if (Feature.IsSupported)
Feature.DoSomething();
}
} (compare to preprocessor symbols): class Library {
#if MY_LIBRARY_FEATURE
public static void DoSomething() { ... }
#endif
}
class Consumer {
static void Main() {
#if MY_LIBRARY_FEATURE
Feature.DoSomething();
#endif
}
} RisksThe proposed API doesn't cover every possible pattern that might be useful for feature switches. We are aiming to start with a small, well-defined set of behavior, but need to ensure this doesn't lock us out of future extensions. We can extend these by adding extra constructor parameters to the attributes in the future, as discussed in dotnet/designs#305.
|
namespace System.Diagnostics.CodeAnalysis;
[AttributeUsage(AttributeTargets.Property, Inherited = false)]
public sealed class FeatureCheckAttribute : Attribute
{
public Type FeatureType { get; }
public FeatureCheckAttribute(Type featureType)
}
[AttributeUsage(AttributeTargets.Property, Inherited = false, AllowMultiple = true)]
public sealed class FeatureGuardAttribute : Attribute
{
public Type FeatureType { get; }
public FeatureGuardAttribute(Type featureType)
}
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
public sealed class FeatureSwitchDefinitionAttribute : Attribute
{
public string SwitchName { get; }
public FeatureSwitchDefinitionAttribute(string switchName)
} |
Based on an exploration of the corelib intrinsics analyzer, I've updated the proposal to allow Usage for intrinsics analyzerThese attributes can express the semantics of the (see "Corelib intrinsics analyzer" in dotnet/designs#305 for a more detailed comparison) namespace System.Runtime.Intrinsics.X86;
[RequiresFeature(typeof(Avx2))] // Similar to [CompExactlyDependsOn(typeof(Avx2))]
[FeatureGuard(typeof(Avx))]
public abstract class Avx2 : Avx
{
[FeatureCheck(typeof(Avx2))]
public static new bool IsSupported => // ...
[RequiresFeature(typeof(X64))]
[FeatureGuard(typeof(Avx.X64))]
[FeatureGuard(typeof(Avx2))]
public new abstract class X64 : Avx.X64
{
[FeatureCheck(typeof(X64))]
public static new bool IsSupported => // ...
}
} This allows the IsSupported checks, or equivalent if (Avx2.IsSupported) {
Avx2.DoSomething();
Avx.DoSomething();
}
if (Avx2.X64.IsSupported) {
Avx2.DoSomething();
Avx.X64.DoSomething();
}
[RequiresFeature(typeof(Avx2))]
static void VectorizationHelper1() {
Avx2.DoSomething();
Avx.DoSomething();
}
[RequiresFeature(typeof(Avx2.X64))]
static void VectorizationHelper2() {
Avx2.DoSomething();
Avx.X64.DoSomething();
} Usage for crypto hash algorithmsThese attribute definitions would work as follows for an analyzer designed to propagate requirements on the SHA3 family of hash algorithms: [RequiresFeature(typeof(SHA3_256)]
public abstract class SHA3_256 : HashAlgorithm
{
[FeatureCheck(typeof(SHA3_256))]
public static bool IsSupported => // ...
public static new SHA3_256 Create()
{
CheckSha3Support();
return new Implementation();
}
// ...
[RequiresFeature(typeof(SHA3_256))]
private sealed class Implementation : SHA3_256
{
// ...
}
} Here, var data = new byte[] { 0x01, 0x02 };
SHA3_256.HashData(data); // warning
if (SHA3_256.IsSupported)
SHA3_256.HashData(data); // OK
[RequiresFeature(typeof(SHA3_256))]
static void HashHelper() {
SHA3_256.HashData(data); // OK
} Note on
|
Adds support for annotating static boolean properties with `[FeatureCheckAttribute(typeof(RequiresDynamicCodeAttribute))]`, causing the property to be treated as a guard for analyzer warnings about the corresponding `Requires` attribute. Adds two new warnings for: - Invalid use of `FeatureCheckAttribute` applied to a non-static or non-bool property - Implementation of the property doesn't obviously satisfy the "guard property" (it should return false whenever the guarded feature is disabled). See #94625 for notes on the design. See #96859 for the API proposal. --------- Co-authored-by: Jackson Schuster <36744439+jtschuster@users.noreply.github.com>
Here's a scenario where supporting different attributes for analyzer warnings would be useful: xamarin/xamarin-macios#20059 (comment) Basically we want to annotate APIs in the BCL so that if developers use those APIs they get a warning if they haven't configured their projects correctly (developers have to add a file to their apps explaining why they're using certain APIs, and we want to automatically be able to tell developers they need to add said file + we want to automate annotation of our API surface where we only need manual intervention when we use the native API that needs annotation, and then we have the analyzer tell us all the resulting managed APIs that use said native API, recursively). |
namespace System.Diagnostics.CodeAnalysis;
[AttributeUsage(AttributeTargets.Property, Inherited = false, AllowMultiple = true)]
public sealed class FeatureGuardAttribute : Attribute
{
public Type FeatureType { get; }
public FeatureGuardAttribute(Type featureType)
}
[AttributeUsage(AttributeTargets.Property, Inherited = false)]
public sealed class FeatureSwitchDefinitionAttribute : Attribute
{
public string SwitchName { get; }
public FeatureSwitchDefinitionAttribute(string switchName)
} |
I've typed this up just to make sure I understand what was landed on here. Please let me know if any of the following is meaningfully incorrect.
[FeatureGuard(...)] usage scenario by third party libraryAs a third party library author, I can put Presumably, when I do so I should also [FeatureSwitchDefinition(...)] usage scenario by third party libraryLet's assume my library has a not always used feature that pulls in a lot of code in a way the linker cannot automatically remove when an application doesn't need it. For this example support for a file format. but it is not fundamentally tied to something like RequiresDynamicCodeAttribute, I could define a property like the following, and wrap relevant code with the guard, to make it possible to trim away such features: static internal class TrimmableFeatures
{
[FeatureSwitchDefinition("Contoso.ImageProcessing.IsWebPSupported")]
public bool IsWebPSupported => true;
} This could be combined with a bit of MSBuild in my library's NuGet package to allow applications to say they don't want WebP support when trimmed by setting say a This would of course provide no analyzer level checking, which might be fine for my library, for example if there is no WebP specific public API surface, and I was not seeking help ensuring I code the inside of my library correctly. If I did want analyzer checks, either for inside the library, or potentially even for callers of the library, I'd add a |
@KevinCathcart thanks for the write-up! The general idea is right. A few clarifications:
|
Closing this as completed:
|
Background and motivation
.NET has feature switches which can be set to turn on/off areas of functionality in our libraries, with optional support for removing unused features when trimming or native AOT compiling.
Feature switches suffer from a poor user experience:
This document proposes an attribute-based model for feature switches that will significantly improve the user experience, by removing the need for this XML and enabling analyzer support.
More detail and discussion in dotnet/designs#305.
The attribute model is heavily inspired by the capability-based analyzer draft.
API Proposal
API Usage
FeatureCheck
may be placed on a static boolean property to indicate that it is a check for the referenced feature (represented by a type):FeatureGuard
on a feature type may be used to express dependencies between features.This allows the property, or
RequiresFeatureAttribute
referencing the feature type, to guard calls to APIs annotated as requiring that feature:FeatureGuard
may also be placed directly on a static boolean property as a shorthand, to define a simple guard without a separate feature type. So the annotations onIsDynamicCodeCompiled
could be simplified to:For trimming support,
FeatureSwitchDefinition
may be applied to the attribute type to give the feature a name:When the app is trimmed with the feature switch
"System.Runtime.CompilerServices.RuntimeFeature.IsDynamicCodeSupported"
set tofalse
, the properties are rewritten to returnfalse
, and the guarded branches are removed.The initial implementation will not support
RequiresFeatureAttribute
. Instead, analyzer warnings will initially be limited toRequiresUnreferencedCodeAttribute
,RequiresDynamicCodeAttribute
, andRequiresAssemblyFilesAttribute
. It will still be possible to define an (otherwise unused) type for use inFeatureCheck
andFeatureGuard
, to influence branch elimination.Applications
Aside from the analysis for
RequiresUnreferencedCode
,RequiresDynamicCode
,RequiresAssemblyFiles
, these semantics work well for protecting usage of hardware intrinsics or crypto hash algorithms: #96859 (comment).Alternative Designs
Separate type to represent feature
There could be a level of indirection, so that the feature is represented not by the attribute type, but by a separate type that is linked to the attribute type:
The current proposal allows attribute or non-attribute types.
String-based API
The attributes could instead use strings, making the usage slightly more analogous to preprocessor symbols. The difference is that callsites or code blocks within a method can't be annotated directly, so the
IsSupported
check serves the purpose that#if
serves, but at trim time.(compare to preprocessor symbols):
Risks
The proposed API doesn't cover every possible pattern that might be useful for feature switches. We are aiming to start with a small, well-defined set of behavior, but need to ensure this doesn't lock us out of future extensions. We can extend these by adding extra constructor parameters to the attributes in the future, as discussed in dotnet/designs#305.
Updates
ReplacedChanged back laterFeatureGuardAttribute
withFeatureDependsOnAttribute
FeatureName
toSwitchName
inFeatureSwitchDefinition
RequiresFeatureAttribute
in the proposalFeatureDepndsOnAttribute
back toFeatureGuardAttribute
, allowed on properties or classesThe text was updated successfully, but these errors were encountered: