msgbartop
A site for my programming pet projects
msgbarbottom

16 Dec 09 Module initializers in C#

One feature of the CLR that is not available in C# or VB.NET are module initializers (or module constructors). A module initializer is simply a global function which is named .cctor and marked with the attributes SpecialName and RTSpecialName. It is run when a module (each .NET assembly is comprised of one or more modules, typically just one) is loaded for the first time, and is guaranteed to run before any other code in the module runs, before any type initializers, static constructors or any other initialization code. I wanted to use this feature for a project I was doing but was unable to use it directly in C# so I created my own solution.

Now, why did I need this? Well, I was loading my other assemblies from an unusual place and I wanted to subscribe to the AppDomain.AssemblyResolve event before any types in my assembly were initialized. This feature is clearly not something you’re going to need every day, (actually I can’t think of a single other use case where I’d need it) but if you do, here’s how I solved it.

I used the excellent Mono.Cecil library to create a small program that injects a module initializer into an existing assembly. Cecil is a fantastic library that makes it incredibly easy to manipulate assemblies and can do pretty much anything that you could possibly want to do with IL code. Using it I wrote a small program that simply takes in an assembly filename and the name of a parameterless static void method that exists in the assembly, and injects a module initializer that does nothing but call that method. The original code I wrote, the interesting part without all the error checking and stuff, is shown below:

string assemblyName = "Test.dll";
string typeName = "Foo.Bar.ModuleInit";
string methodName = "Run";

AssemblyDefinition assembly = AssemblyFactory.GetAssembly (assemblyName);
TypeReference voidRef = assembly.MainModule.Import(typeof(void));
var attributes = MethodAttributes.Static
                | MethodAttributes.SpecialName
                | MethodAttributes.RTSpecialName;
var cctor = new MethodDefinition( ".cctor", attributes, voidRef);

TypeDefinition type = assembly.MainModule.Types[typeName];
MethodReference methodRef = type.Methods.GetMethod(methodName,new Type[]{});
cctor.Body.CilWorker.Append(cctor.Body.CilWorker.Create(OpCodes.Call, methodRef));
cctor.Body.CilWorker.Append(cctor.Body.CilWorker.Create(OpCodes.Ret));
assembly.MainModule.Inject(cctor, assembly.MainModule.Types["<Module>"]);
AssemblyFactory.SaveAssembly(assembly, assemblyName);

I then added all the neccessary error handling, unit testing etc. and the resulting program can be downloaded here. The program is just a single executable since I merged Mono.Cecil into it using the excellent ILMerge tool. You can run it from the command line and give it the filename of your assembly and optionally specify the method to call. If no method is explicitly specified then the program looks for a type named ModuleInitializer (may be in any namespace) and looks for a method named Run in that type and calls that method in the module initializer.

Run without specifying the method, looks for ModuleInitializer::Run in any namespace.

InjectModuleInitializer.exe Test.dll

Run and specify the method as Foo.Bar.SomeClass::SomeMethod

InjectModuleInitializer.exe /m:Foo.Bar.SomeClass::SomeMethod Test.dll

Realistically though, if you want to do this you probably want to do it right after building your assembly.  That’s why the assembly also includes an MSBuild task, so that it can be used directly in a .csproj file. The best target to use for this is the AfterBuild target. To use this task in your project you need to edit your .csproj file and add two things:

  1. At the top, inside the <Project> element include the following:
    <UsingTask TaskName="InjectModuleInitializer"
               AssemblyFile="c:\some\folder\InjectModuleInitializer.exe" />
    

    If the InjectModuleInitializer.exe assembly is registered in the global assembly cache you could also write that as

    <UsingTask TaskName="InjectModuleInitializer"
               AssemblyName="InjectModuleInitializer, Version=1.0.0.0, Culture=neutral, PublicKeyToken=818425a64317679d" />
  2. Add the AfterBuild target at the bottom of the file, or anywhere where <target> elements are allowed.
    <Target Name="AfterBuild">
        <InjectModuleInitializer AssemblyFile="$(TargetPath)" />
    </Target>
    

    Or, if you want to explicitly specify the method to run:

    <Target Name="AfterBuild">
        <InjectModuleInitializer
            AssemblyFile="$(TargetPath)"
            ModuleInitializer="SimpleTest.Program::Con" />
    </Target>
    
  3. And there you have it. You can download the console program/MSBuild task or download the source code and build it yourself. Enjoy.

Leave a Comment