XAML hot reloading (#5350)

* Move RobustXaml to a shared package

In a near-future change, I'll make it possible to optionally link to
this from Robust.Client, which will allow JIT compiling XAML.

Also upgrade it to a version of .NET that supports nullability
annotations.

* Re-namespace packages

* Add a JIT compiler, plus hooks that call into it

In Debug, after this change, all XAML will be hot reloaded once every
time an assembly is reloaded.

The new code is compiled with SRE and is _not_ sandboxed -- this is not
suitable to run against prod.

In Release, the hot reload path is totally skipped, using the same trick
as SmugLeaf used in an earlier attempt to implement this functionality.

* Hot reload: watcher

This is a bit of a horror, but there's not in-engine support for
identifying the source tree or the XAML files in it.

* Put everything dangerous behind conditional comp

* Code cleanup, docs

* Fix a bad comment

* Deal a little better with crashes in the watcher

* Make reload failures Info, since they're expected

They were previously causing the integration tests to flag, even though
"a few types fail hot reloading because they're internal" is expected
behavior.

* Fix an unnecessary null check

I removed the ability for CompileCore to return null.

* injectors: null! strings, default primitives

* Tidy documentation (thanks, PJB!)

* Reinstate netstandard2.0, abolish Pidgin

* Internal-ize all of Robust.Xaml

* Add a cautionary note to Sandbox.yml

* Shuffle around where conditional compilation occurs

* Privatize fields in XamlImplementationStorage

* Internalize XamlJitDelegate

* Inline some remarks. No cond. comp in Robust.Xaml

* Use file-scoped namespaces

They aren't allowed at Language Level 8.0. (which I arbitrarily picked
for Robust.Xaml because it's the oldest one that would work)

* Bump language level for R.Xaml, file namespaces

* Force hot reloading off for integration tests

* Fix bizarre comment/behavior in XamlImplementationStorage

* Consistently use interfaces, even in generated code

* Update Robust.Client/ClientIoC.cs

---------

Co-authored-by: Pieter-Jan Briers <pieterjan.briers@gmail.com>
This commit is contained in:
Nyeogmi
2024-08-26 17:16:57 -07:00
committed by GitHub
parent d7aa5daf6a
commit 6396ec472d
35 changed files with 1892 additions and 487 deletions

View File

@@ -1,8 +1,8 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using Microsoft.Build.Framework;
using Robust.Xaml;
namespace Robust.Build.Tasks
{
@@ -37,10 +37,12 @@ namespace Robust.Build.Tasks
var msg = $"CompileRobustXamlTask -> AssemblyFile:{AssemblyFile}, ProjectDirectory:{ProjectDirectory}, OutputPath:{OutputPath}";
BuildEngine.LogMessage(msg, MessageImportance.High);
var res = XamlCompiler.Compile(BuildEngine, input,
var res = XamlAotCompiler.Compile(
BuildEngine, input,
File.ReadAllLines(ReferencesFilePath).Where(l => !string.IsNullOrWhiteSpace(l)).ToArray(),
ProjectDirectory, OutputPath,
(SignAssembly && !DelaySign) ? AssemblyOriginatorKeyFile : null);
OutputPath,
(SignAssembly && !DelaySign) ? AssemblyOriginatorKeyFile : null
);
if (!res.success)
return false;
if (!res.writtentofile)
@@ -65,22 +67,24 @@ namespace Robust.Build.Tasks
return true;
}
// PYREX NOTE: This project was comically null-unsafe before I touched it. I'm just marking what it did accurately
[Required]
public string ReferencesFilePath { get; set; }
public string ReferencesFilePath { get; set; } = null!;
[Required]
public string ProjectDirectory { get; set; }
public string ProjectDirectory { get; set; } = null!;
[Required]
public string AssemblyFile { get; set; }
public string AssemblyFile { get; set; } = null!;
[Required]
public string OriginalCopyPath { get; set; }
public string? OriginalCopyPath { get; set; } = null;
public string OutputPath { get; set; }
public string UpdateBuildIndicator { get; set; }
public string? OutputPath { get; set; }
public string UpdateBuildIndicator { get; set; } = null!;
public string AssemblyOriginatorKeyFile { get; set; }
public string AssemblyOriginatorKeyFile { get; set; } = null!;
public bool SignAssembly { get; set; }
public bool DelaySign { get; set; }
@@ -95,7 +99,7 @@ namespace Robust.Build.Tasks
return rv;
}
public IBuildEngine BuildEngine { get; set; }
public ITaskHost HostObject { get; set; }
public IBuildEngine BuildEngine { get; set; } = null!;
public ITaskHost HostObject { get; set; } = null!;
}
}

View File

@@ -1,37 +0,0 @@
using System.Linq;
using Pidgin;
using static Pidgin.Parser;
namespace Robust.Build.Tasks
{
public static class MathParsing
{
public static Parser<char, float> Single { get; } = Real.Select(c => (float) c);
public static Parser<char, float> Single1 { get; }
= Single.Between(SkipWhitespaces);
public static Parser<char, (float, float)> Single2 { get; }
= Single.Before(SkipWhitespaces).Repeat(2).Select(e =>
{
var arr = e.ToArray();
return (arr[0], arr[1]);
});
public static Parser<char, (float, float, float, float)> Single4 { get; }
= Single.Before(SkipWhitespaces).Repeat(4).Select(e =>
{
var arr = e.ToArray();
return (arr[0], arr[1], arr[2], arr[3]);
});
public static Parser<char, float[]> Thickness { get; }
= SkipWhitespaces.Then(
OneOf(
Try(Single4.Select(c => new[] {c.Item1, c.Item2, c.Item3, c.Item4})),
Try(Single2.Select(c => new[] {c.Item1, c.Item2})),
Try(Single1.Select(c => new[] {c}))
));
}
}

View File

@@ -55,9 +55,11 @@ namespace Robust.Build.Tasks
public bool BuildProjectFile(string projectFileName, string[] targetNames, IDictionary globalProperties,
IDictionary targetOutputs) => throw new NotSupportedException();
public bool ContinueOnError { get; }
public int LineNumberOfTaskNode { get; }
public int ColumnNumberOfTaskNode { get; }
public string ProjectFileOfTaskNode { get; }
// PYREX NOTE: This project was extremely null-unsafe before I touched it. I'm just marking what it did already
// Here's the broken interface of IBuildEngine that we started with
public bool ContinueOnError => default;
public int LineNumberOfTaskNode => default;
public int ColumnNumberOfTaskNode => default;
public string ProjectFileOfTaskNode => null!;
}
}

View File

@@ -1,17 +1,30 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\MSBuild\Robust.Engine.props" />
<!--
PJB3005 (2024-08-24)
So the reason that Robust.Client.Injectors is NS2.0 is that Visual Studio
still ships a .NET FX based MSBuild for some godforsaken reason. This means
that when having Robust.Client.Injectors loaded directly by the main MSBuild
process... that would break.
Except we don't do that anyways right now due to file locking issues, so maybe
it's fine to give up on that. Whatever.
-->
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
<TargetFramework>netstandard2.0</TargetFramework>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
<LangVersion>8.0</LangVersion>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Build.Framework" Version="17.8.3" />
<PackageReference Include="Mono.Cecil" Version="0.11.5" />
<PackageReference Include="Pidgin" Version="2.5.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\XamlX\src\XamlX.IL.Cecil\XamlX.IL.Cecil.csproj" />
<ProjectReference Include="..\Robust.Xaml\Robust.Xaml.csproj" />
</ItemGroup>
</Project>

View File

@@ -1,389 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Microsoft.Build.Framework;
using Mono.Cecil;
using Mono.Cecil.Cil;
using Mono.Cecil.Rocks;
using Pidgin;
using XamlX;
using XamlX.Ast;
using XamlX.Emit;
using XamlX.IL;
using XamlX.Parsers;
using XamlX.Transform;
using XamlX.TypeSystem;
namespace Robust.Build.Tasks
{
/// <summary>
/// Based on https://github.com/AvaloniaUI/Avalonia/blob/c85fa2b9977d251a31886c2534613b4730fbaeaf/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs
/// Adjusted for our UI-Framework
/// </summary>
public partial class XamlCompiler
{
public static (bool success, bool writtentofile) Compile(IBuildEngine engine, string input, string[] references,
string projectDirectory, string output, string strongNameKey)
{
var typeSystem = new CecilTypeSystem(references
.Where(r => !r.ToLowerInvariant().EndsWith("robust.build.tasks.dll"))
.Concat(new[] { input }), input);
var asm = typeSystem.TargetAssemblyDefinition;
if (asm.MainModule.GetType("CompiledRobustXaml", "XamlIlContext") != null)
{
// If this type exists, the assembly has already been processed by us.
// Do not run again, it would corrupt the file.
// This *shouldn't* be possible due to Inputs/Outputs dependencies in the build system,
// but better safe than sorry eh?
engine.LogWarningEvent(new BuildWarningEventArgs("XAMLIL", "", "", 0, 0, 0, 0, "Ran twice on same assembly file; ignoring.", "", ""));
return (true, false);
}
var compileRes = CompileCore(engine, typeSystem);
if (compileRes == null)
return (true, false);
if (compileRes == false)
return (false, false);
var writerParameters = new WriterParameters { WriteSymbols = asm.MainModule.HasSymbols };
if (!string.IsNullOrWhiteSpace(strongNameKey))
writerParameters.StrongNameKeyBlob = File.ReadAllBytes(strongNameKey);
asm.Write(output, writerParameters);
return (true, true);
}
static bool? CompileCore(IBuildEngine engine, CecilTypeSystem typeSystem)
{
var asm = typeSystem.TargetAssemblyDefinition;
var embrsc = new EmbeddedResources(asm);
if (embrsc.Resources.Count(CheckXamlName) == 0)
// Nothing to do
return null;
var xamlLanguage = new XamlLanguageTypeMappings(typeSystem)
{
XmlnsAttributes =
{
typeSystem.GetType("Avalonia.Metadata.XmlnsDefinitionAttribute"),
},
ContentAttributes =
{
typeSystem.GetType("Avalonia.Metadata.ContentAttribute")
},
UsableDuringInitializationAttributes =
{
typeSystem.GetType("Robust.Client.UserInterface.XAML.UsableDuringInitializationAttribute")
},
DeferredContentPropertyAttributes =
{
typeSystem.GetType("Robust.Client.UserInterface.XAML.DeferredContentAttribute")
},
RootObjectProvider = typeSystem.GetType("Robust.Client.UserInterface.XAML.ITestRootObjectProvider"),
UriContextProvider = typeSystem.GetType("Robust.Client.UserInterface.XAML.ITestUriContext"),
ProvideValueTarget = typeSystem.GetType("Robust.Client.UserInterface.XAML.ITestProvideValueTarget"),
};
var emitConfig = new XamlLanguageEmitMappings<IXamlILEmitter, XamlILNodeEmitResult>
{
ContextTypeBuilderCallback = (b,c) => EmitNameScopeField(xamlLanguage, typeSystem, b, c)
};
var transformerconfig = new TransformerConfiguration(
typeSystem,
typeSystem.TargetAssembly,
xamlLanguage,
XamlXmlnsMappings.Resolve(typeSystem, xamlLanguage), CustomValueConverter);
var contextDef = new TypeDefinition("CompiledRobustXaml", "XamlIlContext",
TypeAttributes.Class, asm.MainModule.TypeSystem.Object);
asm.MainModule.Types.Add(contextDef);
var contextClass = XamlILContextDefinition.GenerateContextClass(typeSystem.CreateTypeBuilder(contextDef), typeSystem,
xamlLanguage, emitConfig);
var compiler =
new RobustXamlILCompiler(transformerconfig, emitConfig, true);
bool CompileGroup(IResourceGroup group)
{
var typeDef = new TypeDefinition("CompiledRobustXaml", "!" + group.Name, TypeAttributes.Class,
asm.MainModule.TypeSystem.Object);
//typeDef.CustomAttributes.Add(new CustomAttribute(ed));
asm.MainModule.Types.Add(typeDef);
var builder = typeSystem.CreateTypeBuilder(typeDef);
foreach (var res in group.Resources.Where(CheckXamlName))
{
try
{
engine.LogMessage($"XAMLIL: {res.Name} -> {res.Uri}", MessageImportance.Low);
var xaml = new StreamReader(new MemoryStream(res.FileContents)).ReadToEnd();
var parsed = XDocumentXamlParser.Parse(xaml);
var initialRoot = (XamlAstObjectNode) parsed.Root;
var classDirective = initialRoot.Children.OfType<XamlAstXmlDirective>()
.FirstOrDefault(d => d.Namespace == XamlNamespaces.Xaml2006 && d.Name == "Class");
string classname;
if (classDirective != null && classDirective.Values[0] is XamlAstTextNode tn)
{
classname = tn.Text;
}
else
{
classname = res.Name.Replace(".xaml","");
}
var classType = typeSystem.TargetAssembly.FindType(classname);
if (classType == null)
throw new Exception($"Unable to find type '{classname}'");
compiler.Transform(parsed);
var populateName = $"Populate:{res.Name}";
var buildName = $"Build:{res.Name}";
var classTypeDefinition = typeSystem.GetTypeReference(classType).Resolve();
var populateBuilder = typeSystem.CreateTypeBuilder(classTypeDefinition);
compiler.Compile(parsed, contextClass,
compiler.DefinePopulateMethod(populateBuilder, parsed, populateName,
classTypeDefinition == null),
compiler.DefineBuildMethod(builder, parsed, buildName, true),
null,
(closureName, closureBaseType) =>
populateBuilder.DefineSubType(closureBaseType, closureName, false),
res.Uri, res
);
//add compiled populate method
var compiledPopulateMethod = typeSystem.GetTypeReference(populateBuilder).Resolve().Methods
.First(m => m.Name == populateName);
const string TrampolineName = "!XamlIlPopulateTrampoline";
var trampoline = new MethodDefinition(TrampolineName,
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 RobustXamlLoader.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].OpCode == OpCodes.Call)
{
var op = i[c].Operand as MethodReference;
if (op != null
&& op.Name == TrampolineName)
{
foundXamlLoader = true;
break;
}
if (op != null
&& op.Name == "Load"
&& op.Parameters.Count == 1
&& op.Parameters[0].ParameterType.FullName == "System.Object"
&& op.DeclaringType.FullName == "Robust.Client.UserInterface.XAML.RobustXamlLoader")
{
if (MatchThisCall(i, c - 1))
{
i[c].Operand = trampoline;
foundXamlLoader = true;
}
}
}
}
}
if (!foundXamlLoader)
{
var ctors = classTypeDefinition.GetConstructors()
.Where(c => !c.IsStatic).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 RobustXamlLoader.Load(this) call found anywhere in the type {classType.FullName} and type seems to have custom constructors.");
}
}
}
catch (Exception e)
{
engine.LogErrorEvent(new BuildErrorEventArgs("XAMLIL", "", res.FilePath, 0, 0, 0, 0,
$"{res.FilePath}: {e.Message}", "", "CompileRobustXaml"));
}
res.Remove();
}
return true;
}
if (embrsc.Resources.Count(CheckXamlName) != 0)
{
if (!CompileGroup(embrsc))
return false;
}
return true;
}
private static bool CustomValueConverter(
AstTransformationContext context,
IXamlAstValueNode node,
IXamlType type,
out IXamlAstValueNode result)
{
if (!(node is XamlAstTextNode textNode))
{
result = null;
return false;
}
var text = textNode.Text;
var types = context.GetRobustTypes();
if (type.Equals(types.Vector2))
{
var foo = MathParsing.Single2.Parse(text);
if (!foo.Success)
throw new XamlLoadException($"Unable to parse \"{text}\" as a Vector2", node);
var (x, y) = foo.Value;
result = new RXamlSingleVecLikeConstAstNode(
node,
types.Vector2, types.Vector2ConstructorFull,
types.Single, new[] {x, y});
return true;
}
if (type.Equals(types.Thickness))
{
var foo = MathParsing.Thickness.Parse(text);
if (!foo.Success)
throw new XamlLoadException($"Unable to parse \"{text}\" as a Thickness", node);
var val = foo.Value;
float[] full;
if (val.Length == 1)
{
var u = val[0];
full = new[] {u, u, u, u};
}
else if (val.Length == 2)
{
var h = val[0];
var v = val[1];
full = new[] {h, v, h, v};
}
else // 4
{
full = val;
}
result = new RXamlSingleVecLikeConstAstNode(
node,
types.Thickness, types.ThicknessConstructorFull,
types.Single, full);
return true;
}
if (type.Equals(types.Thickness))
{
var foo = MathParsing.Thickness.Parse(text);
if (!foo.Success)
throw new XamlLoadException($"Unable to parse \"{text}\" as a Thickness", node);
var val = foo.Value;
float[] full;
if (val.Length == 1)
{
var u = val[0];
full = new[] {u, u, u, u};
}
else if (val.Length == 2)
{
var h = val[0];
var v = val[1];
full = new[] {h, v, h, v};
}
else // 4
{
full = val;
}
result = new RXamlSingleVecLikeConstAstNode(
node,
types.Thickness, types.ThicknessConstructorFull,
types.Single, full);
return true;
}
if (type.Equals(types.Color))
{
// TODO: Interpret these colors at XAML compile time instead of at runtime.
result = new RXamlColorAstNode(node, types, text);
return true;
}
result = null;
return false;
}
public const string ContextNameScopeFieldName = "RobustNameScope";
private static void EmitNameScopeField(XamlLanguageTypeMappings xamlLanguage, CecilTypeSystem typeSystem, IXamlTypeBuilder<IXamlILEmitter> typeBuilder, IXamlILEmitter constructor)
{
var nameScopeType = typeSystem.FindType("Robust.Client.UserInterface.XAML.NameScope");
var field = typeBuilder.DefineField(nameScopeType,
ContextNameScopeFieldName, true, false);
constructor
.Ldarg_0()
.Newobj(nameScopeType.GetConstructor())
.Stfld(field);
}
}
interface IResource : IFileSource
{
string Uri { get; }
string Name { get; }
void Remove();
}
interface IResourceGroup
{
string Name { get; }
IEnumerable<IResource> Resources { get; }
}
}

View File

@@ -26,6 +26,7 @@ using Robust.Client.Upload;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.RichText;
using Robust.Client.UserInterface.Themes;
using Robust.Client.UserInterface.XAML.Proxy;
using Robust.Client.Utility;
using Robust.Client.ViewVariables;
using Robust.Shared;
@@ -146,6 +147,16 @@ namespace Robust.Client
deps.Register<IConfigurationManagerInternal, ClientNetConfigurationManager>();
deps.Register<IClientNetConfigurationManager, ClientNetConfigurationManager>();
deps.Register<INetConfigurationManagerInternal, ClientNetConfigurationManager>();
#if TOOLS
deps.Register<IXamlProxyManager, XamlProxyManager>();
deps.Register<IXamlHotReloadManager, XamlHotReloadManager>();
#else
deps.Register<IXamlProxyManager, XamlProxyManagerStub>();
deps.Register<IXamlHotReloadManager, XamlHotReloadManagerStub>();
#endif
deps.Register<IXamlProxyHelper, XamlProxyHelper>();
deps.Register<MarkupTagManager>();
}
}

View File

@@ -19,6 +19,7 @@ using Robust.Client.State;
using Robust.Client.Upload;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.RichText;
using Robust.Client.UserInterface.XAML.Proxy;
using Robust.Client.Utility;
using Robust.Client.ViewVariables;
using Robust.Client.WebViewHook;
@@ -53,6 +54,8 @@ namespace Robust.Client
[Dependency] private readonly IResourceCacheInternal _resourceCache = default!;
[Dependency] private readonly IResourceManagerInternal _resManager = default!;
[Dependency] private readonly IRobustSerializer _serializer = default!;
[Dependency] private readonly IXamlProxyManager _xamlProxyManager = default!;
[Dependency] private readonly IXamlHotReloadManager _xamlHotReloadManager = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IClientNetManager _networkManager = default!;
[Dependency] private readonly IMapManager _mapManager = default!;
@@ -171,6 +174,8 @@ namespace Robust.Client
_reflectionManager.Initialize();
_prototypeManager.Initialize();
_prototypeManager.LoadDefaultPrototypes();
_xamlProxyManager.Initialize();
_xamlHotReloadManager.Initialize();
_userInterfaceManager.Initialize();
_eyeManager.Initialize();
_entityManager.Initialize();

View File

@@ -44,6 +44,10 @@
<ProjectReference Include="..\Robust.Shared\Robust.Shared.csproj" />
</ItemGroup>
<ItemGroup Condition="'$(RobustToolsBuild)' == 'True'">
<ProjectReference Include="..\Robust.Xaml\Robust.Xaml.csproj" />
</ItemGroup>
<!-- Shader embedding -->
<ItemGroup>
<EmbeddedResource Include="Graphics\Clyde\Shaders\*" />

View File

@@ -0,0 +1,20 @@
namespace Robust.Client.UserInterface.XAML.Proxy;
/// <summary>
/// This service locates the SS14 source tree and watches for changes to its xaml files.
/// </summary>
/// <remarks>
/// It then reloads them instantly.
///
/// It depends on <see cref="IXamlProxyManager"/> and is stubbed on non-TOOLS builds.
/// </remarks>
interface IXamlHotReloadManager
{
/// <summary>
/// Initialize the hot reload manager.
/// </summary>
/// <remarks>
/// You can't do anything with this once it's started, including turn it off.
/// </remarks>
void Initialize();
}

View File

@@ -0,0 +1,11 @@
using System;
namespace Robust.Client.UserInterface.XAML.Proxy;
/// <summary>
/// Reexport the Populate method of <see cref="IXamlProxyManager"/> and nothing else.
/// </summary>
public interface IXamlProxyHelper
{
bool Populate(Type t, object o);
}

View File

@@ -0,0 +1,76 @@
using System;
namespace Robust.Client.UserInterface.XAML.Proxy;
/// <summary>
/// This service provides a proxy for Populate, which is the generated function that
/// initializes the UI objects of a Xaml widget.
/// </summary>
/// <remarks>
/// The proxy can always return false: in that case, a Xaml widget will self-populate
/// as usual. This is the behavior on Release builds.
///
/// However, it can also call into an externally-provided implementation of the Xaml
/// widget.
///
/// No source of externally-provided implementations actually exists, by default --
/// you will need to call SetImplementation with a blob of xaml source code to provide
/// one. <see cref="IXamlHotReloadManager" /> is an example of a service that calls into
/// that functionality.
/// </remarks>
internal interface IXamlProxyManager
{
/// <summary>
/// Initialize creates the <see cref="IXamlProxyManager"/>.
/// </summary>
/// <remarks>
/// If the <see cref="IXamlProxyManager" /> is not a stub, then it will spy on the
/// assembly list (from <see cref="Robust.Shared.Reflection.IReflectionManager" />)
/// and find <see cref="XamlMetadataAttribute" /> entries on the loaded types.
/// </remarks>
void Initialize();
/// <summary>
/// Return true if at least one <see cref="Type"/> in the current project expects its XAML
/// to come from a file with the given name.
/// </summary>
/// <remarks>
/// This method supports code that is trying to figure out what name the build process
/// would have assigned to a resource file. A caller can try a few candidate names and use
/// its "yes" to continue.
///
/// This method is very fast, so it's OK to hammer it!
///
/// Also, on a non-tools build, this always returns false.
/// </remarks>
/// <param name="fileName">the filename</param>
/// <returns>true if expected</returns>
bool CanSetImplementation(string fileName);
/// <summary>
/// Replace the implementation of <paramref name="fileName"/> with <paramref name="fileContent" />,
/// compiling it if needed.
///
/// All types based on <paramref name="fileName" /> will be recompiled.
/// </summary>
/// <remarks>
/// This may fail and the caller won't be notified. (There will usually be logs.)
///
/// On a non-tools build, this fails silently.
/// </remarks>
/// <param name="fileName">the name of the file</param>
/// <param name="fileContent">the new content of the file</param>
void SetImplementation(string fileName, string fileContent);
/// <summary>
/// If we have a JIT version of the XAML code for <paramref name="t" />, then call
/// the new implementation on <paramref name="o" />.
/// </summary>
/// <remarks>
/// <paramref name="o" /> may be a subclass of <paramref name="t" />.
/// </remarks>
/// <param name="t">the static type of the object</param>
/// <param name="o">the object</param>
/// <returns>true if we called a hot reloaded implementation</returns>
bool Populate(Type t, object o);
}

View File

@@ -0,0 +1,195 @@
#if TOOLS
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Robust.Shared.Asynchronous;
using Robust.Shared.ContentPack;
using Robust.Shared.IoC;
using Robust.Shared.Log;
namespace Robust.Client.UserInterface.XAML.Proxy;
/// <summary>
/// The real implementation of <see cref="IXamlHotReloadManager" />.
/// </summary>
/// <remarks>
/// Its behavior is described there.
/// </remarks>
internal sealed class XamlHotReloadManager : IXamlHotReloadManager
{
private const string MarkerFileName = "SpaceStation14.sln";
[Dependency] ILogManager _logManager = null!;
[Dependency] private readonly IResourceManager _resources = null!;
[Dependency] private readonly ITaskManager _taskManager = null!;
[Dependency] private readonly IXamlProxyManager _xamlProxyManager = null!;
private ISawmill _sawmill = null!;
private FileSystemWatcher? _watcher;
public void Initialize()
{
_sawmill = _logManager.GetSawmill("xamlhotreload");
var codeLocation = InferCodeLocation();
if (codeLocation == null)
{
_sawmill.Warning($"could not find code -- where is {MarkerFileName}?");
return;
}
_sawmill.Info($"code location: {codeLocation}");
// must not be gc'ed or else it will stop reporting
// therefore: keep a reference
_watcher = CreateWatcher(codeLocation);
}
/// <summary>
/// Create a file system watcher that identifies XAML changes in a given
/// location.
/// </summary>
/// <param name="location">the location (a real path on the OS file system)</param>
/// <returns>the new watcher</returns>
/// <exception cref="ArgumentOutOfRangeException">if <see cref="FileSystemWatcher"/> violates its type-related postconditions</exception>
private FileSystemWatcher CreateWatcher(string location)
{
var watcher = new FileSystemWatcher(location)
{
IncludeSubdirectories = true,
NotifyFilter = NotifyFilters.LastWrite,
};
watcher.Changed += (_, args) =>
{
switch (args.ChangeType)
{
case WatcherChangeTypes.Renamed:
case WatcherChangeTypes.Deleted:
return;
case WatcherChangeTypes.Created:
case WatcherChangeTypes.Changed:
case WatcherChangeTypes.All:
break;
default:
throw new ArgumentOutOfRangeException(nameof(args));
}
_taskManager.RunOnMainThread(() =>
{
var resourceFileName =
ResourceFileName(location, args.FullPath, _xamlProxyManager.CanSetImplementation);
if (resourceFileName == null)
{
return;
}
string newText;
try
{
newText = File.ReadAllText(args.FullPath);
}
catch (IOException ie)
{
_sawmill.Warning($"error attempting a hot reload -- skipped: {ie}");
return;
}
_xamlProxyManager.SetImplementation(resourceFileName, newText);
});
};
watcher.EnableRaisingEvents = true;
return watcher;
}
/// <summary>
/// Using the content roots of the project, infer the location of its code.
/// </summary>
/// <remarks>
/// This kind of introspection is almost universally a bad idea, but we don't
/// feasibly have other options, so I've buried it in a private method.
/// </remarks>
/// <returns>the inferred code location or null</returns>
private string? InferCodeLocation()
{
// ascend upwards from each content root until the solution file is found
foreach (var contentRoot in _resources.GetContentRoots())
{
var systemPath = contentRoot.ToRelativeSystemPath();
while (true)
{
var files = Array.Empty<string>();
try
{
files = Directory.GetFiles(systemPath);
}
catch (IOException) { } // this is allowed to fail, and if so we just keep going up
if (files.Any(f => Path.GetFileName(f).Equals(MarkerFileName, StringComparison.InvariantCultureIgnoreCase)))
{
return systemPath;
}
DirectoryInfo? newPath = null;
try
{
newPath = Directory.GetParent(systemPath);
}
catch (IOException) { } // ditto here. if we don't find it, we're in the wrong place
if (newPath == null)
{
break;
}
systemPath = newPath.FullName;
}
}
return null;
}
/// <summary>
/// Infer the name of the resource file associated with the XAML item at the given path.
/// </summary>
/// <param name="codeLocation">the code location</param>
/// <param name="realPath">the real path of the file</param>
/// <param name="isDesired">a function returning true if something expects this file</param>
/// <returns>the name of a desired resource that matches this file, or null</returns>
private string? ResourceFileName(string codeLocation, string realPath, Predicate<string> isDesired)
{
// start with the name of the file and systematically add each super-directory until we reach
// the inferred code location.
//
// for /home/pyrex/ss14/Content.Client/Instruments/UI/InstrumentMenu.xaml, the following names
// will be tried:
//
// - InstrumentMenu.xaml
// - UI.InstrumentMenu.xaml
// - Instruments.UI.InstrumentMenu.xaml
// - Content.Client.Instruments.UI.InstrumentMenu.xaml
var resourceFileName = Path.GetFileName(realPath);
var super = Directory.GetParent(realPath);
var canonicalCodeLocation = Path.GetFullPath(codeLocation);
while (true)
{
// did someone want it: OK, jump out
if (isDesired(resourceFileName))
{
return resourceFileName;
}
if (super == null || Path.GetFullPath(super.FullName) == canonicalCodeLocation)
{
return null;
}
resourceFileName = super.Name + "." + resourceFileName;
super = super.Parent;
}
}
}
#endif

View File

@@ -0,0 +1,15 @@
namespace Robust.Client.UserInterface.XAML.Proxy;
/// <summary>
/// A stub implementation of <see cref="XamlHotReloadManager"/>. Its
/// behavior is to do nothing.
/// </summary>
internal sealed class XamlHotReloadManagerStub : IXamlHotReloadManager
{
/// <summary>
/// Do nothing.
/// </summary>
public void Initialize()
{
}
}

View File

@@ -0,0 +1,221 @@
#if TOOLS
using System;
using System.Collections.Generic;
using System.Reflection;
using Robust.Shared.Log;
using Robust.Xaml;
namespace Robust.Client.UserInterface.XAML.Proxy;
/// <summary>
/// This is a utility class that tracks the relationship between resource file names,
/// Xamlx-compatible <see cref="Uri"/>s, <see cref="Type"/>s that are interested in a
/// given file, and implementations of Populate.
/// </summary>
internal sealed class XamlImplementationStorage
{
/// <summary>
/// For each filename, we store its last known <see cref="Uri"/>.
/// </summary>
/// <remarks>
/// When we compile the new implementation, we will use the same <see cref="Uri"/>.
/// </remarks>
private readonly Dictionary<string, Uri> _fileUri = new();
/// <summary>
/// For each filename, we store its last known content.
/// </summary>
/// <remarks>
/// This is known even for AOT-compiled code -- therefore, we can use this table
/// to convert an AOT-compiled Control to a JIT-compiled one.
/// </remarks>
private readonly Dictionary<string, string> _fileContent = new();
/// <summary>
/// For each filename, we store the type interested in this file.
/// </summary>
private readonly Dictionary<string, Type> _fileType = new();
/// <summary>
/// For each type, store the JIT-compiled implementation of Populate.
/// </summary>
/// <remarks>
/// If no such implementation exists, then methods that would normally
/// find and call a JIT'ed implementation will do nothing and return
/// false instead. As an ultimate result, the AOT'ed implementation
/// will be used.
/// </remarks>
private readonly Dictionary<Type, MethodInfo> _populateImplementations = new();
private readonly ISawmill _sawmill;
private readonly XamlJitDelegate _jitDelegate;
/// <summary>
/// Create the storage.
/// </summary>
/// <remarks>
/// It would be weird to call this from any type outside of
/// <see cref="Robust.Client.UserInterface.XAML.Proxy" />.
/// </remarks>
/// <param name="sawmill">the (shared) logger</param>
/// <param name="jitDelegate">
/// a delegate that calls the
/// <see cref="XamlJitCompiler"/>, possibly handling errors
/// </param>
public XamlImplementationStorage(ISawmill sawmill, XamlJitDelegate jitDelegate)
{
_sawmill = sawmill;
_jitDelegate = jitDelegate;
}
/// <summary>
/// Inspect <paramref name="assembly" /> for types that declare a <see cref="XamlMetadataAttribute"/>.
/// </summary>
/// <remarks>
/// We can only do hot reloading if we know this basic information.
///
/// Note that even release-mode content artifacts contain this attribute.
/// </remarks>
/// <param name="assembly">the assembly</param>
/// <returns>an IEnumerable of types with xaml metadata</returns>
private IEnumerable<(Type, XamlMetadataAttribute)> TypesWithXamlMetadata(Assembly assembly)
{
foreach (var type in assembly.GetTypes())
{
if (type.GetCustomAttribute<XamlMetadataAttribute>() is not { } attr)
{
continue;
}
yield return (type, attr);
}
}
/// <summary>
/// Add all Xaml-annotated types from <paramref name="assembly" /> to this storage.
/// </summary>
/// <remarks>
/// We don't JIT these types, but we store enough info that we could JIT
/// them if we wanted to.
/// </remarks>
/// <param name="assembly">an assembly</param>
public void Add(Assembly assembly)
{
foreach (var (type, metadata) in TypesWithXamlMetadata(assembly))
{
// this can fail, but if it does, that means something is _really_ wrong
// with the compiler, or someone tried to write their own Xaml metadata
Uri uri;
try
{
uri = new Uri(metadata.Uri);
}
catch (UriFormatException)
{
throw new InvalidProgramException(
$"XamlImplementationStorage encountered an malformed Uri in the metadata for {type.FullName}: " +
$"{metadata.Uri}. this is a bug in XamlAotCompiler"
);
}
var fileName = metadata.FileName;
var content = metadata.Content;
_fileUri[fileName] = uri;
_fileContent[fileName] = content;
if (!_fileType.TryAdd(fileName, type))
{
throw new InvalidProgramException(
$"XamlImplementationStorage observed that two types were interested in the same Xaml filename: " +
$"{fileName}. ({type.FullName} and {_fileType[fileName].FullName}). this is a bug in XamlAotCompiler"
);
}
}
}
/// <summary>
/// Quietly JIT every type with XAML metadata.
/// </summary>
/// <remarks>
/// This should have no visible effect except that the <see cref="XamlJitDelegate"/>
/// may dump some info messages into the terminal about cases where the
/// hot reload failed.
/// </remarks>
public void ForceReloadAll()
{
foreach (var (fileName, fileContent) in _fileContent)
{
SetImplementation(fileName, fileContent, true);
}
}
/// <summary>
/// Return true if calling <see cref="SetImplementation" /> on <paramref name="fileName" /> would not be a no-op.
/// </summary>
/// <remarks>
/// That is: if some type cares about the contents of <paramref name="fileName" />.
/// </remarks>
/// <param name="fileName">the filename</param>
/// <returns>true if not a no-op</returns>
public bool CanSetImplementation(string fileName)
{
return _fileType.ContainsKey(fileName);
}
/// <summary>
/// Replace the implementation of <paramref name="fileName"/> by JIT-ing
/// <paramref name="fileContent"/>.
/// </summary>
/// <remarks>
/// If nothing cares about the implementation of <paramref name="fileName"/>, then this will do nothing.
/// </remarks>
/// <param name="fileName">the name of the file whose implementation should be replaced</param>
/// <param name="fileContent">the new implementation</param>
/// <param name="quiet">if true, then don't bother to log</param>
public void SetImplementation(string fileName, string fileContent, bool quiet)
{
if (!_fileType.TryGetValue(fileName, out var type))
{
_sawmill.Warning($"SetImplementation called with {fileName}, but no types care about its contents");
return;
}
var uri =
_fileUri.GetValueOrDefault(fileName) ??
throw new InvalidProgramException("file URI missing (this is a bug in ImplementationStorage)");
if (!quiet)
{
_sawmill.Debug($"replacing {fileName} for {type}");
}
var impl = _jitDelegate(type, uri, fileName, fileContent);
if (impl != null)
{
_populateImplementations[type] = impl;
}
_fileContent[fileName] = fileContent;
}
/// <summary>
/// Call the JITed implementation of Populate on a XAML-associated object <paramref name="o"/>.
///
/// If no JITed implementation exists, return false.
/// </summary>
/// <param name="t">the static type of <paramref name="o"/></param>
/// <param name="o">an instance of <paramref name="t"/> (can be a subclass)</param>
/// <returns>true if a JITed implementation existed</returns>
public bool Populate(Type t, object o)
{
if (!_populateImplementations.TryGetValue(t, out var implementation))
{
// pop out if we never JITed anything
return false;
}
implementation.Invoke(null, [null, o]);
return true;
}
}
#endif

View File

@@ -0,0 +1,17 @@
using System;
using System.Reflection;
namespace Robust.Client.UserInterface.XAML.Proxy;
/// <summary>
/// This callback has the approximate type of <see cref="Robust.Xaml.XamlJitCompiler.Compile"/>,
/// but it has no error-signaling faculty.
/// </summary>
/// <remarks>
/// Implementors of this delegate should inform the users of errors in their own way.
///
/// Hot reloading failures should not directly take down the process, so implementors
/// should not rethrow exceptions unless they have a strong reason to believe they
/// will be caught.
/// </remarks>
internal delegate MethodInfo? XamlJitDelegate(Type type, Uri uri, string filename, string content);

View File

@@ -0,0 +1,29 @@
using System;
namespace Robust.Client.UserInterface.XAML.Proxy;
/// <summary>
/// Metadata to support JIT compilation of XAML resources for a type.
/// </summary>
/// <remarks>
/// We can feed XamlX data from this type, along with new content, to get new XAML
/// resources.
///
/// This type is inert and is generated for release artifacts too, not just debug
/// artifacts. Released content should support hot reloading if loaded in a debug
/// client, but this is untested.
/// </remarks>
[AttributeUsage(validOn: AttributeTargets.Class, Inherited = false)]
public sealed class XamlMetadataAttribute: System.Attribute
{
public readonly string Uri;
public readonly string FileName;
public readonly string Content;
public XamlMetadataAttribute(string uri, string fileName, string content)
{
Uri = uri;
FileName = fileName;
Content = content;
}
}

View File

@@ -0,0 +1,14 @@
using System;
using Robust.Shared.IoC;
namespace Robust.Client.UserInterface.XAML.Proxy;
internal sealed class XamlProxyHelper: IXamlProxyHelper
{
[Dependency] private IXamlProxyManager _xamlProxyManager = default!;
public bool Populate(Type t, object o)
{
return _xamlProxyManager.Populate(t, o);
}
}

View File

@@ -0,0 +1,127 @@
#if TOOLS
using System;
using System.Collections.Generic;
using System.Reflection;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Reflection;
using Robust.Xaml;
namespace Robust.Client.UserInterface.XAML.Proxy;
/// <summary>
/// The real implementation of <see cref="IXamlProxyManager"/>.
/// </summary>
public sealed class XamlProxyManager: IXamlProxyManager
{
ISawmill _sawmill = null!;
[Dependency] IReflectionManager _reflectionManager = null!;
[Dependency] ILogManager _logManager = null!;
XamlImplementationStorage _xamlImplementationStorage = null!;
List<Assembly> _knownAssemblies = [];
XamlJitCompiler? _xamlJitCompiler;
/// <summary>
/// Initialize this, subscribing to assembly changes.
/// </summary>
public void Initialize()
{
_sawmill = _logManager.GetSawmill("xamlhotreload");
_xamlImplementationStorage = new XamlImplementationStorage(_sawmill, Compile);
AddAssemblies();
_reflectionManager.OnAssemblyAdded += (_, _) => { AddAssemblies(); };
}
/// <summary>
/// Return true if setting the implementation of <paramref name="fileName" />
/// would not be a no-op.
/// </summary>
/// <param name="fileName">the file name</param>
/// <returns>true or false</returns>
public bool CanSetImplementation(string fileName)
{
return _xamlImplementationStorage.CanSetImplementation(fileName);
}
/// <summary>
/// Replace the implementation of <paramref name="fileName" />, failing
/// silently if the new content does not compile. (but still logging)
/// </summary>
/// <param name="fileName">the file name</param>
/// <param name="fileContent">the new content</param>
public void SetImplementation(string fileName, string fileContent)
{
_xamlImplementationStorage.SetImplementation(fileName, fileContent, false);
}
/// <summary>
/// Add all the types from all known assemblies, then force-JIT everything
/// again.
/// </summary>
private void AddAssemblies()
{
foreach (var a in _reflectionManager.Assemblies)
{
if (!_knownAssemblies.Contains(a))
{
_knownAssemblies.Add(a);
_xamlImplementationStorage.Add(a);
_xamlJitCompiler = null;
}
}
// Always use the JITed versions on debug builds
_xamlImplementationStorage.ForceReloadAll();
}
/// <summary>
/// Populate <paramref name="o" /> using the JIT compiler, if possible.
/// </summary>
/// <param name="t">the static type of <paramref name="o" /></param>
/// <param name="o">a <paramref name="t" /> instance or subclass</param>
/// <returns>true if there was a JITed implementation</returns>
public bool Populate(Type t, object o)
{
return _xamlImplementationStorage.Populate(t, o);
}
/// <summary>
/// Calls <see cref="XamlJitCompiler.Compile"/> using a stored
/// <see cref="XamlJitCompiler"/> instance.
/// </summary>
/// <param name="t">the <see cref="Type"/> that cares about this Xaml</param>
/// <param name="uri">the <see cref="Uri" /> of this xaml (from the type's metadata)</param>
/// <param name="fileName">the filename of this xaml (from the type's metadata)</param>
/// <param name="content">the new content of the xaml file</param>
/// <returns>the MethodInfo for the new JITed implementation</returns>
private MethodInfo? Compile(Type t, Uri uri, string fileName, string content)
{
// initialize XamlJitCompiler lazily because constructing it has
// very high CPU cost
XamlJitCompiler xjit;
lock(this)
{
xjit = _xamlJitCompiler ??= new XamlJitCompiler();
}
var result = xjit.Compile(t, uri, fileName, content);
if (result is XamlJitCompilerResult.Error e)
{
_sawmill.Info($"hot reloading failed: {t.FullName}; {fileName}; {e.Raw.Message} {e.Hint ?? ""}");
return null;
}
if (result is XamlJitCompilerResult.Success s)
{
return s.MethodInfo;
}
throw new InvalidOperationException($"totally unexpected result from compiler operation: {result}");
}
}
#endif

View File

@@ -0,0 +1,50 @@
using System;
namespace Robust.Client.UserInterface.XAML.Proxy;
/// <summary>
/// The stub implementation of <see cref="IXamlProxyManager"/>.
/// </summary>
public sealed class XamlProxyManagerStub: IXamlProxyManager
{
/// <summary>
/// Do nothing.
/// </summary>
public void Initialize()
{
}
/// <summary>
/// Return false. Nothing is ever interested in a Xaml content update when
/// hot reloading is off.
/// </summary>
/// <param name="fileName">the filename</param>
/// <returns>false</returns>
public bool CanSetImplementation(string fileName)
{
return false;
}
/// <summary>
/// Do nothing. A hot reload will always silently fail if hot reloading is off.
/// </summary>
/// <param name="fileName"></param>
/// <param name="fileContent"></param>
public void SetImplementation(string fileName, string fileContent)
{
}
/// <summary>
/// Return false.
/// </summary>
/// <remarks>
/// There will never be a JIT-ed implementation of Populate if hot reloading is off.
/// </remarks>
/// <param name="t">the static type of <paramref name="o" /></param>
/// <param name="o">an instance of <paramref name="t" /> or a subclass</param>
/// <returns>false</returns>
public bool Populate(Type t, object o)
{
return false;
}
}

View File

@@ -10,6 +10,8 @@ AllowedVerifierErrors:
- InterfaceMethodNotImplemented
# EVERYTHING in these namespaces is allowed.
# Note that, due to a historical bug in the sandbox, any namespace _prefixed_ with one of these
# is also allowed. (For instance, RobustBats.X, or ContentFarm.Y)
WhitelistedNamespaces:
- Robust
- Content

View File

@@ -17,6 +17,7 @@ using Robust.Client.GameStates;
using Robust.Client.Player;
using Robust.Client.Timing;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.XAML.Proxy;
using Robust.Server;
using Robust.Server.Console;
using Robust.Server.GameStates;
@@ -929,6 +930,8 @@ namespace Robust.UnitTesting
deps.Register<IClientConsoleHost, TestingClientConsoleHost>(true);
deps.Register<IConsoleHost, TestingClientConsoleHost>(true);
deps.Register<IConsoleHostInternal, TestingClientConsoleHost>(true);
deps.Register<IXamlProxyManager, XamlProxyManagerStub>(true);
deps.Register<IXamlHotReloadManager, XamlHotReloadManagerStub>(true);
Options?.InitIoC?.Invoke();
deps.BuildGraph();

View File

@@ -0,0 +1,4 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Robust.Client")]
[assembly: InternalsVisibleTo("Robust.Client.Injectors")]

View File

@@ -0,0 +1,32 @@
using System;
using Mono.Cecil;
namespace Robust.Xaml;
/// <summary>
/// Source: https://github.com/jbevain/cecil/blob/master/Test/Mono.Cecil.Tests/Extensions.cs
/// </summary>
internal static class CecilExtensions
{
/// <summary>
/// Specialize some generic parameters of a reference to a generic method. The return value
/// is a more monomorphized version.
/// </summary>
/// <param name="self">the original MethodReference</param>
/// <param name="arguments">the specialized arguments</param>
/// <returns>the monomorphic MethodReference</returns>
/// <exception cref="ArgumentException">if the number of passed arguments is wrong</exception>
public static MethodReference MakeGenericMethod(this MethodReference self, params TypeReference[] arguments)
{
if (self.GenericParameters.Count != arguments.Length)
throw new ArgumentException();
var instance = new GenericInstanceMethod(self);
foreach (var argument in arguments)
{
instance.GenericArguments.Add(argument);
}
return instance;
}
}

View File

@@ -1,11 +1,11 @@
using Microsoft.Build.Framework;
namespace Robust.Build.Tasks
namespace Robust.Xaml
{
/// <summary>
/// Taken from https://github.com/AvaloniaUI/Avalonia/blob/c85fa2b9977d251a31886c2534613b4730fbaeaf/src/Avalonia.Build.Tasks/Extensions.cs
/// </summary>
public static class Extensions
internal static class Extensions
{
//shamefully copied from avalonia
public static void LogMessage(this IBuildEngine engine, string message, MessageImportance imp)

View File

@@ -0,0 +1,249 @@
using System;
using System.Linq;
using Mono.Cecil;
using Mono.Cecil.Cil;
using Mono.Cecil.Rocks;
using Mono.Collections.Generic;
using XamlX.TypeSystem;
namespace Robust.Xaml;
/// <summary>
/// Class that performs find/replace operations on IL in assemblies that contain
/// SS14 content.
/// </summary>
/// <remarks>
/// This code used to live in Robust.Client.Injectors.
///
/// Paul Ritter wrote a lot of code that does low-level Cecil based patching
/// of AoT-compiled XamlX code.
///
/// That's "fine" (it's not actually fine) -- this class just moves that all
/// to one place, and removes the extremely verbose Cecil-based type lookups
/// to a separate shared location.
/// </remarks>
internal sealed class LowLevelCustomizations
{
public const string TrampolineName = "!XamlIlPopulateTrampoline";
public const int ExpectedNMetadataArgs = 3;
private readonly CecilTypeSystem _typeSystem;
private readonly AssemblyDefinition _asm;
private readonly TypeDefinition _iocManager;
private readonly TypeDefinition _iXamlProxyHelper;
private readonly TypeDefinition _systemType;
private readonly TypeDefinition _stringType;
private readonly TypeDefinition _xamlMetadataAttributeType;
private readonly MethodReference _resolveXamlProxyHelperMethod;
private readonly MethodReference _populateMethod;
private readonly MethodReference _getTypeFromHandleMethod;
private readonly MethodReference _xamlMetadataAttributeConstructor;
/// <summary>
/// Create a <see cref="LowLevelCustomizations"/> object.
/// </summary>
/// <param name="typeSystem">the <see cref="CecilTypeSystem" /></param>
/// <exception cref="NullReferenceException">if some needed types were undefined</exception>
public LowLevelCustomizations(CecilTypeSystem typeSystem)
{
// resolve every type that we look for or substitute in when doing surgery
// what a mess!
_typeSystem = typeSystem;
_asm = typeSystem.TargetAssemblyDefinition;
TypeDefinition ResolveType(string name) =>
typeSystem.GetTypeReference(_typeSystem.FindType(name)).Resolve()
?? throw new NullReferenceException($"type must exist: {name}");
_iocManager = ResolveType("Robust.Shared.IoC.IoCManager");
_iXamlProxyHelper = ResolveType(
"Robust.Client.UserInterface.XAML.Proxy.IXamlProxyHelper"
);
_resolveXamlProxyHelperMethod = _asm.MainModule.ImportReference(
_iocManager.Methods
.First(m => m.Name == "Resolve")
.MakeGenericMethod(_iXamlProxyHelper)
);
_populateMethod = _asm.MainModule.ImportReference(
_iXamlProxyHelper.Methods
.First(m => m.Name == "Populate")
);
_systemType = ResolveType("System.Type");
_getTypeFromHandleMethod = _asm.MainModule.ImportReference(
_systemType.Resolve()
.Methods
.First(m => m.Name == "GetTypeFromHandle")
);
_stringType = ResolveType("System.String");
_xamlMetadataAttributeType = ResolveType(
"Robust.Client.UserInterface.XAML.Proxy.XamlMetadataAttribute"
);
_xamlMetadataAttributeConstructor = _asm.MainModule.ImportReference(
_xamlMetadataAttributeType
.GetConstructors()
.First(
c => c.Parameters.Count == ExpectedNMetadataArgs &&
c.Parameters.All(p => p.ParameterType.FullName == "System.String")
)
);
}
/// <summary>
/// Creates a "trampoline" -- this is a method on the given subject which has the following general logic:
///
/// <code>
/// void TrampolineName(Subject subject) {
/// if (IoCManager.Resolve{XamlProxyHelper}().Populate(typeof(Subject), subject)) {
/// return;
/// }
/// aotPopulateMethod(null, subject)
/// }
/// </code>
///
/// </summary>
/// <param name="subject">the type to create a trampoline on</param>
/// <param name="aotPopulateMethod">the populate method to call if XamlProxyHelper's Populate method returns false</param>
/// <returns>the new trampoline method</returns>
private MethodDefinition CreateTrampoline(TypeDefinition subject, MethodDefinition aotPopulateMethod)
{
var trampoline = new MethodDefinition(
TrampolineName,
MethodAttributes.Static | MethodAttributes.Private,
_asm.MainModule.TypeSystem.Void
);
trampoline.Parameters.Add(new ParameterDefinition(subject));
subject.Methods.Add(trampoline);
void Emit(Instruction i) => trampoline.Body.Instructions.Add(i);
Emit(Instruction.Create(OpCodes.Call, _resolveXamlProxyHelperMethod));
Emit(Instruction.Create(OpCodes.Ldtoken, subject));
Emit(Instruction.Create(OpCodes.Call, _getTypeFromHandleMethod));
Emit(Instruction.Create(OpCodes.Ldarg_0));
Emit(Instruction.Create(OpCodes.Callvirt, _populateMethod));
var ret = Instruction.Create(OpCodes.Ret);
Emit(Instruction.Create(OpCodes.Brtrue_S, ret));
Emit(Instruction.Create(OpCodes.Ldnull));
Emit(Instruction.Create(OpCodes.Ldarg_0));
Emit(Instruction.Create(OpCodes.Call, aotPopulateMethod));
Emit(ret);
return trampoline;
}
/// <summary>
/// Creates a trampoline on <paramref name="subject" />, then replaces
/// calls to RobustXamlLoader.Load with calls to the generated trampoline.
/// Returns true if the patching succeeded.
/// </summary>
/// <param name="subject">the subject</param>
/// <param name="aotPopulateMethod">the populate method</param>
/// <returns>true</returns>
public bool TrampolineCallsToXamlLoader(TypeDefinition subject, MethodDefinition aotPopulateMethod)
{
// PYREX NOTE: This logic is brittle and has a lot of cases
// I do not understand all of them, but I have faithfully ported them
// Paul Ritter wrote most of this
var trampoline = CreateTrampoline(subject, aotPopulateMethod);
var foundXamlLoader = false;
// Find RobustXamlLoader.Load(this) and replace it with !XamlIlPopulateTrampoline(this)
foreach (var method in subject.Methods
.Where(m => !m.Attributes.HasFlag(MethodAttributes.Static)))
{
var i = method.Body.Instructions;
for (var c = 1; c < i.Count; c++)
{
if (i[c].OpCode == OpCodes.Call)
{
var op = i[c].Operand as MethodReference;
if (op != null
&& op.Name == TrampolineName)
{
foundXamlLoader = true;
break;
}
if (op != null
&& op.Name == "Load"
&& op.Parameters.Count == 1
&& op.Parameters[0].ParameterType.FullName == "System.Object"
&& op.DeclaringType.FullName == "Robust.Client.UserInterface.XAML.RobustXamlLoader")
{
if (MatchThisCall(i, c - 1))
{
i[c].Operand = trampoline;
foundXamlLoader = true;
}
}
}
}
}
if (!foundXamlLoader)
{
var ctors = subject.GetConstructors()
.Where(c => !c.IsStatic)
.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));
foundXamlLoader = true;
}
}
return foundXamlLoader;
}
private static bool MatchThisCall(Collection<Instruction> instructions, int idx)
{
var i = instructions[idx];
// A "normal" way of passing `this` to a static method:
// ldarg.0
// call void [uAvalonia.Markup.Xaml]Avalonia.Markup.Xaml.AvaloniaXamlLoader::Load(object)
return i.OpCode == OpCodes.Ldarg_0 || (i.OpCode == OpCodes.Ldarg && i.Operand?.Equals(0) == true);
}
/// <summary>
/// Add a XamlMetadataAttribute to a given type, containing all the compiler
/// parameters for its Populate method.
/// </summary>
/// <param name="subject">the subject type</param>
/// <param name="uri">the URI we generated</param>
/// <param name="filename">the filename</param>
/// <param name="content">the new content</param>
public void AddXamlMetadata(TypeDefinition subject, Uri uri, string filename, string content)
{
var attribute = new CustomAttribute(_xamlMetadataAttributeConstructor);
var args = new string[ExpectedNMetadataArgs] // reference this so that changing the number is a compile error
{
uri.ToString(), filename, content
};
foreach (var arg in args)
{
attribute.ConstructorArguments.Add(
new CustomAttributeArgument(_stringType, arg)
);
}
subject.CustomAttributes.Add(attribute);
}
}

113
Robust.Xaml/MathParsing.cs Normal file
View File

@@ -0,0 +1,113 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
namespace Robust.Xaml;
internal static class MathParsing
{
private static float[]? ParseSingleArr(string input)
{
// Transliteration note: The original patterns in this file were Pidgin parsers
// All of them were variations on Real.Select(c => (float c)).Between(SkipWhiteSpaces).Repeat(n)
// They somehow handled commas too, but I don't know how
//
// SkipWhitespace splits based on char.IsWhitespace:
// https://github.com/benjamin-hodgson/Pidgin/blob/cc72abb/Pidgin/Parser.Whitespace.cs#L30
var items = SplitStringByFunction(input, (c) => c == ',' || char.IsWhiteSpace(c));
var outs = new float[items.Count];
for (var i = 0; i < outs.Length; i++)
{
// Parser.Real ultimately resorts to double.TryParse
// https://github.com/benjamin-hodgson/Pidgin/blob/cc72abb/Pidgin/Parser.Number.cs#L222
var parsed = double.TryParse(
items[i],
NumberStyles.Float | NumberStyles.AllowExponent | NumberStyles.AllowLeadingSign,
CultureInfo.InvariantCulture,
out var d
);
if (!parsed)
{
return null;
}
outs[i] = (float)d;
}
return outs;
}
private static List<string> SplitStringByFunction(string s, Func<char, bool> isSeparator)
{
// we want to split by commas _or_ char.IsWhitespace
// C#'s Split() can do one but not both
var splitItems = new List<string>();
var itemInProgress = new StringBuilder();
foreach (var c in s)
{
if (isSeparator(c))
{
if (itemInProgress.Length > 0)
{
splitItems.Add(itemInProgress.ToString());
itemInProgress.Clear();
}
}
else
{
itemInProgress.Append(c);
}
}
if (itemInProgress.Length > 0)
{
splitItems.Add(itemInProgress.ToString());
}
return splitItems;
}
/// <summary>
/// Parse a vector of two floats separated by commas or spaces, such as
/// "1,2" or "1.5 2.5"
/// </summary>
/// <param name="s">the string representation of the vector</param>
/// <returns>the parsed floats, or null if parsing failed</returns>
public static (float, float)? ParseVector2(string s)
{
var arr = ParseSingleArr(s);
if (arr == null)
{
return null;
}
if (arr.Length == 2)
{
return (arr[0], arr[1]);
}
return null;
}
/// <summary>
/// Parse a vector of one, two, or four floats separated by commas or
/// spaces, such as "1", "1e2,2e3" or ".1,.2,.3,.4"
/// </summary>
/// <param name="s">the string representation of the vector</param>
/// <returns>the parsed floats, or null if parsing failed</returns>
public static float[]? ParseThickness(string s)
{
var arr = ParseSingleArr(s);
if (arr == null)
{
return null;
}
if (arr.Length == 1 || arr.Length == 2 || arr.Length == 4)
{
return arr;
}
return null;
}
}

View File

@@ -1,10 +1,9 @@
using System.Reflection.Emit;
using XamlX.Ast;
using XamlX.Ast;
using XamlX.Emit;
using XamlX.IL;
using XamlX.TypeSystem;
namespace Robust.Build.Tasks
namespace Robust.Xaml
{
internal class RXamlColorAstNode
: XamlAstNode, IXamlAstValueNode, IXamlAstILEmitableNode

View File

@@ -6,9 +6,9 @@ using XamlX.Emit;
using XamlX.IL;
using XamlX.TypeSystem;
namespace Robust.Build.Tasks
namespace Robust.Xaml
{
public abstract class RXamlVecLikeConstAstNode<T>
internal abstract class RXamlVecLikeConstAstNode<T>
: XamlAstNode, IXamlAstValueNode, IXamlAstILEmitableNode
where T : unmanaged
{
@@ -47,7 +47,7 @@ namespace Robust.Build.Tasks
}
}
public sealed class RXamlSingleVecLikeConstAstNode : RXamlVecLikeConstAstNode<float>
internal sealed class RXamlSingleVecLikeConstAstNode : RXamlVecLikeConstAstNode<float>
{
public RXamlSingleVecLikeConstAstNode(
IXamlLineInfo lineInfo,
@@ -69,7 +69,7 @@ namespace Robust.Build.Tasks
}
}
public sealed class RXamlInt32VecLikeConstAstNode : RXamlVecLikeConstAstNode<int>
internal sealed class RXamlInt32VecLikeConstAstNode : RXamlVecLikeConstAstNode<int>
{
public RXamlInt32VecLikeConstAstNode(
IXamlLineInfo lineInfo,

View File

@@ -2,9 +2,9 @@
using XamlX.Transform;
using XamlX.TypeSystem;
namespace Robust.Build.Tasks
namespace Robust.Xaml
{
class RXamlWellKnownTypes
internal class RXamlWellKnownTypes
{
public XamlTypeWellKnownTypes XamlIlTypes { get; }
public IXamlType Single { get; }

View File

@@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<LangVersion>12.0</LangVersion>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Build.Framework" Version="17.8.3" />
<PackageReference Include="Mono.Cecil" Version="0.11.5" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\XamlX\src\XamlX.IL.Cecil\XamlX.IL.Cecil.csproj" />
</ItemGroup>
</Project>

View File

@@ -1,5 +1,4 @@
using System.Diagnostics;
using System.Linq;
using System.Linq;
using XamlX;
using XamlX.Ast;
using XamlX.Emit;
@@ -7,7 +6,7 @@ using XamlX.IL;
using XamlX.Transform;
using XamlX.TypeSystem;
namespace Robust.Build.Tasks
namespace Robust.Xaml
{
/// <summary>
/// Emitters & Transformers based on:
@@ -15,7 +14,7 @@ namespace Robust.Build.Tasks
/// - https://github.com/AvaloniaUI/Avalonia/blob/c85fa2b9977d251a31886c2534613b4730fbaeaf/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AddNameScopeRegistration.cs
/// - https://github.com/AvaloniaUI/Avalonia/blob/afb8ae6f3c517dae912729511483995b16cb31af/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/IgnoredDirectivesTransformer.cs
/// </summary>
public class RobustXamlILCompiler : XamlILCompiler
internal class RobustXamlILCompiler : XamlILCompiler
{
public RobustXamlILCompiler(TransformerConfiguration configuration, XamlLanguageEmitMappings<IXamlILEmitter, XamlILNodeEmitResult> emitMappings, bool fillWithDefaults) : base(configuration, emitMappings, fillWithDefaults)
{
@@ -41,8 +40,9 @@ namespace Robust.Build.Tasks
&& mg.Children.OfType<RobustNameScopeRegistrationXamlIlNode>().Any())
return node;
IXamlAstValueNode value = null;
IXamlAstValueNode? value = null;
for (var c = 0; c < pa.Values.Count; c++)
{
if (pa.Values[c].Type.GetClrType().Equals(context.Configuration.WellKnownTypes.String))
{
value = pa.Values[c];
@@ -57,6 +57,7 @@ namespace Robust.Build.Tasks
break;
}
}
if (value != null)
{
@@ -84,9 +85,9 @@ namespace Robust.Build.Tasks
class RobustNameScopeRegistrationXamlIlNode : XamlAstNode, IXamlAstManipulationNode
{
public IXamlAstValueNode Name { get; set; }
public IXamlType TargetType { get; }
public IXamlType? TargetType { get; }
public RobustNameScopeRegistrationXamlIlNode(IXamlAstValueNode name, IXamlType targetType) : base(name)
public RobustNameScopeRegistrationXamlIlNode(IXamlAstValueNode name, IXamlType? targetType) : base(name)
{
TargetType = targetType;
Name = name;
@@ -104,7 +105,7 @@ namespace Robust.Build.Tasks
{
var scopeField = context.RuntimeContext.ContextType.Fields.First(f =>
f.Name == XamlCompiler.ContextNameScopeFieldName);
f.Name == XamlCustomizations.ContextNameScopeFieldName);
var namescopeRegisterFunction = context.Configuration.TypeSystem
.FindType("Robust.Client.UserInterface.XAML.NameScope").Methods
.First(m => m.Name == "Register");
@@ -128,7 +129,7 @@ namespace Robust.Build.Tasks
return XamlILNodeEmitResult.Void(1);
}
return default;
return default!; // PYREX NOTE: This doesn't seem safe! But it's what we were doing before Nullable
}
}
}
@@ -161,7 +162,7 @@ namespace Robust.Build.Tasks
{
if (!(node is HandleRootObjectScopeNode))
{
return null;
return null!; // PYREX NOTE: This doesn't seem safe, but it predates Nullable on this file
}
var controlType = context.Configuration.TypeSystem.FindType("Robust.Client.UserInterface.Control");
@@ -170,7 +171,7 @@ namespace Robust.Build.Tasks
var dontAbsorb = codeGen.DefineLabel();
var end = codeGen.DefineLabel();
var contextScopeField = context.RuntimeContext.ContextType.Fields.First(f =>
f.Name == XamlCompiler.ContextNameScopeFieldName);
f.Name == XamlCustomizations.ContextNameScopeFieldName);
var controlNameScopeField = controlType.Fields.First(f => f.Name == "NameScope");
var nameScopeType = context.Configuration.TypeSystem
.FindType("Robust.Client.UserInterface.XAML.NameScope");

View File

@@ -1,33 +1,23 @@
using System.Collections.Generic;
using System.Linq;
using Mono.Cecil;
using Mono.Cecil.Cil;
using Mono.Collections.Generic;
using XamlX.TypeSystem;
namespace Robust.Build.Tasks
namespace Robust.Xaml
{
/// <summary>
/// Helpers taken from:
/// Helpers taken from AvaloniaUI on GitHub.
/// </summary>
/// <remarks>
/// - https://github.com/AvaloniaUI/Avalonia/blob/c85fa2b9977d251a31886c2534613b4730fbaeaf/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs
/// - https://github.com/AvaloniaUI/Avalonia/blob/c85fa2b9977d251a31886c2534613b4730fbaeaf/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.Helpers.cs
/// </summary>
public partial class XamlCompiler
/// </remarks>
internal partial class XamlAotCompiler
{
static bool CheckXamlName(IResource r) => r.Name.ToLowerInvariant().EndsWith(".xaml")
|| r.Name.ToLowerInvariant().EndsWith(".paml")
|| r.Name.ToLowerInvariant().EndsWith(".axaml");
private static readonly string[] NameSuffixes = {".xaml", ".paml", ".axaml"};
private static bool MatchThisCall(Collection<Instruction> instructions, int idx)
{
var i = instructions[idx];
// A "normal" way of passing `this` to a static method:
// ldarg.0
// call void [Avalonia.Markup.Xaml]Avalonia.Markup.Xaml.AvaloniaXamlLoader::Load(object)
return i.OpCode == OpCodes.Ldarg_0 || (i.OpCode == OpCodes.Ldarg && i.Operand?.Equals(0) == true);
}
static bool CheckXamlName(IResource r) =>
NameSuffixes.Any(suffix => r.Name.ToLowerInvariant().EndsWith(suffix));
interface IResource : IFileSource
{

View File

@@ -0,0 +1,209 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Microsoft.Build.Framework;
using Mono.Cecil;
using XamlX;
using XamlX.Ast;
using XamlX.IL;
using XamlX.Parsers;
using XamlX.TypeSystem;
namespace Robust.Xaml;
/// <summary>
/// Utility class: holds scope information for a Microsoft.Build.Framework
/// build in order to AOT-compile the XAML resources for an assembly.
/// </summary>
/// <remarks>
/// Also embed enough information to support future JIT attempts on those same resources.
///
/// Code primarily by Paul Ritter, touched by Pyrex in 2024.
///
/// Based on https://github.com/AvaloniaUI/Avalonia/blob/c85fa2b9977d251a31886c2534613b4730fbaeaf/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs
/// Adjusted for our UI Framework
/// </remarks>
internal partial class XamlAotCompiler
{
/// <summary>
/// Update the assembly whose name is <paramref name="input" />, then
/// save an updated assembly to <paramref name="output"/>.
/// </summary>
/// <param name="engine">the Microsoft build engine (used for logging)</param>
/// <param name="input">the input assembly by name</param>
/// <param name="references">all the assemblies that the input Xaml is allowed to reference</param>
/// <param name="output">the place to put the output assembly</param>
/// <param name="strongNameKey">
/// a file to use in order to generate a "strong name" for the assembly
/// (https://learn.microsoft.com/en-us/dotnet/standard/assembly/strong-named)
/// </param>
/// <returns>
/// true if this succeeds and
/// true if the result was written to <paramref name="output"/>
/// </returns>
public static (bool success, bool writtentofile) Compile(IBuildEngine engine, string input, string[] references,
string output, string? strongNameKey)
{
var typeSystem = new CecilTypeSystem(references
.Where(r => !r.ToLowerInvariant().EndsWith("robust.build.tasks.dll"))
.Concat(new[] { input }), input);
var asm = typeSystem.TargetAssemblyDefinition;
if (asm.MainModule.GetType("CompiledRobustXaml", "XamlIlContext") != null)
{
// If this type exists, the assembly has already been processed by us.
// Do not run again, it would corrupt the file.
// This *shouldn't* be possible due to Inputs/Outputs dependencies in the build system,
// but better safe than sorry eh?
engine.LogWarningEvent(new BuildWarningEventArgs("XAMLIL", "", "", 0, 0, 0, 0, "Ran twice on same assembly file; ignoring.", "", ""));
return (true, false);
}
var compileRes = CompileCore(engine, typeSystem);
if (!compileRes)
return (false, false);
var writerParameters = new WriterParameters { WriteSymbols = asm.MainModule.HasSymbols };
if (!string.IsNullOrWhiteSpace(strongNameKey))
writerParameters.StrongNameKeyBlob = File.ReadAllBytes(strongNameKey);
asm.Write(output, writerParameters);
return (true, true);
}
/// <summary>
/// For each XAML resource, identify its affiliated class, invoke the
/// AOT compiler, update the class to call into the generated code,
/// and write down metadata for future JIT compiles.
/// </summary>
/// <param name="engine">the Microsoft build engine (for logging)</param>
/// <param name="typeSystem">the type system (which includes info about the target assembly)</param>
/// <returns>true if compilation succeeded in every case</returns>
static bool CompileCore(IBuildEngine engine, CecilTypeSystem typeSystem)
{
var asm = typeSystem.TargetAssemblyDefinition;
var embrsc = new EmbeddedResources(asm);
var xaml = new XamlCustomizations(typeSystem, typeSystem.TargetAssembly);
var lowLevel = new LowLevelCustomizations(typeSystem);
var contextDef = new TypeDefinition("CompiledRobustXaml", "XamlIlContext",
TypeAttributes.Class, asm.MainModule.TypeSystem.Object);
asm.MainModule.Types.Add(contextDef);
var contextClass = XamlILContextDefinition.GenerateContextClass(
typeSystem.CreateTypeBuilder(contextDef), typeSystem,
xaml.TypeMappings, xaml.EmitMappings
);
bool CompileGroup(IResourceGroup group)
{
var typeDef = new TypeDefinition("CompiledRobustXaml", "!" + group.Name, TypeAttributes.Class,
asm.MainModule.TypeSystem.Object);
asm.MainModule.Types.Add(typeDef);
foreach (var res in group.Resources.Where(CheckXamlName))
{
try
{
engine.LogMessage($"XAMLIL: {res.Name} -> {res.Uri}", MessageImportance.Low);
var xamlText = new StreamReader(new MemoryStream(res.FileContents)).ReadToEnd();
var parsed = XDocumentXamlParser.Parse(xamlText);
var initialRoot = (XamlAstObjectNode) parsed.Root;
var classDirective = initialRoot.Children.OfType<XamlAstXmlDirective>()
.FirstOrDefault(d => d.Namespace == XamlNamespaces.Xaml2006 && d.Name == "Class");
string classname;
if (classDirective != null && classDirective.Values[0] is XamlAstTextNode tn)
{
classname = tn.Text;
}
else
{
classname = res.Name.Replace(".xaml","");
}
var classType = typeSystem.TargetAssembly.FindType(classname);
if (classType == null)
throw new InvalidProgramException($"Unable to find type '{classname}'");
xaml.ILCompiler.Transform(parsed);
var populateName = $"Populate:{res.Name}";
var classTypeDefinition = typeSystem.GetTypeReference(classType).Resolve()!;
var populateBuilder = typeSystem.CreateTypeBuilder(classTypeDefinition);
xaml.ILCompiler.Compile(parsed, contextClass,
xaml.ILCompiler.DefinePopulateMethod(populateBuilder, parsed, populateName, true),
null,
null,
(closureName, closureBaseType) =>
populateBuilder.DefineSubType(closureBaseType, closureName, false),
res.Uri, res
);
var compiledPopulateMethod = typeSystem.GetTypeReference(populateBuilder).Resolve().Methods
.First(m => m.Name == populateName);
lowLevel.AddXamlMetadata(classTypeDefinition, new Uri(res.Uri), res.FilePath, xamlText);
var foundXamlLoader = lowLevel.TrampolineCallsToXamlLoader(classTypeDefinition, compiledPopulateMethod);
if (!foundXamlLoader)
{
throw new InvalidProgramException(
$"No call to RobustXamlLoader.Load(this) call found anywhere in the type {classType.FullName} and type seems to have custom constructors.");
}
}
catch (Exception e)
{
engine.LogErrorEvent(new BuildErrorEventArgs("XAMLIL", "", res.FilePath, 0, 0, 0, 0,
$"{res.FilePath}: {e.Message}", "", "CompileRobustXaml"));
}
res.Remove();
}
return true;
}
if (embrsc.Resources.Count(CheckXamlName) != 0)
{
if (!CompileGroup(embrsc))
{
return false;
}
}
return true;
}
}
/// <summary>
/// This is <see cref="IFileSource"/> from XamlX, augmented with the other
/// arguments that the XAML compiler wants.
/// </summary>
/// <remarks>
/// We store these later in the build process inside a XamlMetadataAttribute,
/// in order to support JIT compilation.
/// </remarks>
interface IResource : IFileSource
{
string Uri { get; }
string Name { get; }
void Remove();
}
/// <summary>
/// A named collection of <see cref="IResource"/>s.
/// </summary>
interface IResourceGroup
{
string Name { get; }
IEnumerable<IResource> Resources { get; }
}

View File

@@ -0,0 +1,218 @@
using XamlX;
using XamlX.Ast;
using XamlX.Emit;
using XamlX.IL;
using XamlX.Transform;
using XamlX.TypeSystem;
namespace Robust.Xaml;
/// <summary>
/// Shared XAML config info that both the AOT and JIT compiler can use.
/// </summary>
/// <remarks>
/// This is a bunch of code primarily written by PJB that originally appeared in XamlAotCompiler.cs.
/// </remarks>
internal sealed class XamlCustomizations
{
public const string ContextNameScopeFieldName = "RobustNameScope";
public readonly IXamlTypeSystem TypeSystem;
public readonly XamlLanguageTypeMappings TypeMappings;
public readonly XamlLanguageEmitMappings<IXamlILEmitter, XamlILNodeEmitResult> EmitMappings;
public readonly TransformerConfiguration TransformerConfiguration;
public readonly RobustXamlILCompiler ILCompiler;
/// <summary>
/// Create and hold a bunch of resources related to SS14's particular dialect of XAML.
/// </summary>
/// <param name="typeSystem">
/// the type system for XamlX to use
/// (both <see cref="CecilTypeSystem"/> and <see cref="CecilTypeSystem"/> work)
/// </param>
/// <param name="defaultAssembly">the default assembly (for unqualified names to be looked up in)</param>
public XamlCustomizations(IXamlTypeSystem typeSystem, IXamlAssembly defaultAssembly)
{
TypeSystem = typeSystem;
TypeMappings = new XamlLanguageTypeMappings(typeSystem)
{
XmlnsAttributes =
{
typeSystem.GetType("Avalonia.Metadata.XmlnsDefinitionAttribute"),
},
ContentAttributes =
{
typeSystem.GetType("Avalonia.Metadata.ContentAttribute")
},
UsableDuringInitializationAttributes =
{
typeSystem.GetType("Robust.Client.UserInterface.XAML.UsableDuringInitializationAttribute")
},
DeferredContentPropertyAttributes =
{
typeSystem.GetType("Robust.Client.UserInterface.XAML.DeferredContentAttribute")
},
RootObjectProvider = typeSystem.GetType("Robust.Client.UserInterface.XAML.ITestRootObjectProvider"),
UriContextProvider = typeSystem.GetType("Robust.Client.UserInterface.XAML.ITestUriContext"),
ProvideValueTarget = typeSystem.GetType("Robust.Client.UserInterface.XAML.ITestProvideValueTarget"),
};
EmitMappings = new XamlLanguageEmitMappings<IXamlILEmitter, XamlILNodeEmitResult>
{
ContextTypeBuilderCallback = EmitNameScopeField
};
TransformerConfiguration = new TransformerConfiguration(
typeSystem,
defaultAssembly,
TypeMappings,
XamlXmlnsMappings.Resolve(typeSystem, TypeMappings),
CustomValueConverter
);
ILCompiler = new RobustXamlILCompiler(TransformerConfiguration, EmitMappings, true);
}
/// <summary>
/// Create a field of type NameScope that contains a new NameScope, then
/// alter the type's constructor to initialize that field.
/// </summary>
/// <param name="typeBuilder">the type to alter</param>
/// <param name="constructor">the constructor to alter</param>
private void EmitNameScopeField(
IXamlTypeBuilder<IXamlILEmitter> typeBuilder,
IXamlILEmitter constructor
)
{
var nameScopeType = TypeSystem.FindType("Robust.Client.UserInterface.XAML.NameScope");
var field = typeBuilder.DefineField(nameScopeType,
ContextNameScopeFieldName,
true,
false);
constructor
.Ldarg_0()
.Newobj(nameScopeType.GetConstructor())
.Stfld(field);
}
/// <summary>
/// Convert a <see cref="XamlAstTextNode"/> to some other kind of node,
/// if the purpose of the node appears to be to represent one of various
/// builtin types.
/// </summary>
/// <remarks>
/// (See, for instance, <see cref="RXamlColorAstNode"/>.)
///
/// The arguments here come from an interface built into XamlX.
/// </remarks>
/// <param name="context">context object that holds the TransformerConfiguration</param>
/// <param name="node">the node to consider rewriting</param>
/// <param name="type">the type of that node</param>
/// <param name="result">results get written to here</param>
/// <returns></returns>
/// <exception cref="XamlLoadException">if the literal for a type is poorly spelled for that type</exception>
private static bool CustomValueConverter(
AstTransformationContext context,
IXamlAstValueNode node,
IXamlType type,
out IXamlAstValueNode? result)
{
if (!(node is XamlAstTextNode textNode))
{
result = null;
return false;
}
var text = textNode.Text;
var types = context.GetRobustTypes();
if (type.Equals(types.Vector2))
{
var foo = MathParsing.ParseVector2(text);
if (foo == null)
{
throw new XamlLoadException($"Unable to parse \"{text}\" as a Vector2", node);
}
var (x, y) = foo.Value;
result = new RXamlSingleVecLikeConstAstNode(
node,
types.Vector2,
types.Vector2ConstructorFull,
types.Single,
new[] { x, y });
return true;
}
if (type.Equals(types.Thickness))
{
var foo = MathParsing.ParseThickness(text);
if (foo == null)
{
throw new XamlLoadException($"Unable to parse \"{text}\" as a Thickness", node);
}
var val = foo;
float[] full;
if (val.Length == 1)
{
var u = val[0];
full = new[] { u, u, u, u };
}
else if (val.Length == 2)
{
var h = val[0];
var v = val[1];
full = new[] { h, v, h, v };
}
else // 4
{
full = val;
}
result = new RXamlSingleVecLikeConstAstNode(
node,
types.Thickness,
types.ThicknessConstructorFull,
types.Single,
full);
return true;
}
if (type.Equals(types.Color))
{
// TODO: Interpret these colors at XAML compile time instead of at runtime.
result = new RXamlColorAstNode(node, types, text);
return true;
}
result = null;
return false;
}
/// <summary>
/// Wrap the <paramref name="filePath"/> and <paramref name="contents"/>
/// from a Xaml file or from a XamlMetadataAttribute.
/// </summary>
/// <remarks>
/// This interface is the primary input format that XamlX expects.
/// </remarks>
/// <param name="filePath">the resource file path</param>
/// <param name="contents">the contents</param>
/// <returns>IFileSource</returns>
public IFileSource CreateFileSource(string filePath, byte[] contents)
{
return new InternalFileSource(filePath, contents);
}
/// <summary>
/// A trivial implementation of <see cref="IFileSource"/>.
/// </summary>
private class InternalFileSource(string filePath, byte[] contents) : IFileSource
{
public string FilePath { get; } = filePath;
public byte[] FileContents { get; } = contents;
}
}

View File

@@ -0,0 +1,180 @@
using System;
using System.Reflection;
using System.Reflection.Emit;
using System.Text;
using System.Threading;
using XamlX.IL;
using XamlX.Parsers;
namespace Robust.Xaml;
/// <summary>
/// A JIT compiler for Xaml.
/// </summary>
/// <remarks>
/// Uses <see cref="System.Reflection.Emit"/>, which can find types
/// at runtime without looking for their assemblies on disk.
/// </remarks>
internal sealed class XamlJitCompiler
{
private readonly SreTypeSystem _typeSystem;
private static int _assemblyId;
/// <summary>
/// Construct a XamlJitCompiler.
/// </summary>
/// <remarks>
/// No configuration is needed or possible.
///
/// This is a pretty expensive function because it creates an
/// <see cref="SreTypeSystem"/>, which requires going through the loaded
/// assembly list.
/// </remarks>
public XamlJitCompiler()
{
_typeSystem = new SreTypeSystem();
}
/// <summary>
/// Generate a name for a new dynamic assembly.
/// </summary>
/// <returns>the new name</returns>
private static string GenerateAssemblyName()
{
// make the name unique (even though C# possibly doesn't care)
return
$"{nameof(XamlJitCompiler)}_{Interlocked.Increment(ref _assemblyId)}";
}
/// <summary>
/// Compile the Populate method for <paramref name="t" />, using the given
/// uri/path/contents.
/// </summary>
/// <remarks>
/// These values (except for contents) are generated during the AOT compile
/// process.
///
/// It is not enforced that they be the same after JIT -- the JITed code has
/// no knowledge of the state of the AOT'ed code -- but our code and
/// documentation do assume that.
/// </remarks>
/// <param name="t">the type whose Populate method should be generated</param>
/// <param name="uri">the Uri associated with the Control</param>
/// <param name="filePath">the resource file path for the control</param>
/// <param name="contents">the contents of the new XAML file</param>
/// <returns>Success or Failure depending on whether an error was thrown</returns>
public XamlJitCompilerResult Compile(
Type t,
Uri uri,
string filePath,
string contents
)
{
try
{
var result = CompileOrCrash(t, uri, filePath, contents);
return new XamlJitCompilerResult.Success(result);
}
catch (Exception e)
{
return new XamlJitCompilerResult.Error(
e,
e.Message.StartsWith("Unable to resolve type")
? "Is the type internal? (hot reloading can't handle that right now!)"
: null
);
}
}
private MethodInfo CompileOrCrash(
Type t,
Uri uri,
string filePath,
string contents
)
{
var xaml = new XamlCustomizations(_typeSystem, _typeSystem.FindAssembly(t.Assembly.FullName));
// attempt to parse the code
var document = XDocumentXamlParser.Parse(contents);
// generate a toy assembly to contain everything we make
var assemblyNameString = GenerateAssemblyName();
var assemblyName = new AssemblyName(assemblyNameString);
var assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(
assemblyName,
AssemblyBuilderAccess.RunAndCollect
);
var moduleBuilder = assemblyBuilder.DefineDynamicModule(assemblyNameString);
var contextClassRawBuilder = moduleBuilder.DefineType("ContextClass");
var populateClassRawBuilder = moduleBuilder.DefineType("PopulateClass");
var contextClassBuilder = _typeSystem.CreateTypeBuilder(contextClassRawBuilder);
var populateClassBuilder = _typeSystem.CreateTypeBuilder(populateClassRawBuilder);
var contextClass = XamlILContextDefinition.GenerateContextClass(
contextClassBuilder,
_typeSystem,
xaml.TypeMappings,
xaml.EmitMappings
);
var populateName = "!Populate";
xaml.ILCompiler.Transform(document);
xaml.ILCompiler.Compile(
document,
contextClass,
xaml.ILCompiler.DefinePopulateMethod(
populateClassBuilder,
document,
populateName,
true
),
null,
null,
(closureName, closureBaseType) =>
contextClassBuilder.DefineSubType(closureBaseType, closureName, false),
uri.ToString(),
xaml.CreateFileSource(filePath, Encoding.UTF8.GetBytes(contents))
);
contextClassBuilder.CreateType();
var populateClass = populateClassRawBuilder.CreateTypeInfo()!;
var implementation = populateClass.GetMethod(populateName);
if (implementation == null)
{
throw new NullReferenceException("populate method should have existed");
}
return implementation;
}
}
/// <summary>
/// An enum containing either <see cref="Success" /> (with a <see cref="MethodInfo" />)
/// or <see cref="Error" />. (with an <see cref="Exception" />, and an optional hint
/// for how to fix it)
/// </summary>
/// <remarks>
/// It is not guaranteed that the <see cref="Exception" /> ever appeared on the stack.
/// That is an implementation detail of <see cref="XamlJitCompiler.Compile"/>.
/// </remarks>
public abstract class XamlJitCompilerResult
{
public class Success(MethodInfo methodInfo) : XamlJitCompilerResult
{
public MethodInfo MethodInfo { get; } = methodInfo;
}
public class Error(Exception raw, string? hint) : XamlJitCompilerResult
{
public Exception Raw { get; } = raw;
public string? Hint { get; } = hint;
}
}