336 lines
14 KiB
C#
336 lines
14 KiB
C#
|
using System;
|
||
|
using System.Collections.Generic;
|
||
|
using System.IO;
|
||
|
using System.Reflection;
|
||
|
using System.Threading;
|
||
|
using Mono.Cecil;
|
||
|
using Mono.Cecil.Cil;
|
||
|
using Unity.CompilationPipeline.Common.Diagnostics;
|
||
|
using Unity.CompilationPipeline.Common.ILPostProcessing;
|
||
|
|
||
|
namespace Unity.Jobs.CodeGen
|
||
|
{
|
||
|
// Jobs ILPP entry point
|
||
|
internal partial class JobsILPostProcessor : ILPostProcessor
|
||
|
{
|
||
|
AssemblyDefinition AssemblyDefinition;
|
||
|
List<DiagnosticMessage> DiagnosticMessages = new List<DiagnosticMessage>();
|
||
|
public HashSet<string> Defines { get; private set; }
|
||
|
|
||
|
public override ILPostProcessResult Process(ICompiledAssembly compiledAssembly)
|
||
|
{
|
||
|
bool madeAnyChange = false;
|
||
|
Defines = new HashSet<string>(compiledAssembly.Defines);
|
||
|
|
||
|
try
|
||
|
{
|
||
|
AssemblyDefinition = AssemblyDefinitionFor(compiledAssembly);
|
||
|
}
|
||
|
catch (BadImageFormatException)
|
||
|
{
|
||
|
return new ILPostProcessResult(null, DiagnosticMessages);
|
||
|
}
|
||
|
|
||
|
try
|
||
|
{
|
||
|
// This only works because the PostProcessorAssemblyResolver is explicitly loading
|
||
|
// transitive dependencies (and then some) and so if we can't find a references to
|
||
|
// Unity.Jobs (via EarlyInitHelpers) in there than we are confident the assembly doesn't need processing
|
||
|
var earlyInitHelpers = AssemblyDefinition.MainModule.ImportReference(typeof(EarlyInitHelpers)).CheckedResolve();
|
||
|
}
|
||
|
catch (ResolutionException)
|
||
|
{
|
||
|
return new ILPostProcessResult(null, DiagnosticMessages);
|
||
|
}
|
||
|
|
||
|
madeAnyChange = PostProcessImpl();
|
||
|
|
||
|
// Hack to remove circular references
|
||
|
var selfName = AssemblyDefinition.Name.FullName;
|
||
|
foreach (var referenceName in AssemblyDefinition.MainModule.AssemblyReferences)
|
||
|
{
|
||
|
if (referenceName.FullName == selfName)
|
||
|
{
|
||
|
AssemblyDefinition.MainModule.AssemblyReferences.Remove(referenceName);
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (!madeAnyChange)
|
||
|
return new ILPostProcessResult(null, DiagnosticMessages);
|
||
|
|
||
|
bool hasError = false;
|
||
|
foreach (var d in DiagnosticMessages)
|
||
|
{
|
||
|
if (d.DiagnosticType == DiagnosticType.Error)
|
||
|
{
|
||
|
hasError = true;
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (hasError)
|
||
|
return new ILPostProcessResult(null, DiagnosticMessages);
|
||
|
|
||
|
var pe = new MemoryStream();
|
||
|
var pdb = new MemoryStream();
|
||
|
var writerParameters = new WriterParameters
|
||
|
{
|
||
|
SymbolWriterProvider = new PortablePdbWriterProvider(), SymbolStream = pdb, WriteSymbols = true
|
||
|
};
|
||
|
|
||
|
AssemblyDefinition.Write(pe, writerParameters);
|
||
|
return new ILPostProcessResult(new InMemoryAssembly(pe.ToArray(), pdb.ToArray()), DiagnosticMessages);
|
||
|
}
|
||
|
|
||
|
public override ILPostProcessor GetInstance()
|
||
|
{
|
||
|
return this;
|
||
|
}
|
||
|
|
||
|
public override bool WillProcess(ICompiledAssembly compiledAssembly)
|
||
|
{
|
||
|
if (compiledAssembly.Name.EndsWith("CodeGen.Tests", StringComparison.Ordinal))
|
||
|
return false;
|
||
|
|
||
|
if (compiledAssembly.InMemoryAssembly.PdbData == null || compiledAssembly.InMemoryAssembly.PeData == null)
|
||
|
return false;
|
||
|
|
||
|
var referencesCollections = false;
|
||
|
if (compiledAssembly.Name == "Unity.Collections")
|
||
|
{
|
||
|
return true;
|
||
|
}
|
||
|
for (int i = 0; i < compiledAssembly.References.Length; ++i)
|
||
|
{
|
||
|
var fileName = Path.GetFileNameWithoutExtension(compiledAssembly.References[i]);
|
||
|
if (fileName == "Unity.Collections")
|
||
|
{
|
||
|
referencesCollections = true;
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (!referencesCollections) return false;
|
||
|
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
// *******************************************************************************
|
||
|
// ** NOTE
|
||
|
// ** Everything below this is a copy of the same process used in EntitiesILPostProcessor and
|
||
|
// ** should stay synced with it.
|
||
|
// *******************************************************************************
|
||
|
|
||
|
class PostProcessorAssemblyResolver : IAssemblyResolver
|
||
|
{
|
||
|
private readonly HashSet<string> _referenceDirectories;
|
||
|
private Dictionary<string, HashSet<string>> _referenceToPathMap;
|
||
|
Dictionary<string, AssemblyDefinition> _cache = new Dictionary<string, AssemblyDefinition>();
|
||
|
private ICompiledAssembly _compiledAssembly;
|
||
|
private AssemblyDefinition _selfAssembly;
|
||
|
|
||
|
public PostProcessorAssemblyResolver(ICompiledAssembly compiledAssembly)
|
||
|
{
|
||
|
_compiledAssembly = compiledAssembly;
|
||
|
_referenceToPathMap = new Dictionary<string, HashSet<string>>();
|
||
|
_referenceDirectories = new HashSet<string>();
|
||
|
foreach (var reference in compiledAssembly.References)
|
||
|
{
|
||
|
var assemblyName = Path.GetFileNameWithoutExtension(reference);
|
||
|
if (!_referenceToPathMap.TryGetValue(assemblyName, out var fileList))
|
||
|
{
|
||
|
fileList = new HashSet<string>();
|
||
|
_referenceToPathMap.Add(assemblyName, fileList);
|
||
|
}
|
||
|
fileList.Add(reference);
|
||
|
_referenceDirectories.Add(Path.GetDirectoryName(reference));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
public void Dispose()
|
||
|
{
|
||
|
}
|
||
|
|
||
|
public AssemblyDefinition Resolve(AssemblyNameReference name)
|
||
|
{
|
||
|
return Resolve(name, new ReaderParameters(ReadingMode.Deferred));
|
||
|
}
|
||
|
|
||
|
public AssemblyDefinition Resolve(AssemblyNameReference name, ReaderParameters parameters)
|
||
|
{
|
||
|
{
|
||
|
if (name.Name == _compiledAssembly.Name)
|
||
|
return _selfAssembly;
|
||
|
|
||
|
var fileName = FindFile(name);
|
||
|
if (fileName == null)
|
||
|
return null;
|
||
|
|
||
|
var cacheKey = fileName;
|
||
|
|
||
|
if (_cache.TryGetValue(cacheKey, out var result))
|
||
|
return result;
|
||
|
|
||
|
parameters.AssemblyResolver = this;
|
||
|
|
||
|
var ms = MemoryStreamFor(fileName);
|
||
|
|
||
|
var pdb = fileName + ".pdb";
|
||
|
if (File.Exists(pdb))
|
||
|
parameters.SymbolStream = MemoryStreamFor(pdb);
|
||
|
|
||
|
var assemblyDefinition = AssemblyDefinition.ReadAssembly(ms, parameters);
|
||
|
_cache.Add(cacheKey, assemblyDefinition);
|
||
|
return assemblyDefinition;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private string FindFile(AssemblyNameReference name)
|
||
|
{
|
||
|
if (_referenceToPathMap.TryGetValue(name.Name, out var paths))
|
||
|
{
|
||
|
if (paths.Count == 1)
|
||
|
{
|
||
|
var enumerator = paths.GetEnumerator();
|
||
|
// enumerators begin before the first element
|
||
|
enumerator.MoveNext();
|
||
|
return enumerator.Current;
|
||
|
}
|
||
|
|
||
|
// If we have more than one assembly with the same name loaded we now need to figure out which one
|
||
|
// is being requested based on the AssemblyNameReference
|
||
|
foreach (var path in paths)
|
||
|
{
|
||
|
var onDiskAssemblyName = AssemblyName.GetAssemblyName(path);
|
||
|
if (onDiskAssemblyName.FullName == name.FullName)
|
||
|
return path;
|
||
|
}
|
||
|
throw new ArgumentException($"Tried to resolve a reference in assembly '{name.FullName}' however the assembly could not be found. Known references which did not match: \n{string.Join("\n",paths)}");
|
||
|
}
|
||
|
|
||
|
// Unfortunately the current ICompiledAssembly API only provides direct references.
|
||
|
// It is very much possible that a postprocessor ends up investigating a type in a directly
|
||
|
// referenced assembly, that contains a field that is not in a directly referenced assembly.
|
||
|
// if we don't do anything special for that situation, it will fail to resolve. We should fix this
|
||
|
// in the ILPostProcessing api. As a workaround, we rely on the fact here that the indirect references
|
||
|
// are always located next to direct references, so we search in all directories of direct references we
|
||
|
// got passed, and if we find the file in there, we resolve to it.
|
||
|
foreach (var parentDir in _referenceDirectories)
|
||
|
{
|
||
|
var candidate = Path.Combine(parentDir, name.Name + ".dll");
|
||
|
if (File.Exists(candidate))
|
||
|
{
|
||
|
if (!_referenceToPathMap.TryGetValue(candidate, out var referencePaths))
|
||
|
{
|
||
|
referencePaths = new HashSet<string>();
|
||
|
_referenceToPathMap.Add(candidate, referencePaths);
|
||
|
}
|
||
|
referencePaths.Add(candidate);
|
||
|
|
||
|
return candidate;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
static MemoryStream MemoryStreamFor(string fileName)
|
||
|
{
|
||
|
return Retry(10, TimeSpan.FromSeconds(1), () => {
|
||
|
byte[] byteArray;
|
||
|
using (var fs = new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
|
||
|
{
|
||
|
byteArray = new byte[fs.Length];
|
||
|
var readLength = fs.Read(byteArray, 0, (int)fs.Length);
|
||
|
if (readLength != fs.Length)
|
||
|
throw new InvalidOperationException("File read length is not full length of file.");
|
||
|
}
|
||
|
|
||
|
return new MemoryStream(byteArray);
|
||
|
});
|
||
|
}
|
||
|
|
||
|
private static MemoryStream Retry(int retryCount, TimeSpan waitTime, Func<MemoryStream> func)
|
||
|
{
|
||
|
try
|
||
|
{
|
||
|
return func();
|
||
|
}
|
||
|
catch (IOException)
|
||
|
{
|
||
|
if (retryCount == 0)
|
||
|
throw;
|
||
|
Console.WriteLine($"Caught IO Exception, trying {retryCount} more times");
|
||
|
Thread.Sleep(waitTime);
|
||
|
return Retry(retryCount - 1, waitTime, func);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
public void AddAssemblyDefinitionBeingOperatedOn(AssemblyDefinition assemblyDefinition)
|
||
|
{
|
||
|
_selfAssembly = assemblyDefinition;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
internal static AssemblyDefinition AssemblyDefinitionFor(ICompiledAssembly compiledAssembly)
|
||
|
{
|
||
|
var resolver = new PostProcessorAssemblyResolver(compiledAssembly);
|
||
|
var readerParameters = new ReaderParameters
|
||
|
{
|
||
|
SymbolStream = new MemoryStream(compiledAssembly.InMemoryAssembly.PdbData),
|
||
|
SymbolReaderProvider = new PortablePdbReaderProvider(),
|
||
|
AssemblyResolver = resolver,
|
||
|
ReflectionImporterProvider = new PostProcessorReflectionImporterProvider(),
|
||
|
ReadingMode = ReadingMode.Immediate
|
||
|
};
|
||
|
|
||
|
var peStream = new MemoryStream(compiledAssembly.InMemoryAssembly.PeData);
|
||
|
var assemblyDefinition = AssemblyDefinition.ReadAssembly(peStream, readerParameters);
|
||
|
|
||
|
//apparently, it will happen that when we ask to resolve a type that lives inside Unity.Jobs, and we
|
||
|
//are also postprocessing Unity.Jobs, type resolving will fail, because we do not actually try to resolve
|
||
|
//inside the assembly we are processing. Let's make sure we do that, so that we can use postprocessor features inside
|
||
|
//unity.Jobs itself as well.
|
||
|
resolver.AddAssemblyDefinitionBeingOperatedOn(assemblyDefinition);
|
||
|
|
||
|
return assemblyDefinition;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
internal class PostProcessorReflectionImporterProvider : IReflectionImporterProvider
|
||
|
{
|
||
|
public IReflectionImporter GetReflectionImporter(ModuleDefinition module)
|
||
|
{
|
||
|
return new PostProcessorReflectionImporter(module);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
internal class PostProcessorReflectionImporter : DefaultReflectionImporter
|
||
|
{
|
||
|
private const string SystemPrivateCoreLib = "System.Private.CoreLib";
|
||
|
private AssemblyNameReference _correctCorlib;
|
||
|
|
||
|
public PostProcessorReflectionImporter(ModuleDefinition module) : base(module)
|
||
|
{
|
||
|
_correctCorlib = default;
|
||
|
foreach (var a in module.AssemblyReferences)
|
||
|
{
|
||
|
if (a.Name == "mscorlib" || a.Name == "netstandard" || a.Name == SystemPrivateCoreLib)
|
||
|
{
|
||
|
_correctCorlib = a;
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
public override AssemblyNameReference ImportReference(AssemblyName reference)
|
||
|
{
|
||
|
if (_correctCorlib != null && reference.Name == SystemPrivateCoreLib)
|
||
|
return _correctCorlib;
|
||
|
|
||
|
return base.ImportReference(reference);
|
||
|
}
|
||
|
}
|
||
|
}
|