mirror of
https://github.com/space-wizards/RobustToolbox.git
synced 2026-02-14 19:29:36 +01:00
* 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>
181 lines
5.7 KiB
C#
181 lines
5.7 KiB
C#
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;
|
|
}
|
|
}
|