From 47b91121386735ae54e09a990583bf21fc53fbd9 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Fri, 25 Nov 2022 01:01:48 -0500 Subject: [PATCH] Make AvaloniaXamlIncludeTransformer a group transformer and remove old code with manual IL emit --- src/Avalonia.Base/Platform/AssetLoader.cs | 11 +- .../XamlCompilerTaskExecutor.cs | 139 +----------------- .../AvaloniaXamlIlCompiler.cs | 3 +- .../XamlIncludeGroupTransformer.cs | 133 +++++++++++++++++ .../AvaloniaXamlIlAssetIncludeTransformer.cs | 93 ------------ ...IlConstructorServiceProviderTransformer.cs | 24 ++- .../XamlIl/Runtime/XamlIlRuntimeHelpers.cs | 10 -- 7 files changed, 165 insertions(+), 248 deletions(-) create mode 100644 src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/GroupTransformers/XamlIncludeGroupTransformer.cs delete mode 100644 src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlAssetIncludeTransformer.cs diff --git a/src/Avalonia.Base/Platform/AssetLoader.cs b/src/Avalonia.Base/Platform/AssetLoader.cs index a74da2a178..8d3576d180 100644 --- a/src/Avalonia.Base/Platform/AssetLoader.cs +++ b/src/Avalonia.Base/Platform/AssetLoader.cs @@ -3,16 +3,22 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; +#if !BUILDTASK using Avalonia.Platform.Internal; using Avalonia.Utilities; +#endif namespace Avalonia.Platform { /// /// Loads assets compiled into the application binary. /// - public class AssetLoader : IAssetLoader + public class AssetLoader +#if !BUILDTASK + : IAssetLoader +#endif { +#if !BUILDTASK private static IAssemblyDescriptorResolver s_assemblyDescriptorResolver = new AssemblyDescriptorResolver(); private AssemblyDescriptor? _defaultResmAssembly; @@ -206,7 +212,8 @@ namespace Avalonia.Platform return null; } - +#endif + public static void RegisterResUriParsers() { if (!UriParser.IsKnownScheme("avares")) diff --git a/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs b/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs index 589472785d..31649fd4b6 100644 --- a/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs +++ b/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs @@ -231,8 +231,6 @@ namespace Avalonia.Build.Tasks }); asm.MainModule.Types.Add(typeDef); var builder = typeSystem.CreateTypeBuilder(typeDef); - - var populateMethodsToTransform = new List<(MethodDefinition populateMethod, string resourceFilePath)>(); IReadOnlyCollection parsedXamlDocuments = new List(); foreach (var res in group.Resources.Where(CheckXamlName).OrderBy(x => x.FilePath.ToLowerInvariant())) @@ -347,23 +345,11 @@ namespace Avalonia.Build.Tasks res.Uri, res ); - var compiledPopulateMethod = typeSystem.GetTypeReference(populateBuilder).Resolve() - .Methods.First(m => m.Name == populateName); - - // Include populate method and all nested methods/closures used in the populate method, - // So we can replace style/resource includes in all of them. - populateMethodsToTransform.Add((compiledPopulateMethod, res.FilePath)); - populateMethodsToTransform.AddRange(compiledPopulateMethod.Body.Instructions - .Where(b => b.OpCode == OpCodes.Call || b.OpCode == OpCodes.Callvirt || b.OpCode == OpCodes.Ldftn) - .Select(b => b.Operand) - .OfType() - .Where(m => compiledPopulateMethod.DeclaringType.NestedTypes.Contains(m.DeclaringType)) - .Select(m => m.Resolve()) - .Where(m => m.HasBody) - .Select(m => (m, res.FilePath))); - if (classTypeDefinition != null) { + var compiledPopulateMethod = typeSystem.GetTypeReference(populateBuilder).Resolve() + .Methods.First(m => m.Name == document.PopulateMethod.Name); + var designLoaderFieldType = typeSystem .GetType("System.Action`1") .MakeGenericType(typeSystem.GetType("System.Object")); @@ -512,14 +498,6 @@ namespace Avalonia.Build.Tasks res.Remove(); } - foreach (var (populateMethod, resourceFilePath) in populateMethodsToTransform) - { - if (!TransformXamlIncludes(engine, typeSystem, populateMethod, resourceFilePath, createRootServiceProviderMethod)) - { - return false; - } - } - // Technically that's a hack, but it fixes corert incompatibility caused by deterministic builds int dupeCounter = 1; foreach (var grp in typeDef.NestedTypes.GroupBy(x => x.Name)) @@ -596,116 +574,5 @@ namespace Avalonia.Build.Tasks return true; } - - private static bool TransformXamlIncludes( - IBuildEngine engine, CecilTypeSystem typeSystem, - MethodDefinition populateMethod, string resourceFilePath, - MethodReference createRootServiceProviderMethod) - { - var asm = typeSystem.TargetAssemblyDefinition; - foreach (var instruction in populateMethod.Body.Instructions.ToArray()) - { - const string resolveStyleIncludeName = "ResolveStyleInclude"; - const string resolveResourceInclude = "ResolveResourceInclude"; - if (instruction.OpCode == OpCodes.Call - && instruction.Operand is MethodReference - { - Name: resolveStyleIncludeName or resolveResourceInclude, - DeclaringType: { Name: "XamlIlRuntimeHelpers" } - }) - { - int lineNumber = 0, linePosition = 0; - bool instructionsModified = false; - try - { - var assetSource = (string)instruction.Previous.Previous.Previous.Operand; - lineNumber = GetConstValue(instruction.Previous.Previous); - linePosition = GetConstValue(instruction.Previous); - - var index = populateMethod.Body.Instructions.IndexOf(instruction); - - assetSource = assetSource.Replace("avares://", ""); - - var assemblyNameSeparator = assetSource.IndexOf('/'); - var fileNameSeparator = assetSource.LastIndexOf('/'); - if (assemblyNameSeparator < 0 || fileNameSeparator < 0) - { - throw new InvalidProgramException( - $"Invalid asset source \"{assetSource}\". It must contain assembly name and relative path."); - } - - var assetAssemblyName = assetSource.Substring(0, assemblyNameSeparator); - var assetAssembly = typeSystem.FindAssembly(assetAssemblyName) - ?? throw new InvalidProgramException( - $"Unable to resolve assembly \"{assetAssemblyName}\""); - - var fileName = Path.GetFileNameWithoutExtension(assetSource.Replace('/', '.')); - if (assetAssembly.FindType(fileName) is { } type - && type.FindConstructor() is { } ctor) - { - var ctorMethod = - asm.MainModule.ImportReference(typeSystem.GetMethodReference(ctor)); - instructionsModified = true; - populateMethod.Body.Instructions[index - 3] = Instruction.Create(OpCodes.Nop); - populateMethod.Body.Instructions[index - 2] = Instruction.Create(OpCodes.Nop); - populateMethod.Body.Instructions[index - 1] = Instruction.Create(OpCodes.Nop); - populateMethod.Body.Instructions[index] = Instruction.Create(OpCodes.Newobj, ctorMethod); - } - else - { - var resources = assetAssembly.FindType("CompiledAvaloniaXaml.!AvaloniaResources") - ?? throw new InvalidOperationException( - $"Unable to resolve \"!AvaloniaResources\" type on \"{assetAssemblyName}\" assembly"); - - var relativeName = "Build:" + assetSource.Substring(assemblyNameSeparator); - var buildMethod = resources.FindMethod(m => m.Name == relativeName) - ?? throw new InvalidOperationException( - $"Unable to resolve build method \"{relativeName}\" resource on the \"{assetAssemblyName}\" assembly"); - - var methodReference = asm.MainModule.ImportReference(typeSystem.GetMethodReference(buildMethod)); - instructionsModified = true; - populateMethod.Body.Instructions[index - 3] = Instruction.Create(OpCodes.Nop); - populateMethod.Body.Instructions[index - 2] = Instruction.Create(OpCodes.Nop); - populateMethod.Body.Instructions[index - 1] = - Instruction.Create(OpCodes.Call, createRootServiceProviderMethod); - populateMethod.Body.Instructions[index] = Instruction.Create(OpCodes.Call, methodReference); - } - } - catch (Exception e) - { - if (instructionsModified) - { - engine.LogErrorEvent(new BuildErrorEventArgs("Avalonia", "XAMLIL", resourceFilePath, - lineNumber, linePosition, lineNumber, linePosition, - e.Message, "", "Avalonia")); - return false; - } - else - { - engine.LogWarningEvent(new BuildWarningEventArgs("Avalonia", "XAMLIL", - resourceFilePath, - lineNumber, linePosition, lineNumber, linePosition, - e.Message, "", "Avalonia")); - } - } - - static int GetConstValue(Instruction instruction) - { - if (instruction.OpCode is { Code : >= Code.Ldc_I4_0 and <= Code.Ldc_I4_8 }) - { - return instruction.OpCode.Code - Code.Ldc_I4_0; - } - if (instruction.Operand is not null) - { - return Convert.ToInt32(instruction.Operand); - } - - return 0; - } - } - } - - return true; - } } } diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs index ccda94ff1a..b8350c3f11 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs @@ -56,8 +56,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions new AvaloniaXamlIlSetterTransformer(), new AvaloniaXamlIlConstructorServiceProviderTransformer(), new AvaloniaXamlIlTransitionsTypeMetadataTransformer(), - new AvaloniaXamlIlResolveByNameMarkupExtensionReplacer(), - new AvaloniaXamlIlAssetIncludeTransformer() + new AvaloniaXamlIlResolveByNameMarkupExtensionReplacer() ); InsertBefore( new AvaloniaXamlIlOptionMarkupExtensionTransformer()); diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/GroupTransformers/XamlIncludeGroupTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/GroupTransformers/XamlIncludeGroupTransformer.cs new file mode 100644 index 0000000000..2d30cd18b8 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/GroupTransformers/XamlIncludeGroupTransformer.cs @@ -0,0 +1,133 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers; +using Avalonia.Platform; +using XamlX; +using XamlX.Ast; +using XamlX.Emit; +using XamlX.IL; +using XamlX.Transform; +using XamlX.Transform.Transformers; +using XamlX.TypeSystem; + +namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.GroupTransformers; + +internal class AvaloniaXamlIncludeTransformer : IXamlAstGroupTransformer +{ + public IXamlAstNode Transform(AstGroupTransformationContext context, IXamlAstNode node) + { + if (node is not XamlValueWithManipulationNode valueNode + || valueNode.Value is not XamlAstNewClrObjectNode objectNode + || (objectNode.Type.GetClrType() != context.GetAvaloniaTypes().StyleInclude + && objectNode.Type.GetClrType() != context.GetAvaloniaTypes().ResourceInclude)) + { + return node; + } + + var nodeTypeName = objectNode.Type.GetClrType().Name; + if (valueNode.Manipulation is not XamlObjectInitializationNode + { + Manipulation: XamlPropertyAssignmentNode { Property: { Name: "Source" } } sourceProperty + }) + { + return context.ParseError($"Source property must be set on the \"{nodeTypeName}\" node.", node); + } + + if (sourceProperty.Values.OfType().FirstOrDefault() is not { } sourceUriNode + || sourceUriNode.Type.GetClrType() != context.GetAvaloniaTypes().Uri + || sourceUriNode.Arguments.FirstOrDefault() is not XamlConstantNode { Constant: string originalAssetPath }) + { + // TODO: make it a compiler warning + // Source value can be set with markup extension instead of the Uri object node, we don't support it here yet. + return node; + } + + if (originalAssetPath.StartsWith("avares://")) + { + } + else if (originalAssetPath.StartsWith("/")) + { + var baseUrl = context.CurrentDocument.Uri ?? throw new InvalidOperationException("CurrentDocument URI is null."); + originalAssetPath = baseUrl.Substring(0, baseUrl.LastIndexOf('/')) + originalAssetPath; + } + else + { + return context.ParseError( + $"Avalonia supports only \"avares://\" sources or relative sources starting with \"/\" on the \"{nodeTypeName}\" node.", + node); + } + + AssetLoader.RegisterResUriParsers(); + + originalAssetPath = Uri.UnescapeDataString(new Uri(originalAssetPath).AbsoluteUri); + var assetPath = originalAssetPath.Replace("avares://", ""); + var assemblyNameSeparator = assetPath.IndexOf('/'); + var assembly = assetPath.Substring(0, assemblyNameSeparator); + var fullTypeName = Path.GetFileNameWithoutExtension(assetPath.Replace('/', '.')); + + if (context.Documents.FirstOrDefault(d => string.Equals(d.Uri, originalAssetPath, StringComparison.InvariantCultureIgnoreCase)) is {} targetDocument) + { + if (targetDocument.ClassType is not null) + { + return FromType(context, targetDocument.ClassType, node); + } + + if (targetDocument.BuildMethod is null) + { + return context.ParseError($"\"{originalAssetPath}\" cannot be instantiated.", node); + } + + return FromMethod(context, targetDocument.BuildMethod, node); + } + + + if (context.Configuration.TypeSystem.FindAssembly(assembly) is not { } assetAssembly) + { + return context.ParseError($"Assembly \"{assembly}\" was not found from the \"{originalAssetPath}\" source.", node); + } + + if (assetAssembly.FindType(fullTypeName) is { } type + && type.FindMethod(m => m.Name == "!XamlIlPopulate") is not null) + { + return FromType(context, type, node); + } + else + { + var avaResType = assetAssembly.FindType("CompiledAvaloniaXaml.!AvaloniaResources"); + if (avaResType is null) + { + return context.ParseError( + $"Unable to resolve \"!AvaloniaResources\" type on \"{assembly}\" assembly.", node); + } + + var relativeName = "Build:" + assetPath.Substring(assemblyNameSeparator); + var buildMethod = avaResType.FindMethod(m => m.Name == relativeName); + if (buildMethod is null) + { + return context.ParseError( + $"Unable to resolve build method \"{relativeName}\" resource on the \"{assembly}\" assembly.", + node); + } + + return FromMethod(context, buildMethod, node); + } + } + + private static IXamlAstNode FromType(AstTransformationContext context, IXamlType type, IXamlLineInfo li) + { + IXamlAstNode newObjNode = new XamlAstObjectNode(li, new XamlAstClrTypeReference(li, type, false)); + newObjNode = new AvaloniaXamlIlConstructorServiceProviderTransformer().Transform(context, newObjNode); + newObjNode = new ConstructableObjectTransformer().Transform(context, newObjNode); + return new NewObjectTransformer().Transform(context, newObjNode); + } + + private static IXamlAstNode FromMethod(AstTransformationContext context, IXamlMethod method, IXamlLineInfo li) + { + var sp = context.Configuration.TypeMappings.ServiceProvider; + return new XamlStaticOrTargetedReturnMethodCallNode(li, method, + new[] { new AvaloniaXamlIlConstructorServiceProviderTransformer.InjectServiceProviderNode(sp, li, false) }); + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlAssetIncludeTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlAssetIncludeTransformer.cs deleted file mode 100644 index 377a9e72d9..0000000000 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlAssetIncludeTransformer.cs +++ /dev/null @@ -1,93 +0,0 @@ -using System.Linq; -using XamlX; -using XamlX.Ast; -using XamlX.Emit; -using XamlX.IL; -using XamlX.Transform; -using XamlX.TypeSystem; - -namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers; - -internal class AvaloniaXamlIlAssetIncludeTransformer : IXamlAstTransformer -{ - public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node) - { - if (node is not XamlAstObjectNode objectNode - || (objectNode.Type.GetClrType() != context.GetAvaloniaTypes().StyleInclude - && objectNode.Type.GetClrType() != context.GetAvaloniaTypes().ResourceInclude)) - { - return node; - } - - var nodeTypeName = objectNode.Type.GetClrType().Name; - - var sourceProperty = objectNode.Children.OfType().FirstOrDefault(n => n.Property.GetClrProperty().Name == "Source"); - var directives = objectNode.Children.OfType().ToList(); - if (sourceProperty is null - || objectNode.Children.Count != (directives.Count + 1)) - { - throw new XamlParseException($"Unexpected property on the {nodeTypeName} node", node); - } - - if (sourceProperty.Values.OfType().FirstOrDefault() is not { } sourceTextNode) - { - // TODO: make it a compiler warning - // Source value can be set with markup extension instead of a text node, we don't support it here yet. - return node; - } - - var originalAssetPath = sourceTextNode.Text; - if (!(originalAssetPath.StartsWith("avares://") || originalAssetPath.StartsWith("/"))) - { - return node; - } - - var runtimeHelpers = context.GetAvaloniaTypes().RuntimeHelpers; - var markerMethodName = "Resolve" + nodeTypeName; - var markerMethod = runtimeHelpers.FindMethod(m => m.Name == markerMethodName && m.Parameters.Count == 3); - if (markerMethod is null) - { - throw new XamlParseException($"Marker method \"{markerMethodName}\" was not found for the \"{nodeTypeName}\" node", node); - } - - return new XamlValueWithManipulationNode( - node, - new AssetIncludeMethodNode(node, markerMethod, originalAssetPath), - new XamlManipulationGroupNode(node, directives)); - } - - private class AssetIncludeMethodNode : XamlAstNode, IXamlAstValueNode, IXamlAstILEmitableNode - { - private readonly IXamlMethod _method; - private readonly string _originalAssetPath; - - public AssetIncludeMethodNode( - IXamlAstNode original, IXamlMethod method, string originalAssetPath) - : base(original) - { - _method = method; - _originalAssetPath = originalAssetPath; - } - - public IXamlAstTypeReference Type => new XamlAstClrTypeReference(this, _method.ReturnType, false); - - public XamlILNodeEmitResult Emit(XamlEmitContext context, IXamlILEmitter codeGen) - { - var absoluteSource = _originalAssetPath; - if (absoluteSource.StartsWith("/")) - { - // Avoid Uri class here to avoid potential problems with escaping. - // Keeping string as close to the original as possible. - var absoluteBaseUrl = context.RuntimeContext.BaseUrl; - absoluteSource = absoluteBaseUrl.Substring(0, absoluteBaseUrl.LastIndexOf('/')) + absoluteSource; - } - - codeGen.Ldstr(absoluteSource); - codeGen.Ldc_I4(Line); - codeGen.Ldc_I4(Position); - codeGen.EmitCall(_method); - - return XamlILNodeEmitResult.Type(0, _method.ReturnType); - } - } -} diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlConstructorServiceProviderTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlConstructorServiceProviderTransformer.cs index 35e2624ff9..de654c4595 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlConstructorServiceProviderTransformer.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlConstructorServiceProviderTransformer.cs @@ -21,7 +21,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers c.IsPublic && !c.IsStatic && c.Parameters.Count == 1 && c.Parameters[0] .Equals(sp))) { - on.Arguments.Add(new InjectServiceProviderNode(sp, on)); + on.Arguments.Add(new InjectServiceProviderNode(sp, on, true)); } } } @@ -29,19 +29,33 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers return node; } - class InjectServiceProviderNode : XamlAstNode, IXamlAstValueNode,IXamlAstNodeNeedsParentStack, + internal class InjectServiceProviderNode : XamlAstNode, IXamlAstValueNode,IXamlAstNodeNeedsParentStack, IXamlAstEmitableNode { - public InjectServiceProviderNode(IXamlType type, IXamlLineInfo lineInfo) : base(lineInfo) + private readonly bool _inheritContext; + + public InjectServiceProviderNode(IXamlType type, IXamlLineInfo lineInfo, bool inheritContext) : base(lineInfo) { + _inheritContext = inheritContext; Type = new XamlAstClrTypeReference(lineInfo, type, false); } public IXamlAstTypeReference Type { get; } - public bool NeedsParentStack => true; + public bool NeedsParentStack => _inheritContext; public XamlILNodeEmitResult Emit(XamlEmitContext context, IXamlILEmitter codeGen) { - codeGen.Ldloc(context.ContextLocal); + if (_inheritContext) + { + codeGen.Ldloc(context.ContextLocal); + } + else + { + var method = context.GetAvaloniaTypes().RuntimeHelpers + .FindMethod(m => m.Name == "CreateRootServiceProviderV2"); + codeGen.EmitCall(method); + context.RuntimeContext.Factory(codeGen); + } + return XamlILNodeEmitResult.Type(0, Type.GetClrType()); } } diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs index f547888d6e..d07c354945 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs @@ -17,16 +17,6 @@ namespace Avalonia.Markup.Xaml.XamlIl.Runtime { public static class XamlIlRuntimeHelpers { - public static IStyle ResolveStyleInclude(string absoluteSource, int line, int position) - { - return new StyleInclude((Uri)null) { Source = new Uri(absoluteSource) }.Loaded; - } - - public static IResourceDictionary ResolveResourceInclude(string absoluteSource, int line, int position) - { - return new ResourceInclude((Uri)null) { Source = new Uri(absoluteSource) }.Loaded; - } - public static Func DeferredTransformationFactoryV1(Func builder, IServiceProvider provider) {