-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
I now use a Cecil to inject the module initializer
This allows me to have explicit ordering of the loading of modules and not cause conflicts
- Loading branch information
1 parent
0ace226
commit bd9c283
Showing
13 changed files
with
328 additions
and
17 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
namespace SourceGenerator.Foundations.Injector | ||
{ | ||
internal static class ExceptionFactory | ||
{ | ||
internal static InjectionException AssemblyDoesNotExist(string assembly) | ||
{ | ||
return new InjectionException($"Assembly '{assembly}' does not exist"); | ||
} | ||
|
||
internal static InjectionException NoModuleInitializerTypeFound() | ||
{ | ||
return new InjectionException("Found no type named 'ModuleInitializer', this type must exist or the ModuleInitializer parameter must be used"); | ||
} | ||
|
||
internal static InjectionException InvalidFormatForModuleInitializer() | ||
{ | ||
return new InjectionException("Invalid format for ModuleInitializer parameter, use Full.Type.Name::MethodName"); | ||
} | ||
|
||
internal static InjectionException TypeNameDoesNotExist(string typeName) | ||
{ | ||
return new InjectionException($"No type named '{typeName}' exists in the given assembly!"); | ||
} | ||
|
||
internal static InjectionException MethodNameDoesNotExist(string typeName, string methodName) | ||
{ | ||
return new InjectionException($"No method named '{methodName}' exists in the type '{methodName}'"); | ||
} | ||
|
||
internal static InjectionException ModuleInitializerMayNotBePrivate() | ||
{ | ||
return new InjectionException("Module initializer method may not be private or protected, use public or internal instead"); | ||
} | ||
|
||
internal static InjectionException ModuleInitializerMustBeVoid() | ||
{ | ||
return new InjectionException("Module initializer method must have 'void' as return new InjectionException(type"); | ||
} | ||
|
||
internal static InjectionException ModuleInitializerMayNotHaveParameters() | ||
{ | ||
return new InjectionException("Module initializer method must not have any parameters"); | ||
} | ||
|
||
internal static InjectionException ModuleInitializerMustBeStatic() | ||
{ | ||
return new InjectionException("Module initializer method must be static"); | ||
} | ||
|
||
internal static Exception ModuleNotFound() | ||
{ | ||
return new InjectionException("Unable to find main module in assembly"); | ||
} | ||
} | ||
} |
7 changes: 7 additions & 0 deletions
7
src/SourceGenerator.Foundations.Injector/InjectionException.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
namespace SourceGenerator.Foundations.Injector | ||
{ | ||
public class InjectionException : Exception | ||
{ | ||
public InjectionException(string msg) : base(msg) { } | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,150 @@ | ||
using Mono.Cecil; | ||
using Mono.Cecil.Cil; | ||
using Mono.Cecil.Pdb; | ||
using Mono.Collections.Generic; | ||
using System.Diagnostics; | ||
|
||
namespace SourceGenerator.Foundations.Injector | ||
{ | ||
|
||
internal class Injector : IDisposable | ||
{ | ||
private AssemblyDefinition Assembly { get; set; } | ||
Check warning on line 12 in src/SourceGenerator.Foundations.Injector/Injector.cs GitHub Actions / build
Check warning on line 12 in src/SourceGenerator.Foundations.Injector/Injector.cs GitHub Actions / build
Check warning on line 12 in src/SourceGenerator.Foundations.Injector/Injector.cs GitHub Actions / build, bundle & publish
Check warning on line 12 in src/SourceGenerator.Foundations.Injector/Injector.cs GitHub Actions / build, bundle & publish
Check warning on line 12 in src/SourceGenerator.Foundations.Injector/Injector.cs GitHub Actions / build, bundle & publish
|
||
|
||
private string? PdbFile(string assemblyFile) | ||
{ | ||
Debug.Assert(assemblyFile != null); | ||
string path = Path.ChangeExtension(assemblyFile, ".pdb"); | ||
return File.Exists(path) ? path : null; | ||
} | ||
|
||
public void Inject(string assemblyFile, string typeName, string methodName) | ||
{ | ||
try | ||
{ | ||
if (!File.Exists(assemblyFile)) | ||
{ | ||
throw ExceptionFactory.AssemblyDoesNotExist(assemblyFile); | ||
} | ||
|
||
ReadAssembly(assemblyFile); | ||
MethodReference callee = GetCalleeMethod(typeName, methodName); | ||
InjectInitializer(callee); | ||
|
||
WriteAssembly(assemblyFile, methodName); | ||
} | ||
catch (Exception ex) | ||
{ | ||
throw new InjectionException(ex.Message); | ||
} | ||
} | ||
|
||
private void InjectInitializer(MethodReference callee) | ||
{ | ||
Debug.Assert(Assembly != null); | ||
TypeReference voidRef = Assembly.MainModule.ImportReference(callee.ReturnType); | ||
const MethodAttributes attributes = MethodAttributes.Static | ||
| MethodAttributes.SpecialName | ||
| MethodAttributes.RTSpecialName; | ||
MethodDefinition cctor = new(".cctor", attributes, voidRef); | ||
ILProcessor il = cctor.Body.GetILProcessor(); | ||
il.Append(il.Create(OpCodes.Call, callee)); | ||
il.Append(il.Create(OpCodes.Ret)); | ||
|
||
TypeDefinition? moduleClass = Find(Assembly.MainModule.Types, t => t.Name == "<Module>"); | ||
if(moduleClass == null) | ||
{ | ||
throw ExceptionFactory.ModuleNotFound(); | ||
} | ||
|
||
// Always insert first so that we appear before [ModuleInitalizerAttribute] | ||
moduleClass.Methods | ||
.Insert(0, cctor); | ||
|
||
Debug.Assert(moduleClass != null, "Found no module class!"); | ||
} | ||
|
||
private void WriteAssembly(string assemblyFile, string keyfile) | ||
{ | ||
Debug.Assert(Assembly != null); | ||
var writeParams = new WriterParameters(); | ||
if (PdbFile(assemblyFile) != null) | ||
{ | ||
writeParams.WriteSymbols = true; | ||
writeParams.SymbolWriterProvider = new PdbWriterProvider(); | ||
} | ||
Assembly.Write(assemblyFile, writeParams); | ||
} | ||
|
||
private void ReadAssembly(string assemblyFile) | ||
{ | ||
Debug.Assert(Assembly == null); | ||
|
||
var resolver = new DefaultAssemblyResolver(); | ||
resolver.AddSearchDirectory(Path.GetDirectoryName(assemblyFile)); | ||
|
||
var readParams = new ReaderParameters(ReadingMode.Immediate) | ||
{ | ||
AssemblyResolver = resolver, | ||
InMemory = true | ||
}; | ||
|
||
if (PdbFile(assemblyFile) != null) | ||
{ | ||
readParams.ReadSymbols = true; | ||
readParams.SymbolReaderProvider = new PdbReaderProvider(); | ||
} | ||
Assembly = AssemblyDefinition.ReadAssembly(assemblyFile, readParams); | ||
} | ||
|
||
private MethodReference GetCalleeMethod(string typeName, string methodName) | ||
{ | ||
Debug.Assert(Assembly != null); | ||
ModuleDefinition module = Assembly.MainModule; | ||
TypeDefinition? moduleInitializerClass; | ||
|
||
moduleInitializerClass = Find(module.Types, t => t.FullName == typeName); | ||
if (moduleInitializerClass == null) | ||
{ | ||
throw ExceptionFactory.TypeNameDoesNotExist(typeName); | ||
} | ||
|
||
MethodDefinition? callee = Find(moduleInitializerClass.Methods, m => m.Name == methodName); | ||
if (callee == null) | ||
{ | ||
throw ExceptionFactory.MethodNameDoesNotExist(moduleInitializerClass.FullName, methodName); | ||
} | ||
if (callee.Parameters.Count > 0) | ||
{ | ||
throw ExceptionFactory.ModuleInitializerMayNotHaveParameters(); | ||
} | ||
if (callee.IsPrivate || callee.IsFamily) | ||
{ | ||
throw ExceptionFactory.ModuleInitializerMayNotBePrivate(); | ||
} | ||
if (!callee.ReturnType.FullName.Equals(typeof(void).FullName)) //Don't compare the types themselves, they might be from different CLR versions. | ||
{ | ||
throw ExceptionFactory.ModuleInitializerMustBeVoid(); | ||
} | ||
return !callee.IsStatic ? throw ExceptionFactory.ModuleInitializerMustBeStatic() : callee; | ||
} | ||
|
||
//No LINQ, since we want to target 2.0 | ||
private static T? Find<T>(Collection<T> objects, Predicate<T> condition) where T : class | ||
{ | ||
foreach (T obj in objects) | ||
{ | ||
if (condition(obj)) | ||
{ | ||
return obj; | ||
} | ||
} | ||
return null; | ||
} | ||
|
||
public void Dispose() | ||
{ | ||
Assembly.Dispose(); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
using System.CommandLine; | ||
using System.CommandLine.Builder; | ||
using System.CommandLine.Parsing; | ||
using System.Runtime.CompilerServices; | ||
|
||
namespace SourceGenerator.Foundations.Injector | ||
{ | ||
internal class InjectCommand : RootCommand | ||
{ | ||
|
||
public InjectCommand() | ||
{ | ||
Description = "Takes an assembly and injects a series of methods into the module initalizer that will be called on startup"; | ||
|
||
Argument<string> className = new("className", "The full type name of the class that should be added to the moduel initializer"); | ||
Argument<string> methodName = new("methodName", "The name of the static method that should be added to the module initializer"); | ||
Argument<FileInfo> targetAssembly = new Argument<FileInfo>("targetAssembly", "The path to the assembly file that should be injected"); | ||
targetAssembly.ExistingOnly(); | ||
|
||
AddArgument(className); | ||
AddArgument(methodName); | ||
AddArgument(targetAssembly); | ||
|
||
this.SetHandler(InvokeAsync, className, methodName, targetAssembly); | ||
} | ||
|
||
private Task<int> InvokeAsync(string className, string methodName, FileInfo targetAssembly) | ||
{ | ||
Injector injector = new Injector(); | ||
injector.Inject(targetAssembly.FullName, className, methodName); | ||
return Task.FromResult(0); | ||
} | ||
} | ||
|
||
internal class Program | ||
{ | ||
static async Task<int> Main(string[] args) | ||
{ | ||
return await new CommandLineBuilder(new InjectCommand()) | ||
.UseHelp() | ||
.Build() | ||
.InvokeAsync(args); | ||
} | ||
} | ||
} |
8 changes: 8 additions & 0 deletions
8
src/SourceGenerator.Foundations.Injector/Properties/launchSettings.json
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
{ | ||
"profiles": { | ||
"SourceGenerator.Foundations.Injector": { | ||
"commandName": "Project", | ||
"commandLineArgs": "SGF.Reflection.AssemblyResolver Initialize D:\\Repositories\\SourceGenerator.Foudations\\src\\Sandbox\\ConsoleApp.SourceGenerator\\bin\\Debug\\netstandard2.0\\ConsoleApp.SourceGenerator.dll" | ||
} | ||
} | ||
} |
15 changes: 15 additions & 0 deletions
15
src/SourceGenerator.Foundations.Injector/SourceGenerator.Foundations.Injector.csproj
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
<Project Sdk="Microsoft.NET.Sdk"> | ||
|
||
<PropertyGroup> | ||
<OutputType>Exe</OutputType> | ||
<TargetFramework>net6.0</TargetFramework> | ||
<ImplicitUsings>enable</ImplicitUsings> | ||
<Nullable>enable</Nullable> | ||
</PropertyGroup> | ||
|
||
<ItemGroup> | ||
<PackageReference Include="Mono.Cecil" Version="0.11.5" /> | ||
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" /> | ||
</ItemGroup> | ||
|
||
</Project> |
12 changes: 12 additions & 0 deletions
12
src/SourceGenerator.Foundations.Injector/SourceGenerator.Foundations.Injector.targets
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Linq; | ||
using System.Text; | ||
using System.Threading.Tasks; | ||
|
||
namespace SourceGenerator.Foundations.Injector | ||
{ | ||
class SourceGenerator | ||
{ | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
20 changes: 10 additions & 10 deletions
20
src/SourceGenerator.Foundations.Shared/Runtime/ModuleInitializerAttribute.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,11 +1,11 @@ | ||
using System; | ||
//using System; | ||
|
||
namespace System.Runtime.CompilerServices | ||
{ | ||
/// <summary> | ||
/// Allow for code to hook into the module being initalized and solve | ||
/// the problem for bootstrapping without user intervention | ||
/// </summary> | ||
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] | ||
internal sealed class ModuleInitializerAttribute : Attribute { } | ||
} | ||
//namespace System.Runtime.CompilerServices | ||
//{ | ||
// /// <summary> | ||
// /// Allow for code to hook into the module being initalized and solve | ||
// /// the problem for bootstrapping without user intervention | ||
// /// </summary> | ||
// [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] | ||
// internal sealed class ModuleInitializerAttribute : Attribute { } | ||
//} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.