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. 4
      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

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

@ -81,12 +81,10 @@ public class CompiledBinding : BindingBase
/// <item>Indexers: <c>x => x.Items[0]</c></item>
/// <item>Type casts: <c>x => ((DerivedType)x).Property</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>
/// </list>
/// </remarks>
[RequiresDynamicCode(TrimmingMessages.ExpressionNodeRequiresDynamicCodeMessage)]
[RequiresUnreferencedCode(TrimmingMessages.ExpressionNodeRequiresUnreferencedCodeMessage)]
[UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Expression statically preserves members used in binding expressions.")]
public static CompiledBinding Create<TIn, TOut>(
Expression<Func<TIn, TOut>> expression,
object? source = null,

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

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using Avalonia.Controls;
using Avalonia.Data.Core;
@ -155,12 +156,26 @@ namespace Avalonia.Data
return this;
}
[RequiresUnreferencedCode(TrimmingMessages.StreamPluginRequiresUnreferencedCodeMessage)]
public CompiledBindingPathBuilder StreamTask()
{
_elements.Add(new TaskStreamPathElement());
return this;
}
public CompiledBindingPathBuilder StreamObservable<T>()
{
_elements.Add(new ObservableStreamPathElement<T>());
return this;
}
[RequiresUnreferencedCode(TrimmingMessages.StreamPluginRequiresUnreferencedCodeMessage)]
public CompiledBindingPathBuilder StreamObservable()
{
_elements.Add(new ObservableStreamPathElement());
return this;
}
public CompiledBindingPathBuilder Self()
{
_elements.Add(new SelfPathElement());
@ -197,6 +212,12 @@ namespace Avalonia.Data
return this;
}
public CompiledBindingPathBuilder TypeCast(Type targetType)
{
_elements.Add(new TypeCastPathElement(targetType));
return this;
}
public CompiledBindingPathBuilder TemplatedParent()
{
_elements.Add(new TemplatedParentPathElement());
@ -299,6 +320,14 @@ namespace Avalonia.Data
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
{
public static readonly ObservableStreamPathElement<T> Instance = new ObservableStreamPathElement<T>();
@ -306,6 +335,14 @@ namespace Avalonia.Data
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
{
public static readonly SelfPathElement Instance = new SelfPathElement();
@ -387,7 +424,28 @@ namespace Avalonia.Data
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()
=> $"({Type.FullName})";

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

@ -1,5 +1,4 @@
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
@ -8,9 +7,7 @@ using System.Linq.Expressions;
using System.Reflection;
using System.Threading.Tasks;
using Avalonia.Data.Core.ExpressionNodes;
using Avalonia.Data.Core.ExpressionNodes.Reflection;
using Avalonia.Data.Core.Plugins;
using Avalonia.Reactive;
using Avalonia.Utilities;
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,
/// indexers, AvaloniaProperty access, stream bindings, type casts, and logical operators.
/// </remarks>
[RequiresDynamicCode(TrimmingMessages.ExpressionNodeRequiresDynamicCodeMessage)]
[RequiresUnreferencedCode(TrimmingMessages.ExpressionNodeRequiresUnreferencedCodeMessage)]
internal class BindingExpressionVisitor<TIn>(LambdaExpression expression) : ExpressionVisitor
{
@ -149,17 +145,11 @@ internal class BindingExpressionVisitor<TIn>(LambdaExpression expression) : Expr
instanceType.GetGenericTypeDefinition() == typeof(Task<>) &&
genericArg.IsAssignableFrom(instanceType.GetGenericArguments()[0])))
{
var builderMethod = typeof(CompiledBindingPathBuilder)
.GetMethod(nameof(CompiledBindingPathBuilder.StreamTask))!
.MakeGenericMethod(genericArg);
return Add(instance, node, x => builderMethod.Invoke(x, null));
return Add(instance, node, x => x.StreamTask());
}
else if (typeof(IObservable<>).MakeGenericType(genericArg).IsAssignableFrom(instance?.Type))
else if (instanceType is not null && ObservableStreamPlugin.MatchesType(instanceType))
{
var builderMethod = typeof(CompiledBindingPathBuilder)
.GetMethod(nameof(CompiledBindingPathBuilder.StreamObservable))!
.MakeGenericMethod(genericArg);
return Add(instance, node, x => builderMethod.Invoke(x, null));
return Add(instance, node, x => x.StreamObservable());
}
}
else if (method == BindingExpressionVisitorMembers.CreateDelegateMethod)
@ -194,18 +184,12 @@ internal class BindingExpressionVisitor<TIn>(LambdaExpression expression) : Expr
if (!node.Type.IsValueType && !node.Operand.Type.IsValueType &&
(node.Type.IsAssignableFrom(node.Operand.Type) || node.Operand.Type.IsAssignableFrom(node.Type)))
{
var castMethod = typeof(CompiledBindingPathBuilder)
.GetMethod(nameof(CompiledBindingPathBuilder.TypeCast))!
.MakeGenericMethod(node.Type);
return Add(node.Operand, node, x => castMethod.Invoke(x, null));
return Add(node.Operand, node, x => x.TypeCast(node.Type));
}
}
else if (node.NodeType == ExpressionType.TypeAs)
{
var castMethod = typeof(CompiledBindingPathBuilder)
.GetMethod(nameof(CompiledBindingPathBuilder.TypeCast))!
.MakeGenericMethod(node.Type);
return Add(node.Operand, node, x => castMethod.Invoke(x, null));
return Add(node.Operand, node, x => x.TypeCast(node.Type));
}
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);
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.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?>();
}
[RequiresUnreferencedCode(TrimmingMessages.StreamPluginRequiresUnreferencedCodeMessage)]
private static IObservable<object?> HandleCompleted(Task task)
{
var resultProperty = task.GetType().GetRuntimeProperty("Result");
var resultProperty = GetTaskResult(task);
if (resultProperty != null)
{
@ -80,6 +79,14 @@ namespace Avalonia.Data.Core.Plugins
}
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)
{
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)
{
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)
{
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