Skip to content

Commit

Permalink
I now use a Cecil to inject the module initializer
Browse files Browse the repository at this point in the history
This allows me to have explicit ordering of the loading of modules and not cause conflicts
  • Loading branch information
ByronMayne committed Nov 17, 2023
1 parent 0ace226 commit bd9c283
Show file tree
Hide file tree
Showing 13 changed files with 328 additions and 17 deletions.
55 changes: 55 additions & 0 deletions src/SourceGenerator.Foundations.Injector/ErrorFactory.cs
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");
}
}
}
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) { }
}
}
150 changes: 150 additions & 0 deletions src/SourceGenerator.Foundations.Injector/Injector.cs
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

View workflow job for this annotation

GitHub Actions / build

Non-nullable property 'Assembly' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.

Check warning on line 12 in src/SourceGenerator.Foundations.Injector/Injector.cs

View workflow job for this annotation

GitHub Actions / build

Non-nullable property 'Assembly' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.

Check warning on line 12 in src/SourceGenerator.Foundations.Injector/Injector.cs

View workflow job for this annotation

GitHub Actions / build, bundle & publish

Non-nullable property 'Assembly' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.

Check warning on line 12 in src/SourceGenerator.Foundations.Injector/Injector.cs

View workflow job for this annotation

GitHub Actions / build, bundle & publish

Non-nullable property 'Assembly' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.

Check warning on line 12 in src/SourceGenerator.Foundations.Injector/Injector.cs

View workflow job for this annotation

GitHub Actions / build, bundle & publish

Non-nullable property 'Assembly' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.

Check warning on line 12 in src/SourceGenerator.Foundations.Injector/Injector.cs

View workflow job for this annotation

GitHub Actions / build, bundle & publish

Non-nullable property 'Assembly' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.

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();
}
}
}
45 changes: 45 additions & 0 deletions src/SourceGenerator.Foundations.Injector/Program.cs
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);
}
}
}
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"
}
}
}
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>
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
{
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ static AssemblyResolver()
s_loadedAssemblies = new Dictionary<AssemblyName, Assembly>(new AssemblyNameComparer());
}

[ModuleInitializer]
internal static void Initialize()
{
// The assembly resolvers get added to multiple source generators
Expand Down
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 { }
//}
10 changes: 10 additions & 0 deletions src/SourceGenerator.Foundations.sln
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConsoleApp", "Sandbox\Conso
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConsoleApp.SourceGenerator", "Sandbox\ConsoleApp.SourceGenerator\ConsoleApp.SourceGenerator.csproj", "{594AACB5-B550-46CF-B6E2-16EF826D655A}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SourceGenerator.Foundations.Injector", "SourceGenerator.Foundations.Injector\SourceGenerator.Foundations.Injector.csproj", "{E9B785CC-B19E-499F-915D-7E37F727C50C}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -73,6 +75,14 @@ Global
{594AACB5-B550-46CF-B6E2-16EF826D655A}.Release|Any CPU.Build.0 = Release|Any CPU
{594AACB5-B550-46CF-B6E2-16EF826D655A}.Release|x64.ActiveCfg = Release|x64
{594AACB5-B550-46CF-B6E2-16EF826D655A}.Release|x64.Build.0 = Release|x64
{E9B785CC-B19E-499F-915D-7E37F727C50C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E9B785CC-B19E-499F-915D-7E37F727C50C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E9B785CC-B19E-499F-915D-7E37F727C50C}.Debug|x64.ActiveCfg = Debug|Any CPU
{E9B785CC-B19E-499F-915D-7E37F727C50C}.Debug|x64.Build.0 = Debug|Any CPU
{E9B785CC-B19E-499F-915D-7E37F727C50C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E9B785CC-B19E-499F-915D-7E37F727C50C}.Release|Any CPU.Build.0 = Release|Any CPU
{E9B785CC-B19E-499F-915D-7E37F727C50C}.Release|x64.ActiveCfg = Release|Any CPU
{E9B785CC-B19E-499F-915D-7E37F727C50C}.Release|x64.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,13 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<None Include="..\..\README.md" Pack="True" PackagePath="/"/>
<None Include="..\..\README.md" Pack="True" PackagePath="/" />
<PackageReference Include="Serilog" Version="3.1.1" />
<None Update="SourceGenerator.Foundations.props" Pack="True" PackagePath="build/$(AssemblyName).props"/>
<None Update="SourceGenerator.Foundations.targets" Pack="True" PackagePath="build/$(AssemblyName).targets"/>
<ProjectReference Include="..\SourceGenerator.Foundations.Contracts\SourceGenerator.Foundations.Contracts.csproj" PrivateAssets="All"/>
<ProjectReference Include="..\Plugins\SourceGenerator.Foundations.Windows\SourceGenerator.Foundations.Windows.csproj" PrivateAssets="All"/>
<None Update="SourceGenerator.Foundations.props" Pack="True" PackagePath="build/$(AssemblyName).props" />
<None Update="SourceGenerator.Foundations.targets" Pack="True" PackagePath="build/$(AssemblyName).targets" />
<ProjectReference Include="..\SourceGenerator.Foundations.Contracts\SourceGenerator.Foundations.Contracts.csproj" PrivateAssets="All" />
<ProjectReference Include="..\Plugins\SourceGenerator.Foundations.Windows\SourceGenerator.Foundations.Windows.csproj" PrivateAssets="All" />
<ProjectReference Include="..\Plugins\SourceGenerator.Foundations.Injector\SourceGenerator.Foundations.Injector.csproj" PrivateAssets="All" ReferenceOutputAssembly="False" />
</ItemGroup>
<Target Name="AppendNugetContent">
<ItemGroup>
Expand All @@ -42,6 +43,9 @@
<TfmSpecificPackageFile Include="..\Plugins\SourceGenerator.Foundations.Windows\bin\$(Configuration)\netstandard2.0\SourceGenerator.Foundations.Windows.dll">
<PackagePath>lib/netstandard2.0/SourceGenerator.Foundations.Windows.dll</PackagePath>
</TfmSpecificPackageFile>
<TfmSpecificPackageFile Include="..\SourceGenerator.Foundations.Injector\bin\$(Configuration)\net6.0\*.*">
<PackagePath>sgf/injector/</PackagePath>
</TfmSpecificPackageFile>
</ItemGroup>
</Target>
<Import Project="..\SourceGenerator.Foundations.Shared\SourceGenerator.Foundations.Shared.projitems" Label="Shared" />
Expand Down
Loading

0 comments on commit bd9c283

Please sign in to comment.