using System; using System.IO; using System.Linq; using Mono.Cecil; using Mono.Cecil.Cil; using Mono.Cecil.Pdb; namespace zzzUnity.Burst.CodeGen { /// /// Provides an assembly resolver with deferred loading and a custom metadata resolver. /// /// /// This class is not thread safe. It needs to be protected outside. /// #if BURST_COMPILER_SHARED public #else internal #endif class AssemblyResolver : BaseAssemblyResolver { private readonly ReadingMode _readingMode; public AssemblyResolver(ReadingMode readingMode = ReadingMode.Deferred) { _readingMode = readingMode; // 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 } public bool LoadDebugSymbols { get; set; } protected 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; 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) { var readerParameters = CreateReaderParameters(); readerParameters.ReadingMode = _readingMode; AssemblyDefinition assemblyDefinition; try { assemblyDefinition = Resolve(name, readerParameters); } catch (Exception ex) { if (readerParameters.ReadSymbols == true) { // Attempt to load without symbols readerParameters.ReadSymbols = false; assemblyDefinition = Resolve(name, readerParameters); } else { throw new AssemblyResolutionException( name, new Exception($"Failed to resolve assembly '{name}' in directories: {string.Join(Environment.NewLine, GetSearchDirectories())}", ex)); } } return assemblyDefinition; } public bool TryResolve(AssemblyNameReference name, out AssemblyDefinition assembly) { try { assembly = Resolve(name); return true; } catch (AssemblyResolutionException) { assembly = null; return false; } } public new void AddSearchDirectory(string directory) { if (!GetSearchDirectories().Contains(directory)) { base.AddSearchDirectory(directory); } } 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; } internal static string NormalizeFilePath(string path) { try { return Path.GetFullPath(new Uri(path).LocalPath).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); } catch (Exception ex) { throw new Exception($"Could not normalize file path: {path}", ex); } } 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); } } /// /// Custom implementation of 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 /// private class CustomSymbolReaderProvider : ISymbolReaderProvider { private readonly Action _logException; public CustomSymbolReaderProvider(Action 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; } } /// /// This class is a wrapper around to protect /// against failure while trying to read debug information in Mono.Cecil /// 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; } } } } }