From 6ab1fbd4323b54667fdb750ec016d38c9e40abba Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Fri, 19 Apr 2019 16:14:01 +0300 Subject: [PATCH] Inject the call to compiled xaml to x:Class-type --- .../ControlCatalog/Pages/ButtonPage.xaml.cs | 9 -- .../Utilities/AvaloniaResourcesIndex.cs | 30 ++++ .../Avalonia.Build.Tasks.csproj | 15 +- .../XamlCompilerTaskExecutor.Helpers.cs | 129 ++++++++++++++++ .../XamlCompilerTaskExecutor.cs | 143 ++++++++++++------ .../Avalonia.Markup.Xaml/XamlIl/xamlil.github | 2 +- 6 files changed, 258 insertions(+), 70 deletions(-) create mode 100644 src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.Helpers.cs diff --git a/samples/ControlCatalog/Pages/ButtonPage.xaml.cs b/samples/ControlCatalog/Pages/ButtonPage.xaml.cs index ebcbeae925..1d0c228a0e 100644 --- a/samples/ControlCatalog/Pages/ButtonPage.xaml.cs +++ b/samples/ControlCatalog/Pages/ButtonPage.xaml.cs @@ -5,14 +5,5 @@ namespace ControlCatalog.Pages { public class ButtonPage : UserControl { - public ButtonPage() - { - this.InitializeComponent(); - } - - private void InitializeComponent() - { - AvaloniaXamlLoader.Load(this); - } } } diff --git a/src/Avalonia.Base/Utilities/AvaloniaResourcesIndex.cs b/src/Avalonia.Base/Utilities/AvaloniaResourcesIndex.cs index 66024236da..ed5bc697b0 100644 --- a/src/Avalonia.Base/Utilities/AvaloniaResourcesIndex.cs +++ b/src/Avalonia.Base/Utilities/AvaloniaResourcesIndex.cs @@ -46,6 +46,36 @@ namespace Avalonia.Utilities Entries = entries }); } + + public static byte[] Create(Dictionary data) + { + var sources = data.ToList(); + var offsets = new Dictionary(); + var coffset = 0; + foreach (var s in sources) + { + offsets[s.Key] = coffset; + coffset += s.Value.Length; + } + var index = sources.Select(s => new AvaloniaResourcesIndexEntry + { + Path = s.Key, + Size = s.Value.Length, + Offset = offsets[s.Key] + }).ToList(); + var output = new MemoryStream(); + var ms = new MemoryStream(); + AvaloniaResourcesIndexReaderWriter.Write(ms, index); + new BinaryWriter(output).Write((int)ms.Length); + ms.Position = 0; + ms.CopyTo(output); + foreach (var s in sources) + { + output.Write(s.Value,0,s.Value.Length); + } + + return output.ToArray(); + } } [DataContract] diff --git a/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj b/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj index 072c78f7ef..4dd6f61c2b 100644 --- a/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj +++ b/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj @@ -43,7 +43,7 @@ - + @@ -57,17 +57,12 @@ - + - - + + - + diff --git a/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.Helpers.cs b/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.Helpers.cs new file mode 100644 index 0000000000..73caf69325 --- /dev/null +++ b/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.Helpers.cs @@ -0,0 +1,129 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Avalonia.Utilities; +using Mono.Cecil; + +namespace Avalonia.Build.Tasks +{ + public static partial class XamlCompilerTaskExecutor + { + interface IResource + { + string Uri { get; } + string Name { get; } + byte[] GetData(); + string FilePath { get; } + void Remove(); + + } + + interface IResourceGroup + { + string Name { get; } + IEnumerable Resources { get; } + } + + class EmbeddedResources : IResourceGroup + { + private readonly AssemblyDefinition _asm; + public string Name => "EmbeddedResource"; + + public IEnumerable Resources => _asm.MainModule.Resources.OfType() + .Select(r => new WrappedResource(_asm, r)).ToList(); + + public EmbeddedResources(AssemblyDefinition asm) + { + _asm = asm; + } + class WrappedResource : IResource + { + private readonly AssemblyDefinition _asm; + private readonly EmbeddedResource _res; + + public WrappedResource(AssemblyDefinition asm, EmbeddedResource res) + { + _asm = asm; + _res = res; + } + + public string Uri => $"resm:{Name}?assembly={_asm.Name}"; + public string Name => _res.Name; + public byte[] GetData() => _res.GetResourceData(); + public string FilePath => Name; + + public void Remove() => _asm.MainModule.Resources.Remove(_res); + } + } + + class AvaloniaResources : IResourceGroup + { + private readonly AssemblyDefinition _asm; + Dictionary _resources = new Dictionary(); + private EmbeddedResource _embedded; + public AvaloniaResources(AssemblyDefinition asm, string projectDir) + { + _asm = asm; + _embedded = ((EmbeddedResource)asm.MainModule.Resources.FirstOrDefault(r => + r.ResourceType == ResourceType.Embedded && r.Name == "!AvaloniaResources")); + if (_embedded == null) + return; + using (var stream = _embedded.GetResourceStream()) + { + var br = new BinaryReader(stream); + var index = AvaloniaResourcesIndexReaderWriter.Read(new MemoryStream(br.ReadBytes(br.ReadInt32()))); + var baseOffset = stream.Position; + foreach (var e in index) + { + stream.Position = e.Offset + baseOffset; + _resources[e.Path] = new AvaloniaResource(this, projectDir, e.Path, br.ReadBytes(e.Size)); + } + } + } + + public void Save() + { + if (_embedded != null) + { + _asm.MainModule.Resources.Remove(_embedded); + _embedded = null; + } + + if (_resources.Count == 0) + return; + + _embedded = new EmbeddedResource("!AvaloniaResources", ManifestResourceAttributes.Public, + AvaloniaResourcesIndexReaderWriter.Create(_resources.ToDictionary(x => x.Key, + x => x.Value.GetData()))); + _asm.MainModule.Resources.Add(_embedded); + } + + public string Name => "AvaloniaResources"; + public IEnumerable Resources => _resources.Values.ToList(); + + class AvaloniaResource : IResource + { + private readonly AvaloniaResources _grp; + private readonly byte[] _data; + + public AvaloniaResource(AvaloniaResources grp, + string projectDir, + string name, byte[] data) + { + _grp = grp; + _data = data; + Name = name; + FilePath = Path.Combine(projectDir, name.TrimStart('/')); + Uri = $"avares://{grp._asm.Name}/{name.TrimStart('/')}"; + } + public string Uri { get; } + public string Name { get; } + public byte[] GetData() => _data; + public string FilePath { get; } + + public void Remove() => _grp._resources.Remove(Name); + } + } + } + +} diff --git a/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs b/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs index 5addf939eb..9eeb3023aa 100644 --- a/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs +++ b/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs @@ -9,45 +9,22 @@ using Microsoft.Build.Framework; using Mono.Cecil; using XamlIl.TypeSystem; using Avalonia.Utilities; +using Mono.Cecil.Cil; using Mono.Cecil.Rocks; using XamlIl; using XamlIl.Ast; using XamlIl.Parsers; using XamlIl.Transform; +using MethodAttributes = Mono.Cecil.MethodAttributes; using TypeAttributes = Mono.Cecil.TypeAttributes; namespace Avalonia.Build.Tasks { - public static class XamlCompilerTaskExecutor + public static partial class XamlCompilerTaskExecutor { - static bool CheckXamlName(string n) => n.ToLowerInvariant().EndsWith(".xaml") - || n.ToLowerInvariant().EndsWith(".paml"); - static Dictionary ReadAvaloniaXamlResources(AssemblyDefinition asm) - { - var rv = new Dictionary(); - var stream = ((EmbeddedResource)asm.MainModule.Resources.FirstOrDefault(r => - r.ResourceType == ResourceType.Embedded && r.Name == "!AvaloniaResources"))?.GetResourceStream(); - if (stream == null) - return rv; - var br = new BinaryReader(stream); - var index = AvaloniaResourcesIndexReaderWriter.Read(new MemoryStream(br.ReadBytes(br.ReadInt32()))); - var baseOffset = stream.Position; - foreach (var e in index.Where(e => CheckXamlName(e.Path))) - { - stream.Position = e.Offset + baseOffset; - rv[e.Path] = br.ReadBytes(e.Size); - } - return rv; - } - - static Dictionary ReadEmbeddedXamlResources(AssemblyDefinition asm) - { - var rv = new Dictionary(); - foreach (var r in asm.MainModule.Resources.OfType().Where(r => CheckXamlName(r.Name))) - rv[r.Name] = r.GetResourceData(); - return rv; - } + static bool CheckXamlName(IResource r) => r.Name.ToLowerInvariant().EndsWith(".xaml") + || r.Name.ToLowerInvariant().EndsWith(".paml"); public class CompileResult { @@ -65,11 +42,12 @@ namespace Avalonia.Build.Tasks { var typeSystem = new CecilTypeSystem(references.Concat(new[] {input}), input); var asm = typeSystem.TargetAssemblyDefinition; - var emres = ReadEmbeddedXamlResources(asm); - var avares = ReadAvaloniaXamlResources(asm); - if (avares.Count == 0 && emres.Count == 0) + var emres = new EmbeddedResources(asm); + var avares = new AvaloniaResources(asm, projectDirectory); + if (avares.Resources.Count(CheckXamlName) == 0 && emres.Resources.Count(CheckXamlName) == 0) // Nothing to do return new CompileResult(true); + var xamlLanguage = AvaloniaXamlIlLanguage.Configure(typeSystem); var compilerConfig = new XamlIlTransformerConfiguration(typeSystem, typeSystem.TargetAssembly, @@ -94,10 +72,9 @@ namespace Avalonia.Build.Tasks asm.MainModule.ImportReference(editorBrowsableAttribute.GetConstructors() .First(c => c.Parameters.Count == 1)); - bool CompileGroup(Dictionary resources, string name, Func uriTransform, - Func pathTransform) + bool CompileGroup(IResourceGroup group) { - var typeDef = new TypeDefinition("CompiledAvaloniaXaml", name, + var typeDef = new TypeDefinition("CompiledAvaloniaXaml", group.Name, TypeAttributes.Class, asm.MainModule.TypeSystem.Object); @@ -107,23 +84,24 @@ namespace Avalonia.Build.Tasks }); asm.MainModule.Types.Add(typeDef); var builder = typeSystem.CreateTypeBuilder(typeDef); - foreach (var res in resources) + + foreach (var res in group.Resources.Where(CheckXamlName)) { try { // StreamReader is needed here to handle BOM - var xaml = new StreamReader(new MemoryStream(res.Value)).ReadToEnd(); + var xaml = new StreamReader(new MemoryStream(res.GetData())).ReadToEnd(); var parsed = XDocumentXamlIlParser.Parse(xaml); var initialRoot = (XamlIlAstObjectNode)parsed.Root; var classDirective = initialRoot.Children.OfType() .FirstOrDefault(d => d.Namespace == XamlNamespaces.Xaml2006 && d.Name == "Class"); - + IXamlIlType classType = null; if (classDirective != null) { if (classDirective.Values.Count != 1 || !(classDirective.Values[0] is XamlIlAstTextNode tn)) throw new XamlIlParseException("x:Class should have a string value", classDirective); - var classType = typeSystem.TargetAssembly.FindType(tn.Text); + classType = typeSystem.TargetAssembly.FindType(tn.Text); if (classType == null) throw new XamlIlParseException($"Unable to find type `{tn.Text}`", classDirective); initialRoot.Type = new XamlIlAstClrTypeReference(classDirective, classType); @@ -131,9 +109,72 @@ namespace Avalonia.Build.Tasks compiler.Transform(parsed); + var populateName = "Populate:" + res.Name; + var buildName = classType != null ? "Build:" + res.Name : null; compiler.Compile(parsed, builder, contextClass, - "Populate:" + res.Key, "Build:" + res.Key, - "NamespaceInfo:" + res.Key, uriTransform(res.Key)); + populateName, buildName, + "NamespaceInfo:" + res.Name, res.Uri); + + if (classType != null) + { + var compiledPopulateMethod = typeSystem.GetTypeReference(builder).Resolve() + .Methods.First(m => m.Name == populateName); + var classTypeDefinition = typeSystem.GetTypeReference(classType).Resolve(); + + + var trampoline = new MethodDefinition("!XamlIlPopulateTrampoline", + MethodAttributes.Static | MethodAttributes.Private, asm.MainModule.TypeSystem.Void); + trampoline.Parameters.Add(new ParameterDefinition(classTypeDefinition)); + classTypeDefinition.Methods.Add(trampoline); + trampoline.Body.Instructions.Add(Instruction.Create(OpCodes.Ldnull)); + trampoline.Body.Instructions.Add(Instruction.Create(OpCodes.Ldarg_0)); + trampoline.Body.Instructions.Add(Instruction.Create(OpCodes.Call, compiledPopulateMethod)); + trampoline.Body.Instructions.Add(Instruction.Create(OpCodes.Ret)); + + var foundXamlLoader = false; + // Find AvaloniaXamlLoader.Load(this) and replace it with !XamlIlPopulateTrampoline(this) + foreach (var method in classTypeDefinition.Methods + .Where(m => !m.Attributes.HasFlag(MethodAttributes.Static))) + { + var i = method.Body.Instructions; + for (var c = 1; c < i.Count; c++) + { + if (i[c - 1].OpCode == OpCodes.Ldarg_0 + && i[c].OpCode == OpCodes.Call) + { + var op = i[c].Operand as MethodReference; + if (op != null + && op.Name == "Load" + && op.Parameters.Count == 1 + && op.Parameters[0].ParameterType.FullName == "System.Object" + && op.DeclaringType.FullName == "Avalonia.Markup.Xaml.AvaloniaXamlLoader") + { + i[c].Operand = trampoline; + foundXamlLoader = true; + } + } + } + } + + if (!foundXamlLoader) + { + var ctors = classTypeDefinition.GetConstructors().ToList(); + // We can inject xaml loader into default constructor + if (ctors.Count == 1 && ctors[0].Body.Instructions.Count(o=>o.OpCode!=OpCodes.Nop) == 3) + { + var i = ctors[0].Body.Instructions; + var retIdx = i.IndexOf(i.Last(x => x.OpCode == OpCodes.Ret)); + i.Insert(retIdx, Instruction.Create(OpCodes.Call, trampoline)); + i.Insert(retIdx, Instruction.Create(OpCodes.Ldarg_0)); + } + else + { + throw new InvalidProgramException( + $"No call to AvaloniaXamlLoader.Load(this) call found anywhere in the type {classType.FullName} and type seems to have custom constructors."); + } + } + + } } catch (Exception e) { @@ -143,26 +184,28 @@ namespace Avalonia.Build.Tasks lineNumber = xe.Line; linePosition = xe.Position; } - engine.LogErrorEvent(new BuildErrorEventArgs("Avalonia", "XAMLIL", pathTransform(res.Key), + engine.LogErrorEvent(new BuildErrorEventArgs("Avalonia", "XAMLIL", res.FilePath, lineNumber, linePosition, lineNumber, linePosition, e.Message, "", "Avalonia")); return false; } + res.Remove(); } return true; } - - if (emres.Count != 0) - if (!CompileGroup(emres, "EmbeddedResource", - name => $"resm:{name}?assembly={asm.Name}", name => name)) + + + if (emres.Resources.Count(CheckXamlName) != 0) + if (!CompileGroup(emres)) return new CompileResult(false); - if (avares.Count != 0) - if (!CompileGroup(avares, "AvaloniaResource", - name => $"avares://{asm.Name}/{name}", - name => Path.Combine(projectDirectory, name.TrimStart('/')))) + if (avares.Resources.Count(CheckXamlName) != 0) + { + if (!CompileGroup(avares)) return new CompileResult(false); - + avares.Save(); + } + var ms = new MemoryStream(); asm.Write(ms); return new CompileResult(true, ms.ToArray()); diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github b/src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github index 2c9c9cb392..c28bd89bf4 160000 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github @@ -1 +1 @@ -Subproject commit 2c9c9cb392d36af4624578254aa376ef0bb5a964 +Subproject commit c28bd89bf4c315c188394139f500f7f4efed4b94