932 lines
34 KiB
C#
932 lines
34 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Text;
|
|
#if BURST_COMPILER_SHARED
|
|
using Burst.Compiler.IL;
|
|
using Burst.Compiler.IL.DebugInfo;
|
|
using Burst.Compiler.IL.Diagnostics;
|
|
using Burst.Compiler.IL.Helpers;
|
|
#endif
|
|
using Mono.Cecil;
|
|
using Mono.Cecil.Cil;
|
|
using Mono.Cecil.Pdb;
|
|
|
|
namespace zzzUnity.Burst.CodeGen
|
|
{
|
|
/// <summary>
|
|
/// Provides an assembly loader with caching depending on the LastWriteTime of the assembly file.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// This class is not thread safe. It needs to be protected outside.
|
|
/// </remarks>
|
|
#if BURST_COMPILER_SHARED
|
|
public
|
|
#else
|
|
internal
|
|
#endif
|
|
class AssemblyLoader : BaseAssemblyResolver
|
|
{
|
|
private readonly Dictionary<string, CacheAssemblyEntry> _nameToEntry;
|
|
private readonly Dictionary<string, CacheAssemblyEntry> _fileToEntry;
|
|
|
|
#if BURST_COMPILER_SHARED
|
|
private readonly AssemblyWatcherManager _assemblyWatcherManager;
|
|
private readonly HashSet<string> _assemblyWatcherChangedFolders;
|
|
|
|
public readonly PortablePdbCache PdbCacheReference;
|
|
|
|
public AssemblyLoader(PortablePdbCache instance, AssemblyWatcherManager assemblyWatcherManager = null)
|
|
#else
|
|
public AssemblyLoader()
|
|
#endif
|
|
{
|
|
_fileToEntry = new Dictionary<string, CacheAssemblyEntry>(StringComparer.Ordinal);
|
|
_nameToEntry = new Dictionary<string, CacheAssemblyEntry>(StringComparer.Ordinal);
|
|
#if BURST_COMPILER_SHARED
|
|
_assemblyWatcherManager = assemblyWatcherManager;
|
|
if (_assemblyWatcherManager != null)
|
|
{
|
|
_assemblyWatcherManager.OnFolderChanged += OnAssemblyWatcherFolderChanged;
|
|
}
|
|
_assemblyWatcherChangedFolders = new HashSet<string>();
|
|
|
|
PdbCacheReference = instance ?? throw new ArgumentException("instance must point to a valid PortablePdbCache instance");
|
|
#endif
|
|
|
|
// We remove all setup by Cecil by default (it adds '.' and 'bin')
|
|
ClearSearchDirectories();
|
|
|
|
LoadDebugSymbols = false; // We don't bother loading the symbols by default now, since we use SRM to handle symbols in a more thread safe manner
|
|
// this is to maintain compatibility with the patch-assemblies path (see BclApp.cs), used by dots runtime
|
|
}
|
|
|
|
#if BURST_COMPILER_SHARED
|
|
private void OnAssemblyWatcherFolderChanged(AssemblyWatcherEventArgs args)
|
|
{
|
|
lock (_assemblyWatcherChangedFolders)
|
|
{
|
|
OnLogDebug?.Invoke(LogMessageType.Debug, $"Folder changed: {args.ChangedFolder}");
|
|
_assemblyWatcherChangedFolders.Add(args.ChangedFolder);
|
|
}
|
|
}
|
|
#endif
|
|
|
|
public bool IsDebugging { get; set; }
|
|
|
|
public bool LoadDebugSymbols { get; set; }
|
|
|
|
#if BURST_COMPILER_SHARED
|
|
private bool CheckAssemblyDirty => _assemblyWatcherManager != null;
|
|
#endif
|
|
|
|
public Action<LogMessageType, string> OnLogDebug { get; set; }
|
|
|
|
internal Action<AssemblyNameReferenceAndPath> OnResolve { get; set; }
|
|
|
|
internal Action<AssemblyNameReference, Action<LogMessageType, string>> OnDirty { get; set; }
|
|
|
|
public void Clear()
|
|
{
|
|
foreach (var entry in this._nameToEntry.Values)
|
|
entry.Definition.Dispose();
|
|
_nameToEntry.Clear();
|
|
_fileToEntry.Clear();
|
|
}
|
|
|
|
public bool EnsureSearchDirectories(string[] folders)
|
|
{
|
|
for (var i = 0; i < folders.Length; i++)
|
|
{
|
|
folders[i] = NormalizeFilePath(folders[i]);
|
|
}
|
|
|
|
// If the existing search directories are the same as the ones we've been passed,
|
|
// then there's nothing to do.
|
|
#if BURST_COMPILER_SHARED
|
|
var existingSearchDirectories = HashSetPool<string>.Get();
|
|
existingSearchDirectories.UnionWith(GetSearchDirectories());
|
|
var newSearchDirectories = HashSetPool<string>.Get();
|
|
newSearchDirectories.UnionWith(folders);
|
|
#else
|
|
var existingSearchDirectories = new HashSet<string>(GetSearchDirectories());
|
|
var newSearchDirectories = new HashSet<string>(folders);
|
|
#endif
|
|
|
|
try
|
|
{
|
|
if (existingSearchDirectories.SetEquals(newSearchDirectories))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
// Otherwise, reset the search directories.
|
|
ClearSearchDirectories();
|
|
foreach (var path in folders)
|
|
{
|
|
if (Directory.Exists(path))
|
|
{
|
|
base.AddSearchDirectory(path);
|
|
}
|
|
else
|
|
{
|
|
//Log(LogMessageType.Warning, $"The assembly search path `{path}` does not exist");
|
|
newSearchDirectories.Remove(path);
|
|
}
|
|
}
|
|
|
|
#if BURST_COMPILER_SHARED
|
|
_assemblyWatcherManager?.UpdateFolders(newSearchDirectories);
|
|
#endif
|
|
}
|
|
finally
|
|
{
|
|
#if BURST_COMPILER_SHARED
|
|
HashSetPool<string>.Return(newSearchDirectories);
|
|
HashSetPool<string>.Return(existingSearchDirectories);
|
|
#endif
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public void ClearSearchDirectories()
|
|
{
|
|
foreach (var dir in GetSearchDirectories())
|
|
{
|
|
RemoveSearchDirectory(dir);
|
|
}
|
|
}
|
|
|
|
public AssemblyDefinition LoadFromStream(Stream peStream, Stream pdbStream = null, ISymbolReaderProvider customSymbolReader=null)
|
|
{
|
|
peStream.Position = 0;
|
|
if (pdbStream != null)
|
|
{
|
|
pdbStream.Position = 0;
|
|
}
|
|
var readerParameters = CreateReaderParameters();
|
|
if (customSymbolReader != null)
|
|
{
|
|
readerParameters.ReadSymbols = true;
|
|
readerParameters.SymbolReaderProvider = customSymbolReader;
|
|
}
|
|
readerParameters.ReadingMode = ReadingMode.Deferred;
|
|
try
|
|
{
|
|
readerParameters.SymbolStream = pdbStream;
|
|
return AssemblyDefinition.ReadAssembly(peStream, readerParameters);
|
|
}
|
|
catch
|
|
{
|
|
readerParameters.ReadSymbols = false;
|
|
readerParameters.SymbolStream = null;
|
|
peStream.Position = 0;
|
|
if (pdbStream != null)
|
|
{
|
|
pdbStream.Position = 0;
|
|
}
|
|
return AssemblyDefinition.ReadAssembly(peStream, readerParameters);
|
|
}
|
|
}
|
|
|
|
public override AssemblyDefinition Resolve(AssemblyNameReference name)
|
|
{
|
|
CacheAssemblyEntry cacheEntry;
|
|
|
|
if (this._nameToEntry.TryGetValue(name.FullName, out cacheEntry))
|
|
{
|
|
if (!IsCacheEntryDirtyAndNotify(cacheEntry))
|
|
{
|
|
OnResolve?.Invoke(new AssemblyNameReferenceAndPath(name, cacheEntry.FilePath));
|
|
return cacheEntry.Definition;
|
|
}
|
|
|
|
RemoveEntryFromCache(cacheEntry.Name, cacheEntry);
|
|
}
|
|
|
|
var readerParameters = CreateReaderParameters();
|
|
readerParameters.ReadingMode = ReadingMode.Deferred;
|
|
AssemblyDefinition assemblyDefinition;
|
|
|
|
try
|
|
{
|
|
assemblyDefinition = this.Resolve(name, readerParameters);
|
|
}
|
|
catch
|
|
{
|
|
if (readerParameters.ReadSymbols == true)
|
|
{
|
|
// Attempt to load without symbols
|
|
readerParameters.ReadSymbols = false;
|
|
assemblyDefinition = this.Resolve(name, readerParameters);
|
|
}
|
|
else
|
|
{
|
|
throw;
|
|
}
|
|
}
|
|
|
|
RegisterAssembly(name, assemblyDefinition);
|
|
OnResolve?.Invoke(new AssemblyNameReferenceAndPath(name, _nameToEntry[name.FullName].FilePath));
|
|
return assemblyDefinition;
|
|
}
|
|
|
|
public bool TryGetFullPath(AssemblyNameReference name, out string fullPath)
|
|
{
|
|
try
|
|
{
|
|
// We don't care about the return value - we just want to ensure
|
|
// that _nameToEntry has the correct cache entry.
|
|
Resolve(name);
|
|
}
|
|
catch (AssemblyResolutionException)
|
|
{
|
|
fullPath = null;
|
|
return false;
|
|
}
|
|
|
|
var cacheEntry = _nameToEntry[name.FullName];
|
|
fullPath = cacheEntry.FilePath;
|
|
return true;
|
|
}
|
|
|
|
public string GetFullPath(AssemblyNameReference name)
|
|
{
|
|
try
|
|
{
|
|
// We don't care about the return value - we just want to ensure
|
|
// that _nameToEntry has the correct cache entry.
|
|
Resolve(name);
|
|
}
|
|
catch (AssemblyResolutionException ex)
|
|
{
|
|
throw new Exception("Unable to resolve assembly using search directories: " + Environment.NewLine + string.Join(Environment.NewLine, GetSearchDirectories()), ex);
|
|
}
|
|
|
|
var cacheEntry = _nameToEntry[name.FullName];
|
|
return cacheEntry.FilePath;
|
|
}
|
|
|
|
private bool IsCacheEntryDirtyAndNotify(CacheAssemblyEntry entry)
|
|
{
|
|
#if BURST_COMPILER_SHARED
|
|
// By default, we don't check assembly dirtiness as it is requiring a costly kernel context switch with the filesystem
|
|
// and hurting significantly the performance for btests
|
|
if (!CheckAssemblyDirty)
|
|
{
|
|
return false;
|
|
}
|
|
#endif
|
|
|
|
if (entry.FileTimeVerified) return false;
|
|
|
|
var lastWriteTime = File.GetLastWriteTime(entry.FilePath);
|
|
// GetLastWriteTime returns 01/01/1601 if the file doesn't exist.
|
|
var fileDoesNotExistTime = DateTime.FromFileTime(0);
|
|
var isDirty = lastWriteTime == fileDoesNotExistTime || lastWriteTime > entry.FileTime;
|
|
entry.FileTimeVerified = true;
|
|
|
|
if (IsDebugging)
|
|
{
|
|
OnLogDebug?.Invoke(LogMessageType.Debug, $"Checking Assembly file timestamp {entry.FilePath} Cached: `{DateTimeToStringPrecise(entry.FileTime)}` OnDisk: `{DateTimeToStringPrecise(lastWriteTime)}` => {(isDirty?"DIRTY" : "Not dirty")}");
|
|
}
|
|
|
|
if (isDirty)
|
|
{
|
|
OnDirty?.Invoke(entry.Definition.Name, OnLogDebug);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private static string DateTimeToStringPrecise(DateTime datetime)
|
|
{
|
|
return datetime.ToString("yyyy-MM-dd HH:mm:ss.fff");
|
|
}
|
|
|
|
public string DumpCache()
|
|
{
|
|
var builder = new StringBuilder();
|
|
if (_nameToEntry.Count > 0)
|
|
{
|
|
foreach (var cacheAssemblyEntry in _nameToEntry)
|
|
{
|
|
builder.AppendLine($"- {cacheAssemblyEntry.Value.ToString()}");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
builder.AppendLine("- [No assemblies in AssemblyLoader cache]");
|
|
}
|
|
|
|
return builder.ToString();
|
|
}
|
|
|
|
public void UpdateCache()
|
|
{
|
|
#if BURST_COMPILER_SHARED
|
|
if (!CheckAssemblyDirty) return;
|
|
|
|
var anythingChanged = false;
|
|
lock (_assemblyWatcherChangedFolders)
|
|
{
|
|
foreach (var changedFolder in _assemblyWatcherChangedFolders)
|
|
{
|
|
OnLogDebug?.Invoke(LogMessageType.Debug, $"Folder changed: `{changedFolder}`");
|
|
|
|
foreach (var key in _fileToEntry.Keys)
|
|
{
|
|
if (key.StartsWith(changedFolder, StringComparison.InvariantCultureIgnoreCase))
|
|
{
|
|
var entry = _fileToEntry[key];
|
|
entry.FileTimeVerified = false;
|
|
OnLogDebug?.Invoke(LogMessageType.Debug, $"Assembly marked dirty: `{key}`");
|
|
anythingChanged = true;
|
|
}
|
|
}
|
|
}
|
|
_assemblyWatcherChangedFolders.Clear();
|
|
|
|
foreach (var key in _fileToEntry.Keys)
|
|
{
|
|
var entry = _fileToEntry[key];
|
|
if (entry.FileTimeVerified)
|
|
{
|
|
OnLogDebug?.Invoke(LogMessageType.Debug, $"Assembly not dirty: `{key}`");
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!anythingChanged)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var keys = _nameToEntry.Keys.ToArray();
|
|
|
|
foreach (var key in keys)
|
|
{
|
|
var entry = _nameToEntry[key];
|
|
if (IsCacheEntryDirtyAndNotify(entry))
|
|
{
|
|
RemoveEntryFromCache(key, entry);
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
|
|
private void RemoveEntryFromCache(string entryKey, CacheAssemblyEntry entry)
|
|
{
|
|
#if BURST_COMPILER_SHARED
|
|
PdbCacheReference.RemoveEntry(entry.Definition, OnLogDebug);
|
|
#endif
|
|
_nameToEntry.Remove(entryKey);
|
|
_fileToEntry.Remove(entry.FilePath);
|
|
}
|
|
|
|
public new void AddSearchDirectory(string directory)
|
|
{
|
|
if (!GetSearchDirectories().Contains(directory))
|
|
{
|
|
base.AddSearchDirectory(directory);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Loads the specified assembly.
|
|
/// </summary>
|
|
/// <param name="assemblyLocation">The assembly location.</param>
|
|
/// <param name="useSymbols">if set to <c>true</c> load and use the pdb symbols.</param>
|
|
/// <exception cref="ArgumentNullException">assemblyLocation</exception>
|
|
/// <returns>The loaded Assembly definition.</returns>
|
|
public AssemblyDefinition LoadFromFile(string assemblyLocation)
|
|
{
|
|
if (assemblyLocation == null) throw new ArgumentNullException(nameof(assemblyLocation));
|
|
|
|
// If the file was already loaded, don't try to load it
|
|
CacheAssemblyEntry cacheEntry;
|
|
assemblyLocation = NormalizeFilePath(assemblyLocation);
|
|
if (this._fileToEntry.TryGetValue(assemblyLocation, out cacheEntry))
|
|
{
|
|
if (!IsCacheEntryDirtyAndNotify(cacheEntry))
|
|
{
|
|
return cacheEntry.Definition;
|
|
}
|
|
|
|
RemoveEntryFromCache(cacheEntry.Name, cacheEntry);
|
|
}
|
|
|
|
if (assemblyLocation == null) throw new ArgumentNullException(nameof(assemblyLocation));
|
|
var readerParams = CreateReaderParameters();
|
|
AssemblyDefinition assemblyDefinition;
|
|
try
|
|
{
|
|
assemblyDefinition = AssemblyDefinition.ReadAssembly(assemblyLocation, readerParams);
|
|
}
|
|
catch (Exception)
|
|
{
|
|
if (readerParams.ReadSymbols == true)
|
|
{
|
|
// Attempt to load without symbols
|
|
readerParams.ReadSymbols = false;
|
|
assemblyDefinition = AssemblyDefinition.ReadAssembly(assemblyLocation, readerParams);
|
|
}
|
|
else
|
|
{
|
|
throw;
|
|
}
|
|
}
|
|
// AssemblyDefinition.Load For some reason, assemblyLoader doesn't cache properly
|
|
RegisterAssembly(assemblyDefinition.Name, assemblyDefinition);
|
|
|
|
return assemblyDefinition;
|
|
}
|
|
|
|
private ReaderParameters CreateReaderParameters()
|
|
{
|
|
var readerParams = new ReaderParameters
|
|
{
|
|
InMemory = true,
|
|
AssemblyResolver = this,
|
|
MetadataResolver = new CustomMetadataResolver(this),
|
|
ReadSymbols = LoadDebugSymbols // We no longer use cecil to read symbol information, prefering SRM thread safe methods, so I`m being explicit here in case the default changes
|
|
};
|
|
|
|
if (LoadDebugSymbols)
|
|
{
|
|
readerParams.SymbolReaderProvider = new CustomSymbolReaderProvider(null);
|
|
}
|
|
|
|
return readerParams;
|
|
}
|
|
|
|
#if BURST_COMPILER_SHARED
|
|
/// <summary>
|
|
/// Resolves a cecil <see cref="MethodReference"/> from a <see cref="MethodReferenceString"/>
|
|
/// </summary>
|
|
/// <param name="methodReferenceString">A method reference string</param>
|
|
/// <returns>A cecil <see cref="MethodReference"/></returns>
|
|
public MethodReference Resolve(MethodReferenceString methodReferenceString)
|
|
{
|
|
if (methodReferenceString == null) throw new ArgumentNullException(nameof(methodReferenceString));
|
|
|
|
var typeReference = Resolve(methodReferenceString.DeclaringType);
|
|
|
|
var typeDefinition = typeReference.StrictResolve();
|
|
|
|
// Initial capacity 1 because that is overwhelmingly the likely outcome.
|
|
var methods = new List<MethodDefinition>(1);
|
|
|
|
foreach (var m in typeDefinition.Methods)
|
|
{
|
|
if (m.Name != methodReferenceString.Name)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (m.Parameters.Count != methodReferenceString.ParameterTypes.Count)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
methods.Add(m);
|
|
}
|
|
|
|
// We expect to match at least one method
|
|
if (methods.Count == 0)
|
|
{
|
|
throw new InvalidOperationException($"Unable to find the method `{methodReferenceString}` from type `{typeReference}`");
|
|
}
|
|
|
|
// If we have more than one match we need to do a more expensive re-evaluation of the methods to figure out which types they use match.
|
|
if (methods.Count > 1)
|
|
{
|
|
MethodDefinition methodToUse = null;
|
|
|
|
foreach (var m in methods)
|
|
{
|
|
var count = m.Parameters.Count;
|
|
|
|
var match = true;
|
|
|
|
for (int i = 0; i < count; i++)
|
|
{
|
|
var parameterTypeFullName = methodReferenceString.ParameterTypes[i].ToString(ReferenceStringFormatOptions.None).Replace("+", "/").Replace("[[", "<").Replace("]]", ">");
|
|
|
|
if (m.Parameters[i].ParameterType.FullName != parameterTypeFullName)
|
|
{
|
|
match = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (match)
|
|
{
|
|
methodToUse = m;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (methodToUse == null)
|
|
{
|
|
throw new Exception($"Unable to resolve the method `{methodReferenceString}` from type `{typeReference}` to a single method from set of {methods.Count} methods");
|
|
}
|
|
|
|
methods.Clear();
|
|
methods.Add(methodToUse);
|
|
}
|
|
|
|
var method = methods[0];
|
|
var methodReference = new MethodReference(method.Name, method.ReturnType, typeReference);
|
|
|
|
foreach (var param in method.Parameters)
|
|
{
|
|
methodReference.Parameters.Add(new ParameterDefinition(param.Name, param.Attributes, param.ParameterType));
|
|
}
|
|
|
|
return methodReference;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Resolves a Cecil <see cref="MethodReference"/> from a reflection <see cref="MethodInfo"/>.
|
|
/// Only used for testing purposes.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// The <see cref="AssemblyDefinition"/> loaded is cached.
|
|
/// </remarks>
|
|
public MethodReference Resolve(System.Reflection.MethodInfo method)
|
|
{
|
|
if (method == null) throw new ArgumentNullException(nameof(method));
|
|
if (method.DeclaringType == null) throw new NotSupportedException($"The method `{method}` must have a declaring type");
|
|
|
|
var thisMethodAssemblyLocation = method.DeclaringType.Assembly.Location;
|
|
var assemblyLocation = thisMethodAssemblyLocation;
|
|
if (assemblyLocation == null)
|
|
{
|
|
throw new ArgumentException($"Cannot determine the assembly location for the method `{method}`", nameof(method));
|
|
}
|
|
if (!File.Exists(assemblyLocation))
|
|
{
|
|
throw new FileNotFoundException($"The assembly [{assemblyLocation}] was not found");
|
|
}
|
|
|
|
AssemblyDefinition definition;
|
|
// Force to load a mono compiled assembly (used on Windows to cross tests between .NET CLR and Mono CLR)
|
|
// For convenience, we add automatically the search directory for the assembly path
|
|
// based on the method being compiled and the generic parameters
|
|
var assemblyLocationFolder = Path.GetDirectoryName(assemblyLocation);
|
|
AddSearchDirectory(assemblyLocationFolder);
|
|
|
|
if (assemblyLocation != thisMethodAssemblyLocation)
|
|
{
|
|
assemblyLocationFolder = Path.GetDirectoryName(thisMethodAssemblyLocation);
|
|
AddSearchDirectory(assemblyLocationFolder);
|
|
}
|
|
|
|
// Helper loop to extract assembly path locations from generic parameters (on declaring type and method)
|
|
// TODO: this is not entirely correct, we would have to inspect deep nested generics to really support this
|
|
foreach (var genericArgument in method.DeclaringType.GetGenericArguments().Concat(method.GetGenericArguments()))
|
|
{
|
|
var location = Path.GetDirectoryName(genericArgument.Assembly.Location);
|
|
AddSearchDirectory(location);
|
|
}
|
|
|
|
var assemblyName = method.DeclaringType.Assembly.GetName();
|
|
definition = Resolve(new AssemblyNameReference(assemblyName.Name, assemblyName.Version));
|
|
|
|
// Resolve the Cecil MethodReference from the System.Reflection.MethodInfo
|
|
var methodReference = definition.MainModule.ImportReference(method);
|
|
|
|
// NOTE: this is a workaround for ref readonly return where Cecil is actually transforming it to an InAttribute as a modreq
|
|
// while the C# modreq is actually `IsReadOnlyAttribute`
|
|
// So in that case we transform the return type with the modreq expected by Cecil
|
|
// otherwise the following methodReference.Resolve() would fail finding the method definition
|
|
// IsReadOnlyAttribute Not Available until netstandard 2.1
|
|
bool hasReadOnlyAttribute = false;
|
|
foreach (var attr in method.ReturnTypeCustomAttributes.GetCustomAttributes(true))
|
|
{
|
|
if (attr.ToString() == "System.Runtime.CompilerServices.IsReadOnlyAttribute")
|
|
{
|
|
hasReadOnlyAttribute = true;
|
|
break;
|
|
}
|
|
}
|
|
if (hasReadOnlyAttribute)
|
|
{
|
|
var typeRef = definition.MainModule.ImportReference(typeof(System.Runtime.InteropServices.InAttribute));
|
|
methodReference.ReturnType = new RequiredModifierType(typeRef, methodReference.ReturnType);
|
|
}
|
|
|
|
if (methodReference?.Resolve() == null)
|
|
{
|
|
throw new InvalidOperationException($"Unable to find method `{methodReference}` from assembly location `{assemblyLocation}`");
|
|
}
|
|
|
|
return methodReference;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Resolves a cecil <see cref="TypeReference"/> from a <see cref="SimpleTypeReferenceString"/>
|
|
/// </summary>
|
|
/// <param name="simpleTypeReferenceString">A type reference string</param>
|
|
/// <returns>A cecil <see cref="TypeReference"/></returns>
|
|
public TypeReference Resolve(SimpleTypeReferenceString simpleTypeReferenceString)
|
|
{
|
|
if (simpleTypeReferenceString == null) throw new ArgumentNullException(nameof(simpleTypeReferenceString));
|
|
var assemblyOfMainType = Resolve(simpleTypeReferenceString.Assembly);
|
|
|
|
var subTypes = simpleTypeReferenceString.FullName.Split(new char[] {'+'});
|
|
Guard.Assert(subTypes.Length >= 1);
|
|
var typeReference = (TypeReference)assemblyOfMainType.MainModule.GetType(subTypes[0]);
|
|
if (typeReference == null)
|
|
{
|
|
throw new InvalidOperationException(
|
|
$"Unable to find type `{simpleTypeReferenceString.FullName}` from assembly `{simpleTypeReferenceString.Assembly}`");
|
|
}
|
|
|
|
for (var i = 1; i < subTypes.Length; i++)
|
|
{
|
|
var subType = subTypes[i];
|
|
var definition = typeReference.StrictResolve();
|
|
var nestedDefinition = definition.NestedTypes.FirstOrDefault(nestedType => nestedType.Name == subType);
|
|
if (nestedDefinition == null)
|
|
{
|
|
throw new InvalidOperationException($"Unable to find nested type `{subType}` from typename `{simpleTypeReferenceString.FullName}` assembly `{simpleTypeReferenceString.Assembly}`");
|
|
}
|
|
typeReference = nestedDefinition;
|
|
}
|
|
|
|
var generic = simpleTypeReferenceString as GenericInstanceTypeReferenceString;
|
|
if (generic != null)
|
|
{
|
|
var genericType = new GenericInstanceType(typeReference);
|
|
foreach (var genericArgType in generic.GenericArguments)
|
|
{
|
|
var simpleGenericArgType = genericArgType as SimpleTypeReferenceString;
|
|
if (simpleGenericArgType == null)
|
|
{
|
|
throw new InvalidOperationException($"Unable to resolve generic argument `{genericArgType}`");
|
|
}
|
|
genericType.GenericArguments.Add(Resolve(simpleGenericArgType));
|
|
}
|
|
typeReference = genericType;
|
|
}
|
|
|
|
return typeReference;
|
|
}
|
|
#endif
|
|
|
|
private void RegisterAssembly(AssemblyNameReference name, AssemblyDefinition assembly)
|
|
{
|
|
if (assembly == null)
|
|
throw new ArgumentNullException(nameof(assembly));
|
|
string fullName = name.FullName;
|
|
var filename = GetAssemblyFileName(assembly);
|
|
var entry = new CacheAssemblyEntry(assembly, filename);
|
|
_nameToEntry[fullName] = entry;
|
|
// Duplicate the entry (mscorlib 2.0.0 can be remapped to 4.0.0, so better cache them both)
|
|
_nameToEntry[assembly.Name.FullName] = entry;
|
|
_fileToEntry[filename] = entry;
|
|
#if BURST_COMPILER_SHARED
|
|
PdbCacheReference.AddEntry(assembly, OnLogDebug);
|
|
#endif
|
|
}
|
|
|
|
private string GetAssemblyFileName(AssemblyDefinition assembly)
|
|
{
|
|
string fileName = assembly.MainModule.FileName;
|
|
if (fileName == null)
|
|
{
|
|
throw new InvalidOperationException($"Unable to find original assembly file from {assembly.Name}");
|
|
}
|
|
return NormalizeFilePath(fileName);
|
|
}
|
|
|
|
protected override void Dispose(bool disposing)
|
|
{
|
|
#if BURST_COMPILER_SHARED
|
|
if (_assemblyWatcherManager != null)
|
|
{
|
|
_assemblyWatcherManager.OnFolderChanged -= OnAssemblyWatcherFolderChanged;
|
|
_assemblyWatcherManager.Dispose();
|
|
}
|
|
#endif
|
|
|
|
Clear();
|
|
base.Dispose(disposing);
|
|
}
|
|
|
|
private class CacheAssemblyEntry
|
|
{
|
|
public CacheAssemblyEntry(AssemblyDefinition assemblyDefinition, string filePath)
|
|
{
|
|
Definition = assemblyDefinition;
|
|
FilePath = filePath;
|
|
FileTime = File.GetLastWriteTime(filePath);
|
|
FileTimeVerified = true;
|
|
}
|
|
|
|
public string Name => Definition.FullName;
|
|
|
|
public readonly AssemblyDefinition Definition;
|
|
|
|
public readonly string FilePath;
|
|
|
|
public readonly DateTime FileTime;
|
|
|
|
public bool FileTimeVerified { get; set; }
|
|
|
|
public override string ToString() => $"{Name} => {FilePath} {DateTimeToStringPrecise(FileTime)}";
|
|
}
|
|
|
|
private static string NormalizeFilePath(string path)
|
|
{
|
|
return Path.GetFullPath(new Uri(path).LocalPath).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
|
|
}
|
|
|
|
private class CustomMetadataResolver : MetadataResolver
|
|
{
|
|
public CustomMetadataResolver(IAssemblyResolver assemblyResolver) : base(assemblyResolver)
|
|
{
|
|
}
|
|
|
|
public override MethodDefinition Resolve(MethodReference method)
|
|
{
|
|
if (method is MethodDefinition methodDef)
|
|
{
|
|
return methodDef;
|
|
}
|
|
|
|
if (method.GetElementMethod() is MethodDefinition methodDef2)
|
|
{
|
|
return methodDef2;
|
|
}
|
|
|
|
return base.Resolve(method);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Custom implementation of <see cref="ISymbolReaderProvider"/> to:
|
|
/// - to load pdb/mdb through a MemoryStream to avoid locking the file on the disk
|
|
/// - catch any exceptions while loading the symbols and report them back
|
|
/// </summary>
|
|
private class CustomSymbolReaderProvider : ISymbolReaderProvider
|
|
{
|
|
private readonly Action<string, Exception> _logException;
|
|
|
|
public CustomSymbolReaderProvider(Action<string, Exception> logException)
|
|
{
|
|
_logException = logException;
|
|
}
|
|
|
|
public ISymbolReader GetSymbolReader(ModuleDefinition module, string fileName)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(fileName)) return null;
|
|
|
|
string pdbFileName = fileName;
|
|
try
|
|
{
|
|
fileName = NormalizeFilePath(fileName);
|
|
pdbFileName = GetPdbFileName(fileName);
|
|
|
|
if (File.Exists(pdbFileName))
|
|
{
|
|
var pdbStream = ReadToMemoryStream(pdbFileName);
|
|
if (IsPortablePdb(pdbStream))
|
|
return new SafeDebugReaderProvider(new PortablePdbReaderProvider().GetSymbolReader(module, pdbStream));
|
|
|
|
return new SafeDebugReaderProvider(new NativePdbReaderProvider().GetSymbolReader(module, pdbStream));
|
|
}
|
|
}
|
|
catch (Exception ex) when (_logException != null)
|
|
{
|
|
_logException?.Invoke($"Unable to load symbol `{pdbFileName}`", ex);
|
|
return null;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
private static MemoryStream ReadToMemoryStream(string filename)
|
|
{
|
|
return new MemoryStream(File.ReadAllBytes(filename));
|
|
}
|
|
|
|
public ISymbolReader GetSymbolReader(ModuleDefinition module, Stream symbolStream)
|
|
{
|
|
throw new NotSupportedException();
|
|
}
|
|
|
|
private static string GetPdbFileName(string assemblyFileName)
|
|
{
|
|
return Path.ChangeExtension(assemblyFileName, ".pdb");
|
|
}
|
|
|
|
private static bool IsPortablePdb(Stream stream)
|
|
{
|
|
if (stream.Length < 4L)
|
|
return false;
|
|
long position = stream.Position;
|
|
try
|
|
{
|
|
return (int)new BinaryReader(stream).ReadUInt32() == 1112167234;
|
|
}
|
|
finally
|
|
{
|
|
stream.Position = position;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// This class is a wrapper around <see cref="ISymbolReader"/> to protect
|
|
/// against failure while trying to read debug information in Mono.Cecil
|
|
/// </summary>
|
|
private class SafeDebugReaderProvider : ISymbolReader
|
|
{
|
|
private readonly ISymbolReader _reader;
|
|
|
|
public SafeDebugReaderProvider(ISymbolReader reader)
|
|
{
|
|
_reader = reader;
|
|
}
|
|
|
|
|
|
public void Dispose()
|
|
{
|
|
try
|
|
{
|
|
_reader.Dispose();
|
|
}
|
|
catch
|
|
{
|
|
// ignored
|
|
}
|
|
}
|
|
|
|
public ISymbolWriterProvider GetWriterProvider()
|
|
{
|
|
// We are not protecting here as we are not suppose to write to PDBs
|
|
return _reader.GetWriterProvider();
|
|
}
|
|
|
|
public bool ProcessDebugHeader(ImageDebugHeader header)
|
|
{
|
|
try
|
|
{
|
|
return _reader.ProcessDebugHeader(header);
|
|
}
|
|
catch
|
|
{
|
|
// ignored
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public MethodDebugInformation Read(MethodDefinition method)
|
|
{
|
|
try
|
|
{
|
|
return _reader.Read(method);
|
|
}
|
|
catch
|
|
{
|
|
// ignored
|
|
}
|
|
return null;
|
|
}
|
|
}
|
|
}
|
|
|
|
#if !BURST_COMPILER_SHARED
|
|
public enum LogMessageType
|
|
{
|
|
Debug,
|
|
}
|
|
#endif
|
|
}
|
|
|
|
/// <summary>
|
|
/// This class is a container for keeping an assembly reference and path together
|
|
/// </summary>
|
|
internal sealed class AssemblyNameReferenceAndPath
|
|
{
|
|
/// <summary>
|
|
/// Get the assembly name reference
|
|
/// </summary>
|
|
public readonly AssemblyNameReference AssemblyNameReference;
|
|
/// <summary>
|
|
/// Get the full path to the assembly
|
|
/// </summary>
|
|
public readonly string FullPath;
|
|
|
|
internal AssemblyNameReferenceAndPath(AssemblyNameReference assemblyNameReference, string fullPath)
|
|
{
|
|
AssemblyNameReference = assemblyNameReference;
|
|
FullPath = fullPath;
|
|
}
|
|
}
|
|
}
|