- The Imminent Potential of Microsoft Intermediate Language (MSIL)
- Brief Overview
- Emitting Dynamic Assemblies
- Creating a "Hello World" Emitter
- Summary
Creating a "Hello World" Emitter
To keep from writing a book here, I've kept the example program short. This program emits an assembly that's the equivalent of a "Hello, World" program. It demonstrates the basics of writing an emitter and is practicable in the space we have remaining.
Let's jump right into the code. Listing 1 demonstrates a single instance of the code we want to approximate in MSIL. Listing 2 demonstrates the emitter that will generate the MSIL that in turn will create a single assembly named Hello.exe that will finally write to the console whatever text we create the emitter with.
Listing 1A Simple Hello, World! Application
using System; namespace Hello { class Class1 { [STAThread] static void Main(string[] args) { Console.WriteLine("Hello, World"); } } }
Listing 1 demonstrates a basic sample console application that can probably be found in every .NET book available. The most interesting thing about this sample program is that it demonstrates how easy it is to create console applications, which are still useful for utility applications.
Listing 2 demonstrates the C# code necessary to dynamically generate an application identical to the assembly that will be created from Listing 1. The first thing you'll notice is that the emitter is an order of magnitude longer. (That is, it's at least ten times longer than the C# version it emulates.)
Listing 2An Emitter That Generates a Vanilla Hello.exe Application
1: using System; 2: using System.Reflection; 3: using System.Reflection.Emit; 4: 5: namespace HelloWorldEmitter 6: { 7: class EmitterDemo 8: { 9: [STAThread] 10: static void Main(string[] args) 11: { 12: Usage(args); 13: System.Diagnostics.Debug.Assert(args.Length == 1); 14: Emitter.Run(args[0]); 15: 16: } 17: 18: private static void Usage(string[] args) 19: { 20: if( args.Length == 1 ) return; 21: const string usage = 22: "HelloWorld <text echo to the console>\r\n" + 23: "(e.g. HelloWorld \"Hello World!\"\r\n" + 24: "will emit a dynamic assembly that displays\r\n" + 25: "the parameter text.)\r\n\r\n" + 26: "Copyright \xa9 2002. All Rights Reserved.\r\n" + 27: "by Paul Kimmel. pkimmel@softconcepts.com\r\n"; 28: 29: Console.WriteLine(usage); 30: Console.ReadLine(); 31: 32: } 33: } 34: 35: public class Emitter 36: { 37: public static void Run(string toEmit) 38: { 39: Emitter emitter = new Emitter(toEmit); 40: emitter.Emit(); 41: } 42: 43: private string toEmit; 44: private AssemblyName assemblyName; 45: private AssemblyBuilder assemblyBuilder; 46: private ModuleBuilder moduleBuilder; 47: private TypeBuilder typeBuilder; 48: private MethodBuilder methodBuilder; 49: 50: protected Emitter(string toEmit) 51: { 52: this.toEmit = toEmit; 53: } 54: 55: public void Emit() 56: { 57: const string name = "Hello.exe"; 58: assemblyName = new AssemblyName(); 59: assemblyName.Name = "Hello"; 60: 61: // Define dynamic assembly 62: assemblyBuilder = CreateAssemblyBuilder(assemblyName, 63: AssemblyBuilderAccess.Save); 64: 65: // Define dynamic module 66: moduleBuilder = CreateModuleBuilder(assemblyBuilder); 67: 68: // Define dynamic type 69: typeBuilder = CreateTypeBuilder(moduleBuilder); 70: 71: // Define dynamic method 72: methodBuilder = CreateMethodBuilder(typeBuilder); 73: 74: // Apply attributes 75: methodBuilder.SetCustomAttribute( CreateAttributeBuilder()); 76: 77: // Establish entry point Main 78: assemblyBuilder.SetEntryPoint(methodBuilder); 79: 80: // Write the lines of code 81: EmitCode(methodBuilder, toEmit); 82: 83: // Create the type 84: typeBuilder.CreateType(); 85: 86: // Save the dynamic assembly 87: assemblyBuilder.Save(name); 88: } 89: 90: private AssemblyBuilder CreateAssemblyBuilder( 91: AssemblyName aName, AssemblyBuilderAccess access) 92: { 93: return AppDomain.CurrentDomain 94: .DefineDynamicAssembly(aName, access); 95: } 96: 97: private ModuleBuilder CreateModuleBuilder( 98: AssemblyBuilder builder) 99: { 100: return assemblyBuilder.DefineDynamicModule("Class1.mod", 101: "Hello.exe", false); 102: } 103: 104: private TypeBuilder CreateTypeBuilder(ModuleBuilder builder) 105: { 106: return builder.DefineType("Class1"); 107: } 108: 109: private MethodBuilder CreateMethodBuilder(TypeBuilder builder) 110: { 111: return builder.DefineMethod("Main", 112: MethodAttributes.Private | MethodAttributes.Static 113: | MethodAttributes.HideBySig, 114: CallingConventions.Standard, typeof(void), 115: new Type[]{typeof(string[])}); 116: } 117: 118: private CustomAttributeBuilder CreateAttributeBuilder() 119: { 120: return new CustomAttributeBuilder( 121: typeof(System.STAThreadAttribute).GetConstructor(new Type[]{}), 122: new object[]{}); 123: } 124: 125: private void EmitCode(MethodBuilder builder, string text) 126: { 127: ILGenerator generator = builder.GetILGenerator(); 128: generator.Emit(OpCodes.Ldstr, text); 129: 130: MethodInfo methodInfo = typeof(System.Console).GetMethod( 131: "WriteLine", 132: BindingFlags.Public | BindingFlags.Static, null, 133: new Type[]{typeof(string)}, null); 134: 135: generator.Emit(OpCodes.Call, methodInfo); 136: generator.Emit(OpCodes.Ret); 137: } 138: } 139: }
Lines 7 through 33 implement the EmitterDemo class, which is the entry point for this sample application. The EmitterDemo class makes sure that one string is passed to the console application and invokes Emitter.Run method.
The rest of the code implements the Emitter class, which generates the dynamic Hello.exe assembly. Emitter.Run creates an instance of the Emitter class and invokes the Emit method. The Emit method was written very deliberately. If you follow the Emit method from the first statement (line 55) to the last (line 88), you can get an idea of the macro steps for emitting a dynamic assembly. (The steps in the previous section describe the macro, beginning with creating an AssemblyBuilder and ending after you've emitted that last member, demonstrated on lines 62 and 81, respectively.) Finally, we save the assembly on line 87, which writes the assembly to disk.
If you want to run the assembly in memory, you can define the dynamic assembly with AssemblyBuilderAccess.Run. You don't have to write an assembly to disk to use it. You create an instance of the type in memory, as demonstrated on line 84, and use the type. (If you use an emitted type in memory, you'll have to use reflection to invoke operations on the type. Writing the type to file will allow you to use Intellisense and strongly typed elements of the assembly.)
Finally, there's a simple ploy you can use to learn to write MSIL. It's the same trick you can use to learn to write C#: Read and write a lot of it. If you want to write a specific emitter, write a solution in C# and use the ildasm.exe utility to see how .NET emitted the IL.