From 559b1f7866028a1d23f74ba43c3e8614a8e9f284 Mon Sep 17 00:00:00 2001 From: Jonathan Pryor Date: Wed, 3 Dec 2025 13:22:58 -0500 Subject: [PATCH] chore: BindableType preserves properties and more MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Context: https://github.com/unoplatform/uno.chefs/pull/1712 Context: https://github.com/unoplatform/uno/pull/21920 Context: https://github.com/unoplatform/uno.extensions/pull/2966 Context: https://github.com/unoplatform/uno.extensions/pull/2969 Context: https://discord.com/channels/732297728826277939/732297837953679412/1444909702512119899 While running the uno.chefs app on macOS under NativeAOT: dotnet publish -c Release -r osx-x64 -f net10.0-desktop -p:TargetFrameworkOverride=net10.0-desktop -bl \ Chefs/Chefs.csproj \ -p:SelfContained=true -p:PublishAot=true -p:IsAotCompatible=true -p:UseSkiaRendering=true \ -p:IlcGenerateMapFile=true -p:IlcGenerateMstatFile=true -p:IlcGenerateDgmlFile=true \ -p:EmitCompilerGeneratedFiles=true -p:CompilerGeneratedFilesOutputPath=`pwd`/_gen Chefs/bin/Release/net10.0-desktop/osx-x64/publish/Chefs Console output would contain the following errors: fail: Uno.UI.Dispatching.NativeDispatcher[0] The [TabNavigation] property getter does not exist on type [Microsoft.UI.Xaml.Controls.FlipView] fail: Uno.UI.Dispatching.NativeDispatcher[0] The [Y] property getter does not exist on type [Windows.Foundation.Point] fail: Uno.UI.Dispatching.NativeDispatcher[0] The [X] property getter does not exist on type [Windows.Foundation.Point] At this point, a description of the relation between NativeAOT and Reflection would be useful. *Parts* of System.Reflection can be used. Which parts *can* be used, and how to use them, is oddly *not* well specified in the [Native AOT deployment][0] documentation. What doesn't work is outlined in the [limitations][1] section: > * No dynamic loading, for example, `Assembly.LoadFile`. > * No run-time code generation, for example, `System.Reflection.Emit`. What *always* appears to work is: * `Type.GetType()` returns a `Type` instance. What ***can*** work is: * `Type.GetType(string)`, *so long as* an assembly-qualified name is used as a *string constant* // bad string name = GetSomeTypeName(); Type type = Type.GetType(name); // good Type type = Type.GetType("My.Example.TypeName, ExampleAssembly"); * `Type.GetMethod(string)`, but see fine print below. * `Type.GetProperty(string)`, but see fine print below. * `MethodInfo.Invoke()` * … **The Fine Print**: in order for many Reflection-based APIs such as `Type.GetMethod(string)` to work, the program must contain "reflection metadata." We can now describe, briefly, what NativeAOT does: 1. Accept IL/assemblies as input 2. Internally trim the IL, optimizes them, etc. 3. Generates a native binary containing two separate bits of data: * Native machine code for execution * "reflection metadata" You can get an inkling of what methods survived trimming and become native code by using `dotnet publish -p:IlcGenerateMstatFile=true …`, which will produce a `.mstat` file, a PE file which references every type and member which is in the native binary, ***after*** inlining. (Meaning if a member is inlined, it *won't* be present!) Use e.g. `monodis --memberref App.mstat` to list post-trimmed members. You can get an inkling of the "reflection metadata" that is present by using `dotnet publish -p:IlcGenerateMetadataLog=true …`, which produces a `.metadata.csv` file. It's not particularly scrutible, but can be used to verify observed behavior. Consider one of the above failure messages: The [TabNavigation] property getter does not exist on type [Microsoft.UI.Xaml.Controls.FlipView] An obvious question to ask: is the `TabNavigation` property in the reflection metadata? Search for `"TabNavigation"` (with quotes!), and it's *not* in there. Compare to when this commit is in use, and: 66087019, Property, "TabNavigation", "3410be9a 6810bea8 5410beae 5410beb2" 3410be9a, ConstantStringValue, "TabNavigation", "" The 3rd column is the property name `TabNavigation`, while the 2nd column is the type; for properties, we want `Property`. The 4th column is "Children", one of which is: 5410beae, MethodSemantics, "Getter : [HasThis] Microsoft.UI.Xaml.Input.KeyboardNavigationMode get_TabNavigation()", "50086c65" which at least verifies that `TabNavigation` is in play. Programs can control what is contained within reflection metadata by using string constants with Reflection APIs, for example: typeof(SomeKnownType).GetMethod("ConstantName") will ensure that reflection metadata contains `SomeKnownType.ConstantName()`, and `ConstantName()` will be returned from `typeof(SomeKnownType).GetMethods()` and can be invoked with `MethodInfo.Invoke()`. If you *don't* use `typeof(SomeKnownType)` or string constants with `Type.GetType(string)` or `type.GetMethod("constant")`, then you need to "suggest" that some pieces of information be added to reflection metadata. The two predominant ways to do this are via the custom attributes: * [`DynamicallyAccessedMembersAttribute`][2] * [`DynamicDependencyAttribute`][3] From the Uno perspective, enter `BindingPropertyHelper`: *everything* is based on Reflection! Fortunately NativeAOT "supports" Reflection! (See above.) *Unfortunately*, we now need to convince NativeAOT to store the reflection metadata Uno requires in order to work! Various other PRs have dealt with some of this, such as #21920. Returning to the original failure messages, e.g. fail: Uno.UI.Dispatching.NativeDispatcher[0] The [TabNavigation] property getter does not exist on type [Microsoft.UI.Xaml.Controls.FlipView] During the app build, `src/SourceGenerators/Uno.UI.SourceGenerators` generates a `BindableMetadata.g.cs` file which will mention `[Bindable]` types, e.g. for `Microsoft.UI.Xaml.Controls.FlipView`: /// /// Builder for Microsoft.UI.Xaml.Controls.FlipView /// [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] static class MetadataBuilder_239 { internal static global::Uno.UI.DataBinding.IBindableType Build(global::Uno.UI.DataBinding.BindableType parent) { var bindableType = parent ?? new global::Uno.UI.DataBinding.BindableType(151, typeof(global::Microsoft.UI.Xaml.Controls.FlipView)); // … } private static object CreateInstance() => new global::Microsoft.UI.Xaml.Controls.FlipView(); } `BindableMetadata.g.cs` was needed in the ye olde Xamarin world order to tell *its* linker to e.g. "preserve the `FlipView` default ctor!" *Now*, we can build upon this infrastructure and update `BindableType` to request that Reflection information regarding properties, fields, and constructors be retained. We can do this by updating the `BindableType` constructor: partial class BindableType { internal const DynamicallyAccessedMemberTypes TypeRequirements = DynamicallyAccessedMemberTypes.PublicProperties | …; public BindableType(int estimatedPropertySize, [DynamicallyAccessedMembers(TypeRequirements)] Type sourceType) => … } *Because* we have `BindableMetadata.g.cs` *and* because it provides `typeof(FlipView)`, this directs NativeAOT to make property and other reflection metadata available to the app, *fixing* the error messages. Expand this addition of `[DynamicallyAccessedMembers]` to also include `BindableProperty` and related types. Note: the IL2111 warning "Method 'System.Type.TypeInitializer.get' …" is *caused by* `typeof(Type)` (?!), which is particularly odd given that we don't use `Type.TypeInitializer`! Thanks to Alexander Köplinger for [looking into this][4]. [0]: https://learn.microsoft.comdotnet/core/deploying/native-aot [1]: https://learn.microsoft.comdotnet/core/deploying/native-aot/#limitations-of-native-aot-deployment [2]: https://learn.microsoft.comdotnet/api/system.diagnostics.codeanalysis.dynamicallyaccessedmembersattribute?view=net-10.0 [3]: https://learn.microsoft.comdotnet/api/system.diagnostics.codeanalysis.dynamicdependencyattribute?view=net-10.0 [4]: https://discord.com/channels/732297728826277939/732297837953679412/1445773677152174150 --- .../BindableTypeProvidersGenerationTask.cs | 1 + src/Uno.UI/DataBinding/BindableProperty.cs | 8 +++++- src/Uno.UI/DataBinding/BindableType.cs | 25 +++++++++++++++--- src/Uno.UI/DataBinding/IBindableProperty.cs | 2 ++ src/Uno.UI/DataBinding/IBindableType.cs | 2 ++ .../Xaml/Controls/Frame/Frame.Properties.cs | 5 ++++ src/Uno.UI/UI/Xaml/DependencyProperty.cs | 26 ++++++++++++++----- .../Navigation/PageStackEntry.Properties.cs | 3 ++- 8 files changed, 60 insertions(+), 12 deletions(-) diff --git a/src/SourceGenerators/Uno.UI.SourceGenerators/BindableTypeProviders/BindableTypeProvidersGenerationTask.cs b/src/SourceGenerators/Uno.UI.SourceGenerators/BindableTypeProviders/BindableTypeProvidersGenerationTask.cs index 51ad7abef74f..12986efd5d3a 100644 --- a/src/SourceGenerators/Uno.UI.SourceGenerators/BindableTypeProviders/BindableTypeProvidersGenerationTask.cs +++ b/src/SourceGenerators/Uno.UI.SourceGenerators/BindableTypeProviders/BindableTypeProvidersGenerationTask.cs @@ -302,6 +302,7 @@ where field.IsStatic writer.AppendLineIndented("[global::System.Diagnostics.CodeAnalysis.SuppressMessage(\"Microsoft.Maintainability\", \"CA1502:AvoidExcessiveComplexity\", Justification=\"Must be ignored even if generated code is checked.\")]"); writer.AppendLineIndented("[global::System.Diagnostics.CodeAnalysis.SuppressMessage(\"Microsoft.Maintainability\", \"CA1506:AvoidExcessiveClassCoupling\", Justification = \"Must be ignored even if generated code is checked.\")]"); writer.AppendLineIndented("[global::System.Diagnostics.CodeAnalysis.SuppressMessage(\"Microsoft.Maintainability\", \"CA1505:AvoidUnmaintainableCode\", Justification = \"Must be ignored even if generated code is checked.\")]"); + writer.AppendLineIndented("[global::System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage(\"Trimming\", \"IL2111\", Justification = \"`typeof(Type)` emits IL2111 because of `Type.TypeInitializer`, which is not used.\")]"); using (writer.BlockInvariant("internal static global::Uno.UI.DataBinding.IBindableType Build(global::Uno.UI.DataBinding.BindableType parent)")) { RegisterHintMethod($"MetadataBuilder_{typeInfo.Index:000}", ownerType, "Uno.UI.DataBinding.IBindableType Build(Uno.UI.DataBinding.BindableType)"); diff --git a/src/Uno.UI/DataBinding/BindableProperty.cs b/src/Uno.UI/DataBinding/BindableProperty.cs index 852e7ec0a8b7..373079d0f8f9 100644 --- a/src/Uno.UI/DataBinding/BindableProperty.cs +++ b/src/Uno.UI/DataBinding/BindableProperty.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Text; using System.Threading.Tasks; @@ -24,7 +25,11 @@ public BindableProperty(DependencyProperty property) /// /// This ctor is available for backward compatibility. On newer versions of Uno.UI, the BindableTypeProvidersSourceGenerator uses the single-parameter ctor /// - public BindableProperty(Type propertyType, PropertyGetterHandler getter, PropertySetterHandler? setter) + public BindableProperty( + [DynamicallyAccessedMembers(BindableType.TypeRequirements)] + Type propertyType, + PropertyGetterHandler getter, + PropertySetterHandler? setter) { Getter = getter; Setter = setter; @@ -35,6 +40,7 @@ public BindableProperty(Type propertyType, PropertyGetterHandler getter, Propert public PropertySetterHandler? Setter { get; } + [DynamicallyAccessedMembers(BindableType.TypeRequirements)] public Type PropertyType { get; } public DependencyProperty? DependencyProperty { get; } diff --git a/src/Uno.UI/DataBinding/BindableType.cs b/src/Uno.UI/DataBinding/BindableType.cs index e5f362d4fdc7..635d94bfd4ca 100644 --- a/src/Uno.UI/DataBinding/BindableType.cs +++ b/src/Uno.UI/DataBinding/BindableType.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Text; using System.Threading.Tasks; @@ -16,6 +17,13 @@ namespace Uno.UI.DataBinding /// public class BindableType : IBindableType { + internal const DynamicallyAccessedMemberTypes TypeRequirements = + DynamicallyAccessedMemberTypes.NonPublicConstructors // DependencyProperty.Register() + | DynamicallyAccessedMemberTypes.PublicConstructors + | DynamicallyAccessedMemberTypes.PublicFields + | DynamicallyAccessedMemberTypes.PublicProperties + ; + private readonly Hashtable _properties; private StringIndexerGetterDelegate? _stringIndexerGetter; private StringIndexerSetterDelegate? _stringIndexerSetter; @@ -26,12 +34,16 @@ public class BindableType : IBindableType /// /// Provide an estimated number of properties, so the dictionary does not need to grow unnecessarily. /// The actual .NET type that corresponds to this instance. - public BindableType(int estimatedPropertySize, Type sourceType) + public BindableType( + int estimatedPropertySize, + [DynamicallyAccessedMembers(BindableType.TypeRequirements)] + Type sourceType) { _properties = new Hashtable(estimatedPropertySize); Type = sourceType; } + [DynamicallyAccessedMembers(BindableType.TypeRequirements)] public Type Type { get; } public ActivatorDelegate? CreateInstance() @@ -61,7 +73,9 @@ public void AddActivator(ActivatorDelegate activator) _activator = activator; } - public void AddProperty(string name, PropertyGetterHandler getter, PropertySetterHandler? setter = null) + public void AddProperty< + [DynamicallyAccessedMembers(TypeRequirements)] T + >(string name, PropertyGetterHandler getter, PropertySetterHandler? setter = null) { _properties[name] = new BindableProperty(typeof(T), getter, setter); } @@ -71,7 +85,12 @@ public void AddProperty(DependencyProperty property) _properties[property.Name] = new BindableProperty(property); } - public void AddProperty(string name, Type propertyType, PropertyGetterHandler getter, PropertySetterHandler? setter = null) + public void AddProperty( + string name, + [DynamicallyAccessedMembers(TypeRequirements)] + Type propertyType, + PropertyGetterHandler getter, + PropertySetterHandler? setter = null) { _properties[name] = new BindableProperty(propertyType, getter, setter); } diff --git a/src/Uno.UI/DataBinding/IBindableProperty.cs b/src/Uno.UI/DataBinding/IBindableProperty.cs index e5c730e1752e..4172052a0292 100644 --- a/src/Uno.UI/DataBinding/IBindableProperty.cs +++ b/src/Uno.UI/DataBinding/IBindableProperty.cs @@ -3,6 +3,7 @@ using Microsoft.UI.Xaml; using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Text; @@ -26,6 +27,7 @@ public interface IBindableProperty /// /// Gets the type of the property /// + [DynamicallyAccessedMembers(BindableType.TypeRequirements)] Type PropertyType { get; } /// diff --git a/src/Uno.UI/DataBinding/IBindableType.cs b/src/Uno.UI/DataBinding/IBindableType.cs index 5cb44c7b8788..0c6e07ee837e 100644 --- a/src/Uno.UI/DataBinding/IBindableType.cs +++ b/src/Uno.UI/DataBinding/IBindableType.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Text; using System.Threading.Tasks; @@ -31,6 +32,7 @@ public interface IBindableType /// /// Provides the Type of this bindable type /// + [DynamicallyAccessedMembers(BindableType.TypeRequirements)] Type Type { get; } /// diff --git a/src/Uno.UI/UI/Xaml/Controls/Frame/Frame.Properties.cs b/src/Uno.UI/UI/Xaml/Controls/Frame/Frame.Properties.cs index a918ed8d6e72..9797c4d97c92 100644 --- a/src/Uno.UI/UI/Xaml/Controls/Frame/Frame.Properties.cs +++ b/src/Uno.UI/UI/Xaml/Controls/Frame/Frame.Properties.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using Microsoft.UI.Xaml.Navigation; namespace Microsoft.UI.Xaml.Controls; @@ -117,6 +118,8 @@ public Type CurrentSourcePageType /// /// Identifies the CurrentSourcePageType dependency property. /// + [UnconditionalSuppressMessage("Trimming", "IL2111", Justification = "`typeof(Type)` triggers IL2111 regarding `Type.TypeInitializer`, but Uno doesn't use `Type.TypeInitializer`!")] + // warning IL2111: Method 'System.Type.TypeInitializer.get' with parameters or return value with `DynamicallyAccessedMembersAttribute` is accessed via reflection. Trimmer can't guarantee availability of the requirements of the method. public static DependencyProperty CurrentSourcePageTypeProperty { get; } = DependencyProperty.Register( nameof(CurrentSourcePageType), @@ -174,6 +177,8 @@ public Type SourcePageType /// /// Identifies the SourcePageType dependency property. /// + [UnconditionalSuppressMessage("Trimming", "IL2111", Justification = "`typeof(Type)` triggers IL2111 regarding `Type.TypeInitializer`, but Uno doesn't use `Type.TypeInitializer`!")] + // warning IL2111: Method 'System.Type.TypeInitializer.get' with parameters or return value with `DynamicallyAccessedMembersAttribute` is accessed via reflection. Trimmer can't guarantee availability of the requirements of the method. public static DependencyProperty SourcePageTypeProperty { get; } = DependencyProperty.Register( nameof(SourcePageType), diff --git a/src/Uno.UI/UI/Xaml/DependencyProperty.cs b/src/Uno.UI/UI/Xaml/DependencyProperty.cs index ae69c2f44178..7fef939347b1 100644 --- a/src/Uno.UI/UI/Xaml/DependencyProperty.cs +++ b/src/Uno.UI/UI/Xaml/DependencyProperty.cs @@ -16,6 +16,7 @@ using Uno; using Uno.Extensions; using Uno.UI; +using Uno.UI.DataBinding; using Uno.UI.Dispatching; using Uno.UI.Helpers; using Uno.UI.Xaml.Media; @@ -54,13 +55,22 @@ public sealed partial class DependencyProperty private readonly Flags _flags; private string _name; + [DynamicallyAccessedMembers(BindableType.TypeRequirements)] private Type _propertyType; + [DynamicallyAccessedMembers(BindableType.TypeRequirements)] private Type _ownerType; private readonly int _uniqueId; private static int _globalId = -1; - private DependencyProperty(string name, Type propertyType, Type ownerType, PropertyMetadata defaultMetadata, bool attached) + private DependencyProperty( + string name, + [DynamicallyAccessedMembers(BindableType.TypeRequirements)] + Type propertyType, + [DynamicallyAccessedMembers(BindableType.TypeRequirements)] + Type ownerType, + PropertyMetadata defaultMetadata, + bool attached) { _name = name; _propertyType = propertyType; @@ -140,8 +150,8 @@ internal bool IsDependencyObjectCollection /// A property with the same name has already been declared for the ownerType public static DependencyProperty Register( string name, - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] Type propertyType, - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] Type ownerType, + [DynamicallyAccessedMembers(BindableType.TypeRequirements)] Type propertyType, + [DynamicallyAccessedMembers(BindableType.TypeRequirements)] Type ownerType, PropertyMetadata typeMetadata) { typeMetadata = FixMetadataIfNeeded(propertyType, typeMetadata); @@ -196,8 +206,8 @@ private static PropertyMetadata FixMetadataIfNeeded( /// internal static DependencyProperty Register( string name, - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] Type propertyType, - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] Type ownerType, + [DynamicallyAccessedMembers(BindableType.TypeRequirements)] Type propertyType, + [DynamicallyAccessedMembers(BindableType.TypeRequirements)] Type ownerType, FrameworkPropertyMetadata typeMetadata) #pragma warning disable RS0030 // Do not used banned APIs => Register(name, propertyType, ownerType, (PropertyMetadata)typeMetadata); @@ -214,8 +224,8 @@ internal static DependencyProperty Register( /// A property with the same name has already been declared for the ownerType public static DependencyProperty RegisterAttached( string name, - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] Type propertyType, - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] Type ownerType, + [DynamicallyAccessedMembers(BindableType.TypeRequirements)] Type propertyType, + [DynamicallyAccessedMembers(BindableType.TypeRequirements)] Type ownerType, PropertyMetadata defaultMetadata) { defaultMetadata = FixMetadataIfNeeded(propertyType, defaultMetadata); @@ -297,11 +307,13 @@ public PropertyMetadata GetMetadata(Type forType) return _ownerTypeMetadata.CloneWithOverwrittenDefaultValue(defaultValueForType); } + [DynamicallyAccessedMembers(BindableType.TypeRequirements)] internal Type OwnerType { get { return _ownerType; } } + [DynamicallyAccessedMembers(BindableType.TypeRequirements)] internal Type Type { get { return _propertyType; } diff --git a/src/Uno.UI/UI/Xaml/Navigation/PageStackEntry.Properties.cs b/src/Uno.UI/UI/Xaml/Navigation/PageStackEntry.Properties.cs index 5a300df148b7..65c61bb48cb7 100644 --- a/src/Uno.UI/UI/Xaml/Navigation/PageStackEntry.Properties.cs +++ b/src/Uno.UI/UI/Xaml/Navigation/PageStackEntry.Properties.cs @@ -37,7 +37,8 @@ public Type SourcePageType /// /// Identifies the SourcePageType dependency property. /// - [UnconditionalSuppressMessage("Trimming", "IL2111", Justification = "@jonpryor has no idea what the trimmer is talking about.")] + [UnconditionalSuppressMessage("Trimming", "IL2111", Justification = "`typeof(Type)` triggers IL2111 regarding `Type.TypeInitializer`, but Uno doesn't use `Type.TypeInitializer`!")] + // warning IL2111: Method 'System.Type.TypeInitializer.get' with parameters or return value with `DynamicallyAccessedMembersAttribute` is accessed via reflection. Trimmer can't guarantee availability of the requirements of the method. public static DependencyProperty SourcePageTypeProperty { get; } = DependencyProperty.Register( nameof(SourcePageType),