Skip to content
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

Add feature switch attribute design doc #305

Open
wants to merge 16 commits into
base: main
Choose a base branch
from

Conversation

sbomer
Copy link
Member

@sbomer sbomer commented Nov 22, 2023

Adds a design for an attribute-based model for feature switches, to be supported in the trim/AOT analyzers and in ILLink/ILCompiler. There's significant overlap with @terrajobst's capability API analyzer draft in #261, but this approaches the problem more specifically with trimming support in mind.

accepted/2023/feature-switch-attributes.md Outdated Show resolved Hide resolved
accepted/2023/feature-switch-attributes.md Outdated Show resolved Hide resolved
accepted/2023/feature-switch-attributes.md Show resolved Hide resolved
accepted/2023/feature-switch-attributes.md Outdated Show resolved Hide resolved
accepted/2023/feature-switch-attributes.md Outdated Show resolved Hide resolved
accepted/2023/feature-switch-attributes.md Outdated Show resolved Hide resolved
accepted/2023/feature-switch-attributes.md Outdated Show resolved Hide resolved
accepted/2023/feature-switch-attributes.md Outdated Show resolved Hide resolved
accepted/2023/feature-switch-attributes.md Show resolved Hide resolved

- Feature attribute schemas

We could consider unifying this model with the platform compatibility analyzer. One difference is that the `SupportedOSPlatformAttribute` takes a string indicating the platform name. We would likely need to extend the understanding of feature attributes to support treating "features" differently based on this string, effectively supporting feature attributes which define not a single feature, but a schema that allows representing a class of features. For example:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

one slight complication with this is that we have some more complex logic in the platform compatibility checks, e.g. we know that MacCatalyst is a subset of iOS so checking OperatingSystem.IsIOS() is enough to also check for MacCatalyst.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The platform subsets (versions aside) are semantically almost the same idea as feature guards - IsIOS is annotated with SupportedOSPlatformGuard("maccatalyst") which effectively declares that iOS is a subset of MacCatalyst.

Co-authored-by: Alexander Köplinger <[email protected]>
accepted/2023/feature-switch-attributes.md Outdated Show resolved Hide resolved
accepted/2023/feature-switch-attributes.md Outdated Show resolved Hide resolved
accepted/2023/feature-switch-attributes.md Outdated Show resolved Hide resolved
accepted/2023/feature-switch-attributes.md Outdated Show resolved Hide resolved
accepted/2023/feature-switch-attributes.md Outdated Show resolved Hide resolved
accepted/2023/feature-switch-attributes.md Show resolved Hide resolved
accepted/2023/feature-switch-attributes.md Outdated Show resolved Hide resolved
sbomer and others added 2 commits November 22, 2023 11:36
- Move IsDynamicCodeCompiled goal down
- Mention challenge with arbitrary feature analysis
- Add future extension of validating FeatureSwitch
- Clarify that SomeOtherCondition isn't a FeatureGuard
accepted/2023/feature-switch-attributes.md Outdated Show resolved Hide resolved
accepted/2023/feature-switch-attributes.md Outdated Show resolved Hide resolved
accepted/2023/feature-switch-attributes.md Outdated Show resolved Hide resolved
accepted/2023/feature-switch-attributes.md Outdated Show resolved Hide resolved
accepted/2023/feature-switch-attributes.md Show resolved Hide resolved

We will focus initially on a model where feature switches are booleans that return `true` if a feature is enabled. We aren't considering supporting version checks, or feature switches of the opposite polarity (where `true` means a feature is disabled/unsupported). We will consider what this might look like just enough to gain confidence that our model could be extended to support these cases in the future, but won't design this fully in the first iteration.

## Feature guard attribute
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would help if you could list all the features you'll want to guard today. The only examples that currently exist seem to be around dynamic code. That doesn't seem to be too compelling, especially because those already have attributes.

Also, do you imagine that users can extend the set of features? Or are the features being guarded a closed set that is defined by the .NET platform?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a section with example use cases.

Also, do you imagine that users can extend the set of features?

Yes, with some limitations. I'm suggesting that users should be able to define:

  • guards for existing features supported by the trim analyzer
  • new feature switches with trimming support (but not analyzer support, initially)

So the initial implementation would allow defining guards for RequiresDynamicCodesAttribute and RequiresUnreferencedCodeAttribute, and defining new features with trimming/AOT branch removal support but no analyzer warnings.

But the two seem naturally related, so my goal is for the existing analysis attributes to be defined in terms of a more general model, whether or not we actually teach the analyzer to produce warnings for arbitrary feature attributes.


```csharp
class Feature {
[FeatureGuard(typeof(RequiresDynamicCodeAttribute))]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using a type seems unfortunate because that would require on attribute per feature and would disallow more complex shapes (eg an attribute with flags).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is mentioned under "Feature attribute schemas" in the possible future extensions. I expanded a bit on the example to show what it might look like to define a feature switch where the feature attribute takes extra parameters.

I don't think we're going to be able to cover every possible attribute shape, so my goal is to start with a restricted model, while showing that it can be extended in various directions. I'm not attached to this particular API shape, but defining features in terms of types seemed like a pretty natural starting point. Happy to take other suggestions.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The "Feature switches without feature attributes" has my favorite alternative so far - basically using the feature name string as the uniform representation.

sbomer and others added 3 commits November 28, 2023 13:21
- Move IsDynamicCodeCompiled goal down
- Mention challenge with arbitrary feature analysis
- Add future extension of validating FeatureSwitch
@jtschuster
Copy link
Member

jtschuster commented Nov 30, 2023

Personally, I think it makes the most sense to have a single identifier to associate guards, switches, and requirements to a feature. To me, using the feature name everywhere rather than an attribute makes the most sense, and I don't think we'd get an issue with correlation in the analyzer, trimmer, or Native AOT if we can change the existing Requires attributes.

I think this generalizes well to other feature switches, and fits well with AppContext.TryGetSwitch. Any guarded block (or method body with a RequiresFeatureAttribute) is assumed to have the guarded features "enabled" within it. Calls to APIs with the RequiresFeatureAttribute would compare the required feature name to the list of enabled feature names for that block.

The trimmer and Native AOT could hard code the (new) "UnreferencedCode" and "DynamicCode" feature names as false and could accept other feature names and values at the command line to do branch elimination for arbitrary feature switches.

I also like the proposed NamedFeatureAttribute as the base RequiresFeatureAttribute so we don't need to require FeatureNameAttribute to only be on RequiresFeatureAttribute-derived classes, and can apply the attribute ad-hoc without defining a new derived attribute.

class RequiresFeatureAttribute : Attribute
{
    public string FeatureName { get; }

    public NamedFeatureAttribute(string name) => FeatureName = name;
}
// [FeatureName("MyLibrary.Features.MyFeature")]
// class RequiresMyFeature : RequiresFeatureAttribute { }

class RequiresMyFeature : RequiresFeatureAttribute
{
    public RequiresMyFeature() : base("MyLibrary.Features.MyFeature") { }
}

class Features
{
    [FeatureSwitch("MyLibrary.Features.MyFeature")]
    public static bool MyFeature => AppContext.TryGetSwitch("MyLibrary.Features.MyFeature" out bool val) && val;

    [FeatureSwitch("MyLibrary.Features.MyOtherFeature")]
    public static bool MyOtherFeature => AppContext.TryGetSwitch("MyLibrary.Features.MyOtherFeature" out bool val) && val;

    [FeatureGuard("MyLibrary.Features.MyOtherFeature")]
    [FeatureGuard("MyLibrary.Features.MyFeature")]
    public static bool BothFeatures => MyFeature && MyOtherFeature;
}

public class Program
{
    [RequiresMyFeature]
    public static void MyFeatureMethod() { }

    [Requires("MyLibrary.Features.MyOtherFeature")]
    public static void MyOtherFeatureMethod() { }
}

@sbomer
Copy link
Member Author

sbomer commented Dec 1, 2023

Thanks @jtschuster, I incorporated those suggestions into the "Feature switches without feature attributes" section.

I think the main tradeoff is that the string-based model requires giving a string name to every feature (which we may or may not want for RequiresUnreferencedCodeAttribute), whereas the attribute-based model requires defining an attribute for each feature. At least the attribute types can be private, while the string names are effectively public. However, I do like that the string-based API shape resembles preprocessor symbols.

[FeatureSwitch("MY_LIBRARY_FEATURE")]
public static bool IsSupported => // ...

[IfDefined("MY_LIBRARY_FEATURE")]
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had to resist calling this UnconditionalConditionalAttribute 😅

@vitek-karas
Copy link
Member

I'm going to play devil's advocate and only provide negative feedback ;-)

For the proposal from Jackson - feature name model:

  • The feature name string is the least visible part of the feature. It's rarely used in MSBuild (where it's typically aliased with a property). It's basically never used in C# where one uses the real property. It's only visible in the internal code (MSBuild prop definition, and C# property definition) and in runtime config/AppContext where nobody looks.
  • Would force us to define a feature string name for RequiresUnreferencedCode

For the attribute based model:

  • This requires each feature to define a "Requires" attribute - even if it's not needed and not wanted. For example, we would not really want to define RequiresEventSource attribute, or lot of the others.

If I had to pick one of these two, I would prefer the string based model. I honestly like the "Separate type to define the feature" more... but neither is perfect :-)

@sbomer
Copy link
Member Author

sbomer commented Jan 11, 2024

Per today's discussion:

  • Changed FeatureSwitch to FeatureCheck
  • Changed FeatureName to FeatureSwitchDefinition, in case we want to make the string argument optional and infer the feature name from the type name in the future. (Or should this be just FeatureDefinition?)
  • We agreed on the API shape where FeatureCheck directly references the feature attribute type for now

- Add new examples for FeatureDependsOn
- Update section about relationship between feature checks and guards
- Update API shape with API review feedback
- Update references to FeatureGuard
- Remove implementation notes that were hard to keep updated
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants