Object Test Bench – III: Exploring Framework APIs...

Hi Folks…

How often have you surfed the web, wanting to learn about a new whiz-bang .NET Framework API that you just heard of? Often right? I for one have found myself do this very frequently…

Surfing the web usually comprises of running a web search on the whiz-bang keywords that got you in the mood in the first place. With a bit of luck, the search engine returns a host of sites that contain documentations/tutorials/samples/snippets on the API you are looking for. There are sites like The Code Project, DevX.com, DevHood that are doing a great job in introducing the API to audiences at all levels. Then there also sites like the one you are currently on – the blogs. After reading the documentation/comments on these sites you aren’t really done till you actually try out the code snippets/samples – right?

Here is when OTB can help you out! You no longer need to copy the sample, build it and then debug it before you can get a grip on what’s the API is really about. With as little work as starting the IDE and creating a Zero Impact project, OTB will help you understand the API visually. This is what I am going to show you today. And note that, the API need not really be an .NET Framework API. It could be any 3rd party API written for the .NET platform.

Searching on the web for Reflection.Emit…

One of the killer features that .NET Framework offers the compiler writers is the Reflection.Emit namespace. Now I want to learn about that. So I run a web search with “Reflection.Emit dynamically generated code”. And the first hit on one of the popular search engines is an excellent blog by my fellow Microsoftie, Mike Stall. That’s a good hit – apart from demonstrating how to generate code dynamically, Mike also demonstrates how to debug the dynamic code!!! Ain’t that really totally cool!!!

So what do we do next?
1. Open launch Visual Studio 2005
2. Create a Zero Impact project (I am creating a C# console application – you could create either that or any of the C#/J#/VB projects).
3. Save this text file on your hard disk (say C:\temp\souces.txt)
4. Open C:\temp\source.txt in VS and set a break point in the second line (i.e. the line with “xyz = "hello";”)
5. Turn of Tools::Options::Debugging::General::Enable Just My Code (Managed only)

/*01*/ // Sample of emitting debugging information for dynamic modules
/*02*/ // Reflection emit example adopted from http://blogs.msdn.com/joelpob/archive/2004/01/21/61411.aspx
/*03*/ using System;
/*04*/ using System.Reflection;
/*05*/ using System.Reflection.Emit;
/*06*/ using System.Threading;
/*07*/ using System.Diagnostics.SymbolStore;
/*08*/ 
/*09*/ public class EmitHelloWorld
/*10*/ {
/*11*/ static void Main(string[] args)
/*12*/ {
/*13*/ // create a dynamic assembly and module
/*14*/ AssemblyName assemblyName = new AssemblyName("HelloWorld");
/*15*/ 
/*16*/ AppDomain appDomain = Thread.GetDomain();
/*17*/ AssemblyBuilder assemblyBuilder = appDomain.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.RunAndSave);
/*18*/ 
/*19*/ ModuleBuilder module = assemblyBuilder.DefineDynamicModule("HelloWorld.exe", true); // <-- pass 'true' to track debug info.
/*20*/ 
/*21*/ // Tell Emit about the source file that we want to associate this with.
/*22*/ Guid emptyGuid;
/*23*/ emptyGuid = Guid.Empty;
/*24*/ ISymbolDocumentWriter doc = module.DefineDocument("Source.txt", emptyGuid, emptyGuid, emptyGuid);
/*25*/ 
/*26*/ // create a new type to hold our Main method
/*27*/ TypeAttributes typeAttributes;
/*28*/ typeAttributes = TypeAttributes.Public | TypeAttributes.Class;
/*29*/ TypeBuilder typeBuilder = module.DefineType("HelloWorldType", typeAttributes);
/*30*/ 
/*31*/ // create the Main(string[] args) method
/*32*/ MethodAttributes methodAttributes;
/*33*/ methodAttributes = MethodAttributes.HideBySig | MethodAttributes.Static | MethodAttributes.Public;
/*34*/ Type voidType = Type.GetType("System.Void");
/*35*/ Type[] parameterTypes;
/*36*/ parameterTypes = new Type[] { typeof(string[]) };
/*37*/ MethodBuilder methodbuilder = typeBuilder.DefineMethod("Main", methodAttributes, voidType, parameterTypes);
/*38*/ 
/*39*/ // generate the IL for the Main method
/*40*/ ILGenerator ilGenerator = methodbuilder.GetILGenerator();
/*41*/ OpCode genericOpCode;
/*42*/ 
/*43*/ // Create a local variable of type string, and call it xyz
/*44*/ Type stringType = Type.GetType("System.String");
/*45*/ LocalBuilder localXYZ = ilGenerator.DeclareLocal(stringType);
/*46*/ localXYZ.SetLocalSymInfo("xyz"); // Provide name for the debugger.
/*47*/ 
/*48*/ // Emit sequence point before the IL instructions. This is start line, start col, end line, end column,
/*49*/ 
/*50*/ // Line 2: xyz = "hello";
/*51*/ ilGenerator.MarkSequencePoint(doc, 2, 1, 2, 100);
/*52*/ genericOpCode = OpCodes.Ldstr;
/*53*/ ilGenerator.Emit(genericOpCode, "Hello world!");
/*54*/ genericOpCode = OpCodes.Stloc;
/*55*/ ilGenerator.Emit(genericOpCode, localXYZ);
/*56*/ 
/*57*/ // Line 3: Write(xyz);
/*58*/ Type sysConsoleType = Type.GetType("System.Console");
/*59*/ parameterTypes[0] = typeof(string);
/*60*/ MethodInfo infoWriteLine = sysConsoleType.GetMethod("WriteLine", parameterTypes);
/*61*/ ilGenerator.MarkSequencePoint(doc, 3, 1, 3, 100);
/*62*/ genericOpCode = OpCodes.Ldloc;
/*63*/ ilGenerator.Emit(genericOpCode, localXYZ);
/*64*/ genericOpCode = OpCodes.Call;
/*65*/ ilGenerator.EmitCall(genericOpCode, infoWriteLine, null);
/*66*/ 
/*67*/ // Line 4: return;
/*68*/ ilGenerator.MarkSequencePoint(doc, 4, 1, 4, 100);
/*69*/ genericOpCode = OpCodes.Ret;
/*70*/ ilGenerator.Emit(genericOpCode);
/*71*/ 
/*72*/ // bake it
/*73*/ Type helloWorldType = typeBuilder.CreateType();
/*74*/ 
/*75*/ // run it
/*76*/ MethodInfo mainMethod = helloWorldType.GetMethod("Main");
/*77*/ object[] parameters;
/*78*/ parameters = new string[] { null };
/*79*/ mainMethod.Invoke(null, parameters);
/*80*/ 
/*81*/ // set the entry point for the application and save it
/*82*/ assemblyBuilder.SetEntryPoint(methodbuilder, PEFileKinds.ConsoleApplication);
/*83*/ assemblyBuilder.Save("HelloWorld.exe");
/*84*/ }
/*85*/ }

For ready reference above is the source code from Mike’s blog, that we are going to study visually. Here we go!

Doing it… Visually!

This section assumes that you know your way around Object Test Bench. If not, please go through my previous blog and/or keep it open for reference. 

(Line 14) In the CV, navigate to System.Reflection.AssemblyName. Right click and from the CI menu, select the constructor with a string parameter. The CI dialog will appear. Enter assemblyName as the instance name and "HelloWorld" as parameter and press OK. You will then have the assemblyName object on the test bench. Go ahead and explore it using the enhanced datatips.

(Line 16) In the CV, invoke the GetDomain static method on the System.Threading.Thread class. In the MCR dialog that comes up, save the return value on the test bench as appDomain. In the test bench, right click on appDomain and invoke the DefineDynamicAssembly method on it with assemblyName and System.Reflection.Emit.AssemblyBuilderAccess.RunAndSave as parameters. Using the MCR, save the return value as assemblyBuilder.

(Line 19) Invoke the DefineDynamicModule method on the assemblyBuilder object with "HelloWorld.exe" and true as parameters. Save the return value as module.

(Line 22) Create the emptyGuid object by executing “Guid emptyGuid;” in IW. Note that emptyGuid is already set to System.Guid.Empty. Invoke the DefineDocument method on the module object with "Source.txt" as the first parameter. For the remaining 3 parameters select emptyGuid from the parameter dropdown list. Save the return value as doc.

(Line 27) Create the typeAttributes object by executing “System.Reflection.TypeAttributes typeAttributes;” in the IW. In the test bench, hover over the typeAttributes object to get the enhanced datatips. Using the enhanced datatips and set the value of typeAttributes to “System.Reflection.TypeAttributes.Public | System.Reflection.TypeAttributes.Class”. Invoke the DefineType method on the module object with "HelloWorldType" and typeAttributes as parameters. Save the return value as typeBuilder.

(Line 32) Create the methodAttributes object by executing “System.Reflection.MethodAttributes methodAttributes;” in the IW. Using the enhanced datatips, set methodAttributes to “System.Reflection.MethodAttributes.HideBySig | System.Reflection.MethodAttributes.Static | System.Reflection.MethodAttributes.Public”. Invoke the GetType static method on the System.Type class with "System.Void" as parameter. Save the return value as voidType. Create the parameterTypes object by executing “Type[] parameterTypes;” in the IW. Using the enhanced datatips, set parameterTypes to “new Type[] { typeof(string[]) }”. Invoke the DefineMethod on typeBuilder with "Main", methodAttributes, voidType, parameterTypes as parameters. Save the return value as methodbuilder.

(Line 40) Invoke the GetILGenerator method on the methodbuilder object. Save the return value as ilGenerator. Create the genericOpCode object by executing “System.Reflection.Emit.OpCode genericOpCode;” in the IW.

(Line 44) Invoke the GetType static method on the System.Type class with "System.String" as parameter. Save the return value as stringType. Invoke the DeclareLocal method on the ilGenerator object with stringType as parameter. Save the return value as localXYZ. Invoke the SetLocalSymInfo method on the localXYZ object with "xyz" as parameter.

(Line 51) Invoke the MarkSequencePoint method on the ilGenerator object with doc, 2, 1, 2, 100 as parameters. Using the enhanced datatips, set genericOpCode to System.Reflection.Emit.OpCodes.Ldstr (Important: Please see the notes section below!). Invoke the Emit method on the ilGenerator object with genericOpCode and "Hello world!" as parameters. Using the enhanced datatips, set genericOpCode to System.Reflection.Emit.OpCodes.Stloc. Invoke the Emit method on the ilGenerator object with genericOpCode and localXYZ as parameters.

(Line 58) Invoke the GetType static method on the System.Type class with "System.Console" as parameter. Save the return value as sysConsoleType. Using the enhanced datatips, parameterTypes[0] to typeof(string). Invoke the GetMethod method on the sysConsoleType object with "WriteLine", parameterTypes as parameters. Save the return value as infoWriteLine. Invoke the MarkSequencePoint method on the ilGenerator object with doc, 3, 1, 3, 100 as parameters. Using the enhanced datatips, set genericOpCode to System.Reflection.Emit.OpCodes.Ldloc. Invoke the Emit method on the ilGenerator object with genericOpCode and localXYZ as parameters. Using the enhanced datatips, set genericOpCode to System.Reflection.Emit.OpCodes.Call. Invoke the EmitCall method on the ilGenerator object with genericOpCode, infoWriteLine, null as parameters.

(Line 68) Invoke the MarkSequencePoint method on the ilGenerator object with doc, 4, 1, 4, 100 as parameters. Using the enhanced datatips, set genericOpCode to System.Reflection.Emit.OpCodes.Ret. Invoke the Emit method on the ilGenerator object with genericOpCode as parameter.

(Line 73) Invoke the CreateType method on the typeBuilder object. Save the return value as helloWorldType.

(Line 76) Invoke the GetMethod method on the helloWorldType object with "Main" as parameter. Save the return value as mainMethod. Create the parameters object by executing “object[] parameters;” in the IW. Using the enhanced datatips, set parameters to “new string[] { null }”. Invoke the Invoke method on the mainMethod object with null, parameters as arguments.

Yooooohooooo!!! #1: At this point the break point in source.txt that you had set earlier should be hit. (Here is how it looks on my machine) On you step out of the Sources.txt file, you will get the MCR dialog informing you that the call was successful. (Here is how my IDE looks at this point)

(Line 82) Invoke the SetEntryPoint method on the assemblyBuilder object with methodbuilder, System.Reflection.Emit.PEFileKinds.ConsoleApplication as parameters. Invoke the Save method on the assemblyBuilder object with "HelloWorld.exe" as parameter.

Yooooohooooo!!! #2: If you go and examine the project output directory, you can see HelloWorld.exe and HelloWorld.pdb sitting. What’s more, you can open HelloWorld.exe as a project and on pressing F11, the control will step into Sources.txt.

So what have we learnt!

A lot about Reflection.Emit? Yes of course! But learning Reflection.Emit isn’t the point I am trying to make with this blog. In this blog I have outlined a generic procedure that you employ to visually study and explore any 3rd party API for the .NET platform. Just like the Class Designer helps you get a static view of the API you are studying using, OTB helps you get the dynamic view of that API.

Notes:

Because of a bug in Visual Studio 2005 Beta 2, it will not be possible to set genericOpCode directly to System.Reflection.Emit.OpCodes.Ldstr using the enhanced datatips. As a work around you can set genericOpCode to ( System.Reflection.Emit.OpCode )Type.GetType( "System.Reflection.Emit.OpCodes" ).GetField( "Ldstr" ).GetValue( null ) using the enhanced datatips. This bug has been fixed in the post Beta 2 builds. So you can use the direct method in Visual Studio 2005 RTM.

(To be concluded...)