-
Notifications
You must be signed in to change notification settings - Fork 4.9k
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]: TypeDescriptor-related trimming support #101202
Comments
Tagging subscribers to this area: @dotnet/area-system-componentmodel |
class Program
{
static void Require<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>() { }
static void Main()
{
Require<Other>();
}
}
class Other
{
static IEnumerable<MethodInfo> Walk()
{
// Just some random ***trim safe*** reflection, it's not too important what this does.
Type? t = typeof(AttributeUsageAttribute);
foreach (var m in t.GetMethods())
yield return m;
}
} This warns on If you change it to If we're defining a new API, we should try to design it so that it doesn't run into these issues. |
using System.ComponentModel;
public sealed partial class TypeDescriptor
{
// The main registration API that will cause the linker\trimmer to preserve metadata\members for reflection.
// Instead of .All, we may use more narrow values (public properties, fields, events, members, constructors)
public static void RegisterType<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>();
public static AttributeCollection GetAttributesFromRegisteredType(Type componentType);
public static TypeConverter? GetConverterFromRegisteredType(object component);
public static TypeConverter? GetConverterFromRegisteredType(Type type);
public static EventDescriptorCollection GetEventsFromRegisteredType(Type componentType);
public static PropertyDescriptorCollection GetPropertiesFromRegisteredType(Type componentType);
public static PropertyDescriptorCollection GetPropertiesFromRegisteredType(object component);
}
// Uses Default Interface Methods to add new members.
// Alternative approach is to add 'ICustomTypeDescriptorForRegisteredType : ICustomTypeDescriptor'
public partial interface ICustomTypeDescriptor
{
AttributeCollection GetAttributesFromRegisteredType() => DIM;
TypeConverter? GetConverterFromRegisteredType() => DIM;
EventDescriptorCollection GetEventsFromRegisteredType() => DIM;
PropertyDescriptorCollection GetPropertiesFromRegisteredType() => DIM;
// Defaults to 'null' which then the framework defers to the new feature switch as to validate or not.
public virtual bool? RequireRegisteredTypes => null;
// The overloads that take attributes were purposely omitted for now.
}
public abstract class CustomTypeDescriptor : ICustomTypeDescriptor
{
public virtual AttributeCollection GetAttributesFromRegisteredType();
public virtual TypeConverter? GetConverterFromRegisteredType();
public virtual EventDescriptorCollection GetEventsFromRegisteredType();
public virtual PropertyDescriptorCollection GetPropertiesFromRegisteredType();
// Defaults to 'null' which then the framework defers to possible new feature switch as to validate or not.
public virtual bool? RequireRegisteredTypes => null;
}
public abstract class PropertyDescriptor : MemberDescriptor
{
public virtual TypeConverter ConverterFromRegisteredType { get; }
}
public abstract class TypeDescriptionProvider
{
public virtual void RegisterType<T>();
public virtual ICustomTypeDescriptor GetExtendedTypeDescriptorFromRegisteredType(object instance);
public ICustomTypeDescriptor? GetTypeDescriptorFromRegisteredType(object instance);
public virtual bool IsRegisteredType(System.Type type);
public virtual bool? RequireRegisteredTypes => null;
}
public class ComponentResourceManager
{
public virtual void ApplyResourceToRegisteredType(object value, string objectName, CultureInfo? culture);
} |
Background and motivation
This feature updates the
System.ComponentModel.TypeConverter
assembly to make it trimmer compatible so that it can be used by WinForm apps in NativeAot. This feature is primarily focused on improving correctness of trimming, and not necessarily to reduce size. There will likely be optimizations later for size once correctness is achieved.This area of ComponentModel primarily is the TypeDescriptor which is used to obtain the properties, events and attributes for classes. It has various "provider" extensibility points as an alternative to the default provider which is based on reflection. However, this framework was designed well before any trimming-related scenarios existed and thus today is not safely trimmable.
The need to avoid RUC in all cases, and DAM for most cases
The TypeConverter assembly has been annotated with trimming attributes over the last couple of releases, but depends heavily on the unactionable
[RequiresUnreferencedCode]
(or RUC) which essentially means the code is not safely trimmable.The goal here is to avoid RUC and instead depend only on
[DynamicallyAccessedMembers]
(or DAM) which the linker understands. Unlike CoreClr, NativeAot does not necessarily support reflection on members even when the physical methods exist (after the linker is run due to code references that calls them); the proper use of DAM is necessary for the linker and NativeAot to preserve member metadata necessary for reflection introspection (e.g. obtaining the list of properties) and to add the reflection stubs which are necessary for invoke (e.g. calling a getter\setter).RUC has been used here because the existing APIs do not always have a
Type
- they often takeobject
, which is an issue for the linker since it can only rationalize on a staticType
throughtypeof
or generic<T>
parameters. The cases that takeobject
are not linker-compatible.Some existing methods take
Type
(e.g.TypeDescriptor.GetProperties(Type)
), but DAM is not always desired on these methods since they are often called several levels deep in a call stack, and not during initialization, for example, where the staticType
is known. Callers of such DAM-based APIs would get linker warnings since theType
parameter would not be passed statically and thus the warning would need to be suppressed. Suppressing these can cause run-time issues, such as silently not showing any properties for a given Type.Adding a registration API with DAM along with run-time checks to enforce
The proposal adds new APIs that mirror existing APIs but end with "RegisteredType" and where these new APIs will not have DAM.
In order to tell the linker to preserve the metadata\members for reflection, the proposal adds APIs to "register" a type where the API does have DAM. This is intended to be called during initialization, such as in the designer-generated
InitializeComponent()
.Run-time validation is also added to these new APIs that do not have DAM to verify they are only called with the "registered" types. This validation prevents incorrect results; for example, missing properties from
TypeDescriptor.GetPropertiesFromRegisteredType(Type)
.API Proposal
These may use the
[Experimental]
attribute pending discussion.API Usage
Validation Modes
The new "FromRegisteredType" APIs will validate the type is registered, at least for the reflection provider which requires the DAM attribute via the registration API. Registering a Type also registers base classes, but does not register referenced types such as from properties.
A type provider can have a parent; if a "RegisteredType" API is called on a provider with a parent, the request goes to the parent. The parent provider, if a custom provider, may or may not have implemented this (our shipping providers will). If the provider is unconfigured (
SupportsRegisteredTypes
returnsfalse
) we will assume the provider does not use reflection and will forward to the legacy API instead. This helps people to migrate to the new APIs (if they want to avoid trimmer suppressions) but not worry about whether the provider is configured or not to use registered types. It is valid to register a type even though the provider may ignore it.Existing APIs will have no changes. Various RUC\DAM etc overrides would be suppressed by caller. Most likely this will not work with trimming (as today).
Pending prototyping and the first set of functionality, we may want to enforce that unconfigured providers (that return
null
fromRequiresRegisteredType
), which have the "RegisteredType" members called on them, are validated at runtime. To do this, we may add a feature switch "System.ComponentModel.TypeDescriptor.RequireRegisteredTypes". When this switch is enabled, there will additional validation that will ensure a trimmed application behaves as expected. It is expected that WinForms enables this feature switch. We may also decide to disable certain APIs if they are incompatible with trimming (have RUC or require DAM for low-level methods) and if the new APIs can be used instead.Alternative Designs
Remove existing RUC\DAM
RegisterType
andRequireRegisteredTypes
members.Remove existing RUC\DAM if feature switch is on
Same as the alternative above, but for (3) add a property to the RUC\DAM attributes that specifies a feature switch as a string. The linker will ignore these annotations if the feature switch is on.
This avoids the breaking change and the parallel APIs. However, it makes a non-trivial application with external dependencies more difficult to migrate since code that is not owned, and still uses unregistered types, will no longer get linker warnings for RUC\DAM and thus difficult to determine what types need to be registered.
Risks
No response
The text was updated successfully, but these errors were encountered: