#if TOOLS
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Threading;
using Robust.Shared.Log;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
using Robust.Xaml;
namespace Robust.Client.UserInterface.XAML.Proxy;
///
/// This is a utility class that tracks the relationship between resource file names,
/// Xamlx-compatible s, s that are interested in a
/// given file, and implementations of Populate.
///
internal sealed class XamlImplementationStorage
{
///
/// For each filename, we store its last known .
///
///
/// When we compile the new implementation, we will use the same .
///
private readonly Dictionary _fileUri = new();
///
/// For each filename, we store its last known content.
///
///
/// 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.
///
private readonly Dictionary _fileContent = new();
///
/// For each filename, we store the type interested in this file.
///
private readonly Dictionary _fileType = new();
private readonly Dictionary _fileTypeReverse = new();
///
/// For each type, store the JIT-compiled implementation of Populate.
///
///
/// 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.
///
private readonly Dictionary _populateImplementations = new();
private readonly ISawmill _sawmill;
private readonly XamlJitDelegate _jitDelegate;
private readonly Lock _compileLock = new();
///
/// Create the storage.
///
///
/// It would be weird to call this from any type outside of
/// .
///
/// the (shared) logger
///
/// a delegate that calls the
/// , possibly handling errors
///
public XamlImplementationStorage(ISawmill sawmill, XamlJitDelegate jitDelegate)
{
_sawmill = sawmill;
_jitDelegate = jitDelegate;
}
///
/// Inspect for types that declare a .
///
///
/// We can only do hot reloading if we know this basic information.
///
/// Note that even release-mode content artifacts contain this attribute.
///
/// the assembly
/// an IEnumerable of types with xaml metadata
private IEnumerable<(Type, XamlMetadataAttribute)> TypesWithXamlMetadata(Assembly assembly)
{
foreach (var type in assembly.GetTypes())
{
if (type.GetCustomAttribute() is not { } attr)
{
continue;
}
yield return (type, attr);
}
}
///
/// Add all Xaml-annotated types from to this storage.
///
///
/// We don't JIT these types, but we store enough info that we could JIT
/// them if we wanted to.
///
/// an assembly
public void Add(Assembly assembly)
{
using var _ = _compileLock.EnterScope();
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"
);
}
_fileTypeReverse.Add(type, fileName);
}
}
///
/// Quietly JIT every type with XAML metadata.
///
///
/// This should have no visible effect except that the
/// may dump some info messages into the terminal about cases where the
/// hot reload failed.
///
public void ForceReloadAll()
{
using var _ = _compileLock.EnterScope();
foreach (var (fileName, fileContent) in _fileContent)
{
SetImplementation(fileName, fileContent, true);
}
}
///
/// Return true if calling on would not be a no-op.
///
///
/// That is: if some type cares about the contents of .
///
/// the filename
/// true if not a no-op
public bool CanSetImplementation(string fileName)
{
using var _ = _compileLock.EnterScope();
return _fileType.ContainsKey(fileName);
}
public MethodInfo? CompileType(Type type)
{
if (_fileTypeReverse.TryGetValue(type, out var fileName))
return SetImplementation(fileName, _fileContent[fileName], quiet: true);
_sawmill.Warning($"Type {type} has no XAML file!");
return null;
}
///
/// Replace the implementation of by JIT-ing
/// .
///
///
/// If nothing cares about the implementation of , then this will do nothing.
///
/// the name of the file whose implementation should be replaced
/// the new implementation
/// if true, then don't bother to log
public MethodInfo? SetImplementation(string fileName, string fileContent, bool quiet)
{
using var _ = _compileLock.EnterScope();
if (!_fileType.TryGetValue(fileName, out var type))
{
_sawmill.Warning($"SetImplementation called with {fileName}, but no types care about its contents");
return null;
}
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;
return impl;
}
///
/// Call the JITed implementation of Populate on a XAML-associated object .
///
/// If no JITed implementation exists, return false.
///
/// the static type of
/// an instance of (can be a subclass)
/// true if a JITed implementation existed
public bool Populate(Type t, object o)
{
if (!_populateImplementations.TryGetValue(t, out var implementation))
{
// JIT if needed.
implementation = CompileType(t);
// pop out if we never JITed anything/couldn't JIT
if (implementation == null)
return false;
}
implementation.Invoke(null, [null, o]);
return true;
}
}
#endif