Files
RobustToolbox/Robust.Xaml/XamlCustomizations.cs
Nyeogmi 6396ec472d 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>
2024-08-27 02:16:57 +02:00

219 lines
7.4 KiB
C#

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;
}
}