A game about forced loneliness, made by TACStudios
1using System;
2using System.Collections.Generic;
3using System.IO;
4using System.Reflection;
5using System.Threading;
6using Mono.Cecil;
7using Mono.Cecil.Cil;
8using Unity.CompilationPipeline.Common.Diagnostics;
9using Unity.CompilationPipeline.Common.ILPostProcessing;
10
11namespace Unity.Jobs.CodeGen
12{
13 // Jobs ILPP entry point
14 internal partial class JobsILPostProcessor : ILPostProcessor
15 {
16 AssemblyDefinition AssemblyDefinition;
17 List<DiagnosticMessage> DiagnosticMessages = new List<DiagnosticMessage>();
18 public HashSet<string> Defines { get; private set; }
19
20 public override ILPostProcessResult Process(ICompiledAssembly compiledAssembly)
21 {
22 bool madeAnyChange = false;
23 Defines = new HashSet<string>(compiledAssembly.Defines);
24
25 try
26 {
27 AssemblyDefinition = AssemblyDefinitionFor(compiledAssembly);
28 }
29 catch (BadImageFormatException)
30 {
31 return new ILPostProcessResult(null, DiagnosticMessages);
32 }
33
34 try
35 {
36 // This only works because the PostProcessorAssemblyResolver is explicitly loading
37 // transitive dependencies (and then some) and so if we can't find a references to
38 // Unity.Jobs (via EarlyInitHelpers) in there than we are confident the assembly doesn't need processing
39 var earlyInitHelpers = AssemblyDefinition.MainModule.ImportReference(typeof(EarlyInitHelpers)).CheckedResolve();
40 }
41 catch (ResolutionException)
42 {
43 return new ILPostProcessResult(null, DiagnosticMessages);
44 }
45
46 madeAnyChange = PostProcessImpl();
47
48 // Hack to remove circular references
49 var selfName = AssemblyDefinition.Name.FullName;
50 foreach (var referenceName in AssemblyDefinition.MainModule.AssemblyReferences)
51 {
52 if (referenceName.FullName == selfName)
53 {
54 AssemblyDefinition.MainModule.AssemblyReferences.Remove(referenceName);
55 break;
56 }
57 }
58
59 if (!madeAnyChange)
60 return new ILPostProcessResult(null, DiagnosticMessages);
61
62 bool hasError = false;
63 foreach (var d in DiagnosticMessages)
64 {
65 if (d.DiagnosticType == DiagnosticType.Error)
66 {
67 hasError = true;
68 break;
69 }
70 }
71
72 if (hasError)
73 return new ILPostProcessResult(null, DiagnosticMessages);
74
75 var pe = new MemoryStream();
76 var pdb = new MemoryStream();
77 var writerParameters = new WriterParameters
78 {
79 SymbolWriterProvider = new PortablePdbWriterProvider(), SymbolStream = pdb, WriteSymbols = true
80 };
81
82 AssemblyDefinition.Write(pe, writerParameters);
83 return new ILPostProcessResult(new InMemoryAssembly(pe.ToArray(), pdb.ToArray()), DiagnosticMessages);
84 }
85
86 public override ILPostProcessor GetInstance()
87 {
88 return this;
89 }
90
91 public override bool WillProcess(ICompiledAssembly compiledAssembly)
92 {
93 if (compiledAssembly.Name.EndsWith("CodeGen.Tests", StringComparison.Ordinal))
94 return false;
95
96 if (compiledAssembly.InMemoryAssembly.PdbData == null || compiledAssembly.InMemoryAssembly.PeData == null)
97 return false;
98
99 var referencesCollections = false;
100 if (compiledAssembly.Name == "Unity.Collections")
101 {
102 return true;
103 }
104 for (int i = 0; i < compiledAssembly.References.Length; ++i)
105 {
106 var fileName = Path.GetFileNameWithoutExtension(compiledAssembly.References[i]);
107 if (fileName == "Unity.Collections")
108 {
109 referencesCollections = true;
110 break;
111 }
112 }
113
114 if (!referencesCollections) return false;
115
116 return true;
117 }
118
119 // *******************************************************************************
120 // ** NOTE
121 // ** Everything below this is a copy of the same process used in EntitiesILPostProcessor and
122 // ** should stay synced with it.
123 // *******************************************************************************
124
125 class PostProcessorAssemblyResolver : IAssemblyResolver
126 {
127 private readonly HashSet<string> _referenceDirectories;
128 private Dictionary<string, HashSet<string>> _referenceToPathMap;
129 Dictionary<string, AssemblyDefinition> _cache = new Dictionary<string, AssemblyDefinition>();
130 private ICompiledAssembly _compiledAssembly;
131 private AssemblyDefinition _selfAssembly;
132
133 public PostProcessorAssemblyResolver(ICompiledAssembly compiledAssembly)
134 {
135 _compiledAssembly = compiledAssembly;
136 _referenceToPathMap = new Dictionary<string, HashSet<string>>();
137 _referenceDirectories = new HashSet<string>();
138 foreach (var reference in compiledAssembly.References)
139 {
140 var assemblyName = Path.GetFileNameWithoutExtension(reference);
141 if (!_referenceToPathMap.TryGetValue(assemblyName, out var fileList))
142 {
143 fileList = new HashSet<string>();
144 _referenceToPathMap.Add(assemblyName, fileList);
145 }
146 fileList.Add(reference);
147 _referenceDirectories.Add(Path.GetDirectoryName(reference));
148 }
149 }
150
151 public void Dispose()
152 {
153 }
154
155 public AssemblyDefinition Resolve(AssemblyNameReference name)
156 {
157 return Resolve(name, new ReaderParameters(ReadingMode.Deferred));
158 }
159
160 public AssemblyDefinition Resolve(AssemblyNameReference name, ReaderParameters parameters)
161 {
162 {
163 if (name.Name == _compiledAssembly.Name)
164 return _selfAssembly;
165
166 var fileName = FindFile(name);
167 if (fileName == null)
168 return null;
169
170 var cacheKey = fileName;
171
172 if (_cache.TryGetValue(cacheKey, out var result))
173 return result;
174
175 parameters.AssemblyResolver = this;
176
177 var ms = MemoryStreamFor(fileName);
178
179 var pdb = fileName + ".pdb";
180 if (File.Exists(pdb))
181 parameters.SymbolStream = MemoryStreamFor(pdb);
182
183 var assemblyDefinition = AssemblyDefinition.ReadAssembly(ms, parameters);
184 _cache.Add(cacheKey, assemblyDefinition);
185 return assemblyDefinition;
186 }
187 }
188
189 private string FindFile(AssemblyNameReference name)
190 {
191 if (_referenceToPathMap.TryGetValue(name.Name, out var paths))
192 {
193 if (paths.Count == 1)
194 {
195 var enumerator = paths.GetEnumerator();
196 // enumerators begin before the first element
197 enumerator.MoveNext();
198 return enumerator.Current;
199 }
200
201 // If we have more than one assembly with the same name loaded we now need to figure out which one
202 // is being requested based on the AssemblyNameReference
203 foreach (var path in paths)
204 {
205 var onDiskAssemblyName = AssemblyName.GetAssemblyName(path);
206 if (onDiskAssemblyName.FullName == name.FullName)
207 return path;
208 }
209 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)}");
210 }
211
212 // Unfortunately the current ICompiledAssembly API only provides direct references.
213 // It is very much possible that a postprocessor ends up investigating a type in a directly
214 // referenced assembly, that contains a field that is not in a directly referenced assembly.
215 // if we don't do anything special for that situation, it will fail to resolve. We should fix this
216 // in the ILPostProcessing api. As a workaround, we rely on the fact here that the indirect references
217 // are always located next to direct references, so we search in all directories of direct references we
218 // got passed, and if we find the file in there, we resolve to it.
219 foreach (var parentDir in _referenceDirectories)
220 {
221 var candidate = Path.Combine(parentDir, name.Name + ".dll");
222 if (File.Exists(candidate))
223 {
224 if (!_referenceToPathMap.TryGetValue(candidate, out var referencePaths))
225 {
226 referencePaths = new HashSet<string>();
227 _referenceToPathMap.Add(candidate, referencePaths);
228 }
229 referencePaths.Add(candidate);
230
231 return candidate;
232 }
233 }
234
235 return null;
236 }
237
238 static MemoryStream MemoryStreamFor(string fileName)
239 {
240 return Retry(10, TimeSpan.FromSeconds(1), () => {
241 byte[] byteArray;
242 using (var fs = new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
243 {
244 byteArray = new byte[fs.Length];
245 var readLength = fs.Read(byteArray, 0, (int)fs.Length);
246 if (readLength != fs.Length)
247 throw new InvalidOperationException("File read length is not full length of file.");
248 }
249
250 return new MemoryStream(byteArray);
251 });
252 }
253
254 private static MemoryStream Retry(int retryCount, TimeSpan waitTime, Func<MemoryStream> func)
255 {
256 try
257 {
258 return func();
259 }
260 catch (IOException)
261 {
262 if (retryCount == 0)
263 throw;
264 Console.WriteLine($"Caught IO Exception, trying {retryCount} more times");
265 Thread.Sleep(waitTime);
266 return Retry(retryCount - 1, waitTime, func);
267 }
268 }
269
270 public void AddAssemblyDefinitionBeingOperatedOn(AssemblyDefinition assemblyDefinition)
271 {
272 _selfAssembly = assemblyDefinition;
273 }
274 }
275
276 internal static AssemblyDefinition AssemblyDefinitionFor(ICompiledAssembly compiledAssembly)
277 {
278 var resolver = new PostProcessorAssemblyResolver(compiledAssembly);
279 var readerParameters = new ReaderParameters
280 {
281 SymbolStream = new MemoryStream(compiledAssembly.InMemoryAssembly.PdbData),
282 SymbolReaderProvider = new PortablePdbReaderProvider(),
283 AssemblyResolver = resolver,
284 ReflectionImporterProvider = new PostProcessorReflectionImporterProvider(),
285 ReadingMode = ReadingMode.Immediate
286 };
287
288 var peStream = new MemoryStream(compiledAssembly.InMemoryAssembly.PeData);
289 var assemblyDefinition = AssemblyDefinition.ReadAssembly(peStream, readerParameters);
290
291 //apparently, it will happen that when we ask to resolve a type that lives inside Unity.Jobs, and we
292 //are also postprocessing Unity.Jobs, type resolving will fail, because we do not actually try to resolve
293 //inside the assembly we are processing. Let's make sure we do that, so that we can use postprocessor features inside
294 //unity.Jobs itself as well.
295 resolver.AddAssemblyDefinitionBeingOperatedOn(assemblyDefinition);
296
297 return assemblyDefinition;
298 }
299 }
300
301 internal class PostProcessorReflectionImporterProvider : IReflectionImporterProvider
302 {
303 public IReflectionImporter GetReflectionImporter(ModuleDefinition module)
304 {
305 return new PostProcessorReflectionImporter(module);
306 }
307 }
308
309 internal class PostProcessorReflectionImporter : DefaultReflectionImporter
310 {
311 private const string SystemPrivateCoreLib = "System.Private.CoreLib";
312 private AssemblyNameReference _correctCorlib;
313
314 public PostProcessorReflectionImporter(ModuleDefinition module) : base(module)
315 {
316 _correctCorlib = default;
317 foreach (var a in module.AssemblyReferences)
318 {
319 if (a.Name == "mscorlib" || a.Name == "netstandard" || a.Name == SystemPrivateCoreLib)
320 {
321 _correctCorlib = a;
322 break;
323 }
324 }
325 }
326
327 public override AssemblyNameReference ImportReference(AssemblyName reference)
328 {
329 if (_correctCorlib != null && reference.Name == SystemPrivateCoreLib)
330 return _correctCorlib;
331
332 return base.ImportReference(reference);
333 }
334 }
335}