Browse Source

CompiledBinding.Create must be AOT-compatible, even by using reflection (#20776)

* Implement safe TypeCast expression handling

* Add reflection based (still AOT safe) Path.StreamTask and Path.StreamObservable

* Remove RequiresDynamicCode, as it's no longer true

* Stream bindings are not supported

* ObservableStreamPlugin.MatchesType should handle if type itself is observable

* Fix Cecil not finding generic methods
pull/14481/merge
Max Katz 3 weeks ago
committed by GitHub
parent
commit
89b7d3aaef
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 6
      src/Avalonia.Base/Data/CompiledBinding.cs
  2. 60
      src/Avalonia.Base/Data/CompiledBindingPath.cs
  3. 26
      src/Avalonia.Base/Data/Core/Parsers/BindingExpressionVisitor.cs
  4. 13
      src/Avalonia.Base/Data/Core/Plugins/ObservableStreamPlugin.cs
  5. 11
      src/Avalonia.Base/Data/Core/Plugins/TaskStreamPlugin.cs
  6. 6
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlBindingPathHelper.cs

6
src/Avalonia.Base/Data/CompiledBinding.cs

@ -28,7 +28,7 @@ public class CompiledBinding : BindingBase
/// </summary> /// </summary>
/// <param name="path">The binding path.</param> /// <param name="path">The binding path.</param>
public CompiledBinding(CompiledBindingPath path) => Path = path; public CompiledBinding(CompiledBindingPath path) => Path = path;
/// <summary> /// <summary>
/// Creates a <see cref="CompiledBinding"/> from a lambda expression. /// Creates a <see cref="CompiledBinding"/> from a lambda expression.
/// </summary> /// </summary>
@ -81,12 +81,10 @@ public class CompiledBinding : BindingBase
/// <item>Indexers: <c>x => x.Items[0]</c></item> /// <item>Indexers: <c>x => x.Items[0]</c></item>
/// <item>Type casts: <c>x => ((DerivedType)x).Property</c></item> /// <item>Type casts: <c>x => ((DerivedType)x).Property</c></item>
/// <item>Logical NOT: <c>x => !x.BoolProperty</c></item> /// <item>Logical NOT: <c>x => !x.BoolProperty</c></item>
/// <item>Stream bindings: <c>x => x.TaskProperty</c> (Task/Observable)</item>
/// <item>AvaloniaProperty access: <c>x => x[MyProperty]</c></item> /// <item>AvaloniaProperty access: <c>x => x[MyProperty]</c></item>
/// </list> /// </list>
/// </remarks> /// </remarks>
[RequiresDynamicCode(TrimmingMessages.ExpressionNodeRequiresDynamicCodeMessage)] [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Expression statically preserves members used in binding expressions.")]
[RequiresUnreferencedCode(TrimmingMessages.ExpressionNodeRequiresUnreferencedCodeMessage)]
public static CompiledBinding Create<TIn, TOut>( public static CompiledBinding Create<TIn, TOut>(
Expression<Func<TIn, TOut>> expression, Expression<Func<TIn, TOut>> expression,
object? source = null, object? source = null,

60
src/Avalonia.Base/Data/CompiledBindingPath.cs

@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Reflection; using System.Reflection;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Data.Core; using Avalonia.Data.Core;
@ -155,12 +156,26 @@ namespace Avalonia.Data
return this; return this;
} }
[RequiresUnreferencedCode(TrimmingMessages.StreamPluginRequiresUnreferencedCodeMessage)]
public CompiledBindingPathBuilder StreamTask()
{
_elements.Add(new TaskStreamPathElement());
return this;
}
public CompiledBindingPathBuilder StreamObservable<T>() public CompiledBindingPathBuilder StreamObservable<T>()
{ {
_elements.Add(new ObservableStreamPathElement<T>()); _elements.Add(new ObservableStreamPathElement<T>());
return this; return this;
} }
[RequiresUnreferencedCode(TrimmingMessages.StreamPluginRequiresUnreferencedCodeMessage)]
public CompiledBindingPathBuilder StreamObservable()
{
_elements.Add(new ObservableStreamPathElement());
return this;
}
public CompiledBindingPathBuilder Self() public CompiledBindingPathBuilder Self()
{ {
_elements.Add(new SelfPathElement()); _elements.Add(new SelfPathElement());
@ -197,6 +212,12 @@ namespace Avalonia.Data
return this; return this;
} }
public CompiledBindingPathBuilder TypeCast(Type targetType)
{
_elements.Add(new TypeCastPathElement(targetType));
return this;
}
public CompiledBindingPathBuilder TemplatedParent() public CompiledBindingPathBuilder TemplatedParent()
{ {
_elements.Add(new TemplatedParentPathElement()); _elements.Add(new TemplatedParentPathElement());
@ -299,6 +320,14 @@ namespace Avalonia.Data
public IStreamPlugin CreatePlugin() => new TaskStreamPlugin<T>(); public IStreamPlugin CreatePlugin() => new TaskStreamPlugin<T>();
} }
[RequiresUnreferencedCode(TrimmingMessages.StreamPluginRequiresUnreferencedCodeMessage)]
internal class TaskStreamPathElement : IStronglyTypedStreamElement
{
public static readonly TaskStreamPathElement Instance = new TaskStreamPathElement();
public IStreamPlugin CreatePlugin() => new TaskStreamPlugin();
}
internal class ObservableStreamPathElement<T> : IStronglyTypedStreamElement internal class ObservableStreamPathElement<T> : IStronglyTypedStreamElement
{ {
public static readonly ObservableStreamPathElement<T> Instance = new ObservableStreamPathElement<T>(); public static readonly ObservableStreamPathElement<T> Instance = new ObservableStreamPathElement<T>();
@ -306,6 +335,14 @@ namespace Avalonia.Data
public IStreamPlugin CreatePlugin() => new ObservableStreamPlugin<T>(); public IStreamPlugin CreatePlugin() => new ObservableStreamPlugin<T>();
} }
[RequiresUnreferencedCode(TrimmingMessages.StreamPluginRequiresUnreferencedCodeMessage)]
internal class ObservableStreamPathElement : IStronglyTypedStreamElement
{
public static readonly ObservableStreamPathElement Instance = new ObservableStreamPathElement();
public IStreamPlugin CreatePlugin() => new ObservableStreamPlugin();
}
internal class SelfPathElement : ICompiledBindingPathElement, IControlSourceBindingPathElement internal class SelfPathElement : ICompiledBindingPathElement, IControlSourceBindingPathElement
{ {
public static readonly SelfPathElement Instance = new SelfPathElement(); public static readonly SelfPathElement Instance = new SelfPathElement();
@ -387,7 +424,28 @@ namespace Avalonia.Data
public Type Type => typeof(T); public Type Type => typeof(T);
public Func<object?, object?> Cast => TryCast; public Func<object?, object?> Cast { get; } = TryCast;
public override string ToString()
=> $"({Type.FullName})";
}
internal class TypeCastPathElement : ITypeCastElement
{
public TypeCastPathElement(Type type)
{
Type = type;
Cast = obj =>
{
if (obj is { } result && type.IsInstanceOfType(result))
return result;
return null;
};
}
public Type Type { get; }
public Func<object?, object?> Cast { get; }
public override string ToString() public override string ToString()
=> $"({Type.FullName})"; => $"({Type.FullName})";

26
src/Avalonia.Base/Data/Core/Parsers/BindingExpressionVisitor.cs

@ -1,5 +1,4 @@
using System; using System;
using System.Collections.Generic;
using System.Collections.Specialized; using System.Collections.Specialized;
using System.ComponentModel; using System.ComponentModel;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
@ -8,9 +7,7 @@ using System.Linq.Expressions;
using System.Reflection; using System.Reflection;
using System.Threading.Tasks; using System.Threading.Tasks;
using Avalonia.Data.Core.ExpressionNodes; using Avalonia.Data.Core.ExpressionNodes;
using Avalonia.Data.Core.ExpressionNodes.Reflection;
using Avalonia.Data.Core.Plugins; using Avalonia.Data.Core.Plugins;
using Avalonia.Reactive;
using Avalonia.Utilities; using Avalonia.Utilities;
namespace Avalonia.Data.Core.Parsers; namespace Avalonia.Data.Core.Parsers;
@ -25,7 +22,6 @@ namespace Avalonia.Data.Core.Parsers;
/// can then be converted into <see cref="ExpressionNode"/> instances. It supports property access, /// can then be converted into <see cref="ExpressionNode"/> instances. It supports property access,
/// indexers, AvaloniaProperty access, stream bindings, type casts, and logical operators. /// indexers, AvaloniaProperty access, stream bindings, type casts, and logical operators.
/// </remarks> /// </remarks>
[RequiresDynamicCode(TrimmingMessages.ExpressionNodeRequiresDynamicCodeMessage)]
[RequiresUnreferencedCode(TrimmingMessages.ExpressionNodeRequiresUnreferencedCodeMessage)] [RequiresUnreferencedCode(TrimmingMessages.ExpressionNodeRequiresUnreferencedCodeMessage)]
internal class BindingExpressionVisitor<TIn>(LambdaExpression expression) : ExpressionVisitor internal class BindingExpressionVisitor<TIn>(LambdaExpression expression) : ExpressionVisitor
{ {
@ -149,17 +145,11 @@ internal class BindingExpressionVisitor<TIn>(LambdaExpression expression) : Expr
instanceType.GetGenericTypeDefinition() == typeof(Task<>) && instanceType.GetGenericTypeDefinition() == typeof(Task<>) &&
genericArg.IsAssignableFrom(instanceType.GetGenericArguments()[0]))) genericArg.IsAssignableFrom(instanceType.GetGenericArguments()[0])))
{ {
var builderMethod = typeof(CompiledBindingPathBuilder) return Add(instance, node, x => x.StreamTask());
.GetMethod(nameof(CompiledBindingPathBuilder.StreamTask))!
.MakeGenericMethod(genericArg);
return Add(instance, node, x => builderMethod.Invoke(x, null));
} }
else if (typeof(IObservable<>).MakeGenericType(genericArg).IsAssignableFrom(instance?.Type)) else if (instanceType is not null && ObservableStreamPlugin.MatchesType(instanceType))
{ {
var builderMethod = typeof(CompiledBindingPathBuilder) return Add(instance, node, x => x.StreamObservable());
.GetMethod(nameof(CompiledBindingPathBuilder.StreamObservable))!
.MakeGenericMethod(genericArg);
return Add(instance, node, x => builderMethod.Invoke(x, null));
} }
} }
else if (method == BindingExpressionVisitorMembers.CreateDelegateMethod) else if (method == BindingExpressionVisitorMembers.CreateDelegateMethod)
@ -194,18 +184,12 @@ internal class BindingExpressionVisitor<TIn>(LambdaExpression expression) : Expr
if (!node.Type.IsValueType && !node.Operand.Type.IsValueType && if (!node.Type.IsValueType && !node.Operand.Type.IsValueType &&
(node.Type.IsAssignableFrom(node.Operand.Type) || node.Operand.Type.IsAssignableFrom(node.Type))) (node.Type.IsAssignableFrom(node.Operand.Type) || node.Operand.Type.IsAssignableFrom(node.Type)))
{ {
var castMethod = typeof(CompiledBindingPathBuilder) return Add(node.Operand, node, x => x.TypeCast(node.Type));
.GetMethod(nameof(CompiledBindingPathBuilder.TypeCast))!
.MakeGenericMethod(node.Type);
return Add(node.Operand, node, x => castMethod.Invoke(x, null));
} }
} }
else if (node.NodeType == ExpressionType.TypeAs) else if (node.NodeType == ExpressionType.TypeAs)
{ {
var castMethod = typeof(CompiledBindingPathBuilder) return Add(node.Operand, node, x => x.TypeCast(node.Type));
.GetMethod(nameof(CompiledBindingPathBuilder.TypeCast))!
.MakeGenericMethod(node.Type);
return Add(node.Operand, node, x => castMethod.Invoke(x, null));
} }
throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}."); throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}.");

13
src/Avalonia.Base/Data/Core/Plugins/ObservableStreamPlugin.cs

@ -31,7 +31,18 @@ namespace Avalonia.Data.Core.Plugins
{ {
reference.TryGetTarget(out var target); reference.TryGetTarget(out var target);
return target != null && target.GetType().GetInterfaces().Any(x => return target != null && MatchesType(target.GetType());
}
public static bool MatchesType(Type type)
{
var interfaces = type.GetInterfaces().AsEnumerable();
if (type.IsInterface)
{
interfaces = interfaces.Concat([type]);
}
return interfaces.Any(x =>
x.IsGenericType && x.IsGenericType &&
x.GetGenericTypeDefinition() == typeof(IObservable<>)); x.GetGenericTypeDefinition() == typeof(IObservable<>));
} }

11
src/Avalonia.Base/Data/Core/Plugins/TaskStreamPlugin.cs

@ -61,10 +61,9 @@ namespace Avalonia.Data.Core.Plugins
return Observable.Empty<object?>(); return Observable.Empty<object?>();
} }
[RequiresUnreferencedCode(TrimmingMessages.StreamPluginRequiresUnreferencedCodeMessage)]
private static IObservable<object?> HandleCompleted(Task task) private static IObservable<object?> HandleCompleted(Task task)
{ {
var resultProperty = task.GetType().GetRuntimeProperty("Result"); var resultProperty = GetTaskResult(task);
if (resultProperty != null) if (resultProperty != null)
{ {
@ -80,6 +79,14 @@ namespace Avalonia.Data.Core.Plugins
} }
return Observable.Empty<object>(); return Observable.Empty<object>();
[DynamicDependency("Result", typeof(Task<>))]
[UnconditionalSuppressMessage("Trimming", "IL2070")]
[UnconditionalSuppressMessage("Trimming", "IL2075")]
PropertyInfo? GetTaskResult(Task obj)
{
return obj.GetType().GetProperty("Result");
}
} }
} }
} }

6
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlBindingPathHelper.cs

@ -561,7 +561,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
public void Emit(XamlIlEmitContext context, IXamlILEmitter codeGen) public void Emit(XamlIlEmitContext context, IXamlILEmitter codeGen)
{ {
codeGen.EmitCall(context.GetAvaloniaTypes().CompiledBindingPathBuilder.GetMethod(m => m.Name == "StreamObservable").MakeGenericMethod(new[] { Type })); codeGen.EmitCall(context.GetAvaloniaTypes().CompiledBindingPathBuilder.GetMethod(m => m is { Name: "StreamObservable", IsGenericMethod: true }).MakeGenericMethod(new[] { Type }));
} }
} }
@ -576,7 +576,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
public void Emit(XamlIlEmitContext context, IXamlILEmitter codeGen) public void Emit(XamlIlEmitContext context, IXamlILEmitter codeGen)
{ {
codeGen.EmitCall(context.GetAvaloniaTypes().CompiledBindingPathBuilder.GetMethod(m => m.Name == "StreamTask").MakeGenericMethod(new[] { Type })); codeGen.EmitCall(context.GetAvaloniaTypes().CompiledBindingPathBuilder.GetMethod(m => m is { Name: "StreamTask", IsGenericMethod: true }).MakeGenericMethod(new[] { Type }));
} }
} }
@ -980,7 +980,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
public void Emit(XamlIlEmitContext context, IXamlILEmitter codeGen) public void Emit(XamlIlEmitContext context, IXamlILEmitter codeGen)
{ {
codeGen.EmitCall(context.GetAvaloniaTypes().CompiledBindingPathBuilder.GetMethod(m => m.Name == "TypeCast").MakeGenericMethod(new[] { Type })); codeGen.EmitCall(context.GetAvaloniaTypes().CompiledBindingPathBuilder.GetMethod(m => m is { Name: "TypeCast", IsGenericMethod: true }).MakeGenericMethod(new[] { Type }));
} }
} }

Loading…
Cancel
Save