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 SingleFileUnsupportedAttribute #45705

Closed
mateoatr opened this issue Dec 7, 2020 · 21 comments
Closed

Add SingleFileUnsupportedAttribute #45705

mateoatr opened this issue Dec 7, 2020 · 21 comments
Labels
api-approved API was approved in API review, it can be implemented api-suggestion Early API idea and discussion, it is NOT ready for implementation area-Single-File

Comments

@mateoatr
Copy link
Contributor

mateoatr commented Dec 7, 2020

Background and Motivation

There are different APIs that are unusable in single-file applications (see https://docs.microsoft.com/en-us/dotnet/core/deploying/single-file). This attribute's intent to help us mark methods that are single-file unfriendly so that tools can better reason about them (see #44488.)

As a first step after adding this attribute to the runtime (if approved) we intend to annotate the following APIs inside the runtime libraries: Assembly.Location, Module.FullyQualifiedName, Module.Name, Assembly.GetFile, Assembly.GetFiles, Assembly.CodeBase, Assembly.EscapedCodeBase, AssemblyName.CodeBase, AssemblyName.EscapedCodeBase.

Proposed API

namespace System.Diagnostics.CodeAnalysis
{
    [AttributeUsage(AttributeTargets.Method | AttributeTargets.Constructor, Inherited = false, AllowMultiple = false)]
    public sealed class SingleFileUnsupportedAttribute : Attribute
    {
        public SingleFileUnsupportedAttribute(string message)
        {
            Message = message;
        }

        public string Message { get; }

        public string Url { get; set; }
    }
}

Usage Examples

This attribute should be used to annotate single-file unfriendly methods:

namespace System.Reflection
{
    public abstract partial class Assembly : ICustomAttributeProvider, ISerializable
    {
        ...
-        public virtual string Location => throw NotImplemented.ByDesign;
+        public virtual string Location
+        {
+            [SingleFileUnsupported("Always returns an empty string for single-file apps",
+                "<url pointing to a docs page with more information and different options on how to deal with this>")] 
+           get { throw NotImplemented.ByDesign; }
+        }

        ...
+	[SingleFileUnsupported("Throws IOException for single-file apps", "<url>"]
        public virtual FileStream? GetFile(string name) { throw NotImplemented.ByDesign; }

        ...	
    }
}

Risks

None. With this attribute we expect to better inform users whenever they make use of incompatible methods in their single-file apps.

@mateoatr mateoatr added the api-suggestion Early API idea and discussion, it is NOT ready for implementation label Dec 7, 2020
@Dotnet-GitSync-Bot Dotnet-GitSync-Bot added area-System.Runtime.CompilerServices untriaged New issue has not been triaged by the area owner labels Dec 7, 2020
@mateoatr mateoatr added area-Single-File and removed area-System.Runtime.CompilerServices untriaged New issue has not been triaged by the area owner labels Dec 7, 2020
@ghost
Copy link

ghost commented Dec 7, 2020

Tagging subscribers to this area: @agocke, @vitek-karas
See info in area-owners.md if you want to be subscribed.

Issue Details

Background and Motivation

There are different APIs that are unusable in single-file applications (see https://docs.microsoft.com/en-us/dotnet/core/deploying/single-file). This attribute's intent to help us mark methods that are single-file unfriendly so that tools can better reason about them (see #44488.)

Proposed API

namespace System.Diagnostics.CodeAnalysis
{
    [AttributeUsage(AttributeTargets.Method | AttributeTargets.Constructor, Inherited = false, AllowMultiple = false)]
    public sealed class SingleFileUnsupportedAttribute : Attribute
    {
        public SingleFileUnsupportedAttribute(string message)
        {
            Message = message;
        }

        public string Message { get; }

        public string Url { get; set; }
    }
}

Usage Examples

This attribute should be used to annotate single-file unfriendly methods:

namespace System.Reflection
{
    public abstract partial class Assembly : ICustomAttributeProvider, ISerializable
    {
        ...
-        public virtual string Location => throw NotImplemented.ByDesign;
+        public virtual string Location
+        {
+            [SingleFileUnsupported("Always returns an empty string for single-file apps",
+                "<url pointing to a docs page with more information and different options on how to deal with this>")] 
+           get { throw NotImplemented.ByDesign; }
+        }

        ...
+	[SingleFileUnsupported("Throws IOException for single-file apps", "<url>"]
        public virtual FileStream? GetFile(string name) { throw NotImplemented.ByDesign; }

        ...	
    }
}

Risks

None. With this attribute we expect to better inform users whenever they make use of incompatible methods in their single-file apps.

Author: mateoatr
Assignees: -
Labels:

api-suggestion, area-Single-File

Milestone: -

@marek-safar
Copy link
Contributor

Is there any proposal/design about how are we planning to improve user experience for form factors which have restrictions on existing APIs?

Unless we want to create an attribute for every combination it'd be better to model the design on feature which is more complex. That could be for example reflection emit for pure AOT or COM.

@agocke
Copy link
Member

agocke commented Dec 7, 2020

What kind of broader feature design were you thinking of? A more general attribute like,

[RequiresPlatformCapability(PlatformCapability.SingleFile)]

for example?

@MichalStrehovsky
Copy link
Member

@terrajobst has a more broad design for capability annotations here: dotnet/designs#111. When we were discussing RequiresUnreferencedCodeAttribute (or "TrimmingUnsupportedAttributed" if I were to map it to the nomenclature proposed in this issue) it didn't look straightforward to apply for things like trimming.

@marek-safar
Copy link
Contributor

What kind of broader feature design were you thinking of?

I was thinking about more end-to-end design with variations of needs coming from different features. More comprehensive proposal covering

  • One attribute vs many attributes for all scenarios we know already
  • Cascading logic (e.g. propagating via callers)
  • Which tools will support these attribute and how will be configured
  • Will the attribute issue warning or errors for code which does not work
  • Trimability of code which won't work on form factor (e.g. SRE for NativeAOT) and has capability attribute
  • etc.

@agocke
Copy link
Member

agocke commented Dec 8, 2020

Let me draw out the bounds of this discussion, since I think there are fewer degrees of freedom than are perceived.

Let's say that the goal is to provide a way to mark an API as requiring a capability, and having a separate API that can detect that capability, but at a higher level of abstraction. The bounds then are:

  1. We would like to use attributes for annotation. They're common, user-understandable, constant at the metadata level, and do not affect the type system.

  2. Platform capabilities must be independent, meaning that they should be able to be configured separately.

  3. Platform capabilities must be defined both at the capability "use" site (e.g., an API only available on Windows) and the capability "test" site (e.g., an API which checks if we are running on Windows).

  4. The next highest level of abstraction than what we have (hardcoding certain attributes) is to allow for adding capabilities without changing code in the linker. We can move up the abstraction boundaries by parameterizing. I believe all solutions in this category are isomorphic to [RequiresPlatformCapability(PlatformCapability.SingleFile)], differing only in efficiency or UX.

  5. The second useful level of abstraction is to allow for adding capabilities in third party libraries, without changing the linker or the framework. By requirements (1), (2), and (3), we need a way to use attributes which does not require central coordination, but does provide uniqueness. The only type allowed in attributes which meets these requirements is System.Type. Therefore, all solutions are isomorphic to [RequiresPlatformCapability(new[] { typeof(PlatformCapability.SingleFile) })].

I believe all designs would be variants of this specification.

The open questions I see are:

  • What is the best UX? Is the parameterization syntax good for the user? Is defining a new attribute for each capability a better experience? The attribute could inherit from an attribute similar to the above, so there's no reason why we couldn't still build the flow analysis on the above definition.

  • Does this actually work for our use cases? As far as I can tell, we could not make RequiresUnreferencedCode use this system, as the behaviors that are affected are not possible to define just by annotating APIs. "Silencing all linker warnings" cannot be defined using this system.

  • Does this unnecessarily constrain the design of First draft of an analyzer for capability APIs designs#111? Are we convinced that this design is sufficient to address all use cases and we will be satisfied with it in the future?

@marek-safar
Copy link
Contributor

Let's say that the goal is to provide a way to mark an API as requiring a capability, and having a separate API that can detect that capability, but at a higher level of abstraction.

I think we could try to go beyond that. It'd be great to use the same approach to annotate APIs which have different behaviour based on other factors. I listed a few earlier but one can see such capabilities to be useful even for configurable options like InvariantGlobalization or other feature switches.

We would like to use attributes for annotation. They're common, user-understandable, constant at the metadata level, and do not affect the type system.

Using attributes is one thing and interpreting them is a different thing. As we learnt with platform compatibility attribute it's better to design from begging for intersections and exclusions.

/cc @jeffhandley

@mateoatr
Copy link
Contributor Author

mateoatr commented Dec 9, 2020

I've created a PR with a design doc in the linker to explain a little more what we want to do with the attribute (dotnet/linker#1681). It should also serve as a good place to settle the answers to why this approach and not others (still missing in the doc).

@agocke agocke added the api-ready-for-review API is ready for review, it is NOT ready for implementation label Dec 14, 2020
@agocke
Copy link
Member

agocke commented Dec 14, 2020

@terrajobst I believe this API is ready for review. If you wanted to consider broadened support I think one of my other proposals touches on the same problem

@MichalStrehovsky
Copy link
Member

Should we consider aligning with the Requires naming pattern? RequiresOriginalAssemblies/RequiresAssemblyFiles or something like that?

@agocke
Copy link
Member

agocke commented Jan 4, 2021

Should we consider aligning with the Requires naming pattern? RequiresOriginalAssemblies/RequiresAssemblyFiles or something like that?

Hmm, doesn't read very well to me. SingleFileUnsupported is pretty self-explanatory, but RequiresOriginalAssemblies seems really confusing -- I'd have to read docs to figure out what that means.

@agocke
Copy link
Member

agocke commented Jan 4, 2021

@terrajobst Did you want to sound off on the platform capability design and how it could fold into single-file analysis? I laid out some options in #45705 (comment).

@terrajobst terrajobst added api-approved API was approved in API review, it can be implemented and removed api-ready-for-review API is ready for review, it is NOT ready for implementation labels Jan 5, 2021
@terrajobst
Copy link
Contributor

terrajobst commented Jan 5, 2021

Video

  • The attribute seems reasonable
    • We shouldn't take a message because they are hard to change
    • We should take a diagnostic id
  • Generalizing this to a capability-based model seems too complex
  • We should consider whether we want a capability API (globally for single file or per assembly like HasLocation)
    • Do we need all the cases?
    • Is framework loaded from disk or memory?
    • Is user code loaded from disk or memory?
    • Is application extracted?
  • Is the attribute named correctly?
    • Unsupported seems to imply it throws; here it just means "the behavior might not be what you want" while some actually throw
namespace System.Diagnostics.CodeAnalysis
{
    [AttributeUsage(AttributeTargets.All, Inherited = false, AllowMultiple = false)]
    public sealed class SingleFileUnsupportedAttribute : Attribute
    {
        public SingleFileUnsupportedAttribute();
        public string DiagnosticId { get; set; }
        public string UrlFormat { get; set; }
    }
}

@vitek-karas
Copy link
Member

We shouldn't take a message because they are hard to change

I don't agree with this. The primary reason to have the message is so that the annotated code can provide a domain specific feedback. For example DI frameworks:

  • Without the message I would get something like Calling PopulateDIFromApp is not compatible with single-file, it may not work as expected (or similar vague message). This is basically unactionable - user will have to search for this and hopefully will find some SO answer explaining what this means and how to fix it. Yes, the URL could "solve" this, but it's not that nice.
  • With the message, the DI framework could make it look like Calling PopulateDIFromApp will not work in single-file, instead, please register all necessary types via RegisterDIProvider - this is actually actionable to the end-user.

We're trying to model this attribute to be similar to the RequiresUnreferencedCodeAttribute - which does have a message (exactly for the reason mentioned above - if I recall the API review discussion correctly).

Unsupported seems to imply it throws;

I agree that calling it "Unsupported" doesn't feel right. I like the RequiresSomething templated much better. This goes back to the discussion we had around RequiresUnreferencedCode - the reasoning was:
The attribute should describe the required capability/behavior which the code needs to operate correctly (or which it was designed for). It should not be tied any specific technical case/tool.

This kind of goes into the discussion of: Do we want to treat Xamarin.Android apps as single-file apps as well? And what about most native-AOT apps (iOS, CoreRT), those are technically also single-file... Even Blazor is single-file-like to some extent.
This would actually question the "single-file" part on its own (is Android app single-file - nobody will know, since it's not used as a term in that context).

@agocke
Copy link
Member

agocke commented Jan 5, 2021

We should take a diagnostic id

This sounds like it's based off of our decision making for Obsolete, but after thinking about it I'm not sure it's applicable to this scenario. @terrajobst let's set up a quick meeting to discuss trade-offs here.

@marknn3
Copy link

marknn3 commented Jan 6, 2021

What about naming it SingleFileBehaviorAttribute ? Or something similar.

@jkotas
Copy link
Member

jkotas commented Jan 6, 2021

I do like the idea of aligning with the Requires naming pattern as suggested by @MichalStrehovsky and @vitek-karas for this and future similar properties. What about RequiresAssemblyFiles or RequiresAssemblyPaths ?

RequiresOriginalAssemblies seems really confusing

I agree. It is not clear what "original" refers to. E.g. is it assembly binary before trimming and/or R2R?

@agocke
Copy link
Member

agocke commented Jan 6, 2021

I like the consistency of Requires..., but when I actually saw RequiresUnreferencedCode for the first time I had no idea what it meant. I still think of it as "the attribute that says you'll have linker problems" and not "the attribute stating dynamic dependencies"

My general framing would be: the structure of Requires... is good, but I think it comes at a loss of general readability. That still may be a good tradeoff.

@terrajobst
Copy link
Contributor

After some discussion with @agocke and @vitek-karas we agreed to this API shape instead:

namespace System.Diagnostics.CodeAnalysis
{
    [AttributeUsage(AttributeTargets.Constructor |
                    AttributeTargets.Event |
                    AttributeTargets.Method |
                    AttributeTargets.Property, Inherited = false, AllowMultiple = false)]
    public sealed class RequiresAssemblyFilesAttribute : Attribute
    {
        public RequiresAssemblyFilesAttribute();
        public string Message { get; set; }
        public string Url { get; set; }
    }
}

@dotnet/fxdc, please yell if you see any concerns.

@marek-safar
Copy link
Contributor

Why RequiresAssemblyFilesAttribute and not RequiresAssemblyFileAttribute ?

@vitek-karas
Copy link
Member

Why RequiresAssemblyFilesAttribute and not RequiresAssemblyFileAttribute ?

This was intentional. A relatively common confusion is that the attribute talks about the entire application - not about any single assembly. So we wanted to make it a bit clearer that it's a statement about "multiple assemblies" (as in assemblies in the application).

@ghost ghost locked as resolved and limited conversation to collaborators Feb 25, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
api-approved API was approved in API review, it can be implemented api-suggestion Early API idea and discussion, it is NOT ready for implementation area-Single-File
Projects
None yet
Development

No branches or pull requests

9 participants