Assemblies

Examine and/or load assemblies

The System .Reflection.Assembly class is important in much the same way as the Type class. Static Assembly methods load assemblies into the current application domain, and return an Assembly instance that describes the loaded assembly. An Assembly instance gives you access to the assembly's types and the assembly's resources. There are also static methods that reveal occasionally invaluable bits of information about the call stack, like GetExecutingAssembly, which returns the Assembly that contains the currently executing method, or GetCallingAssembly, which returns the Assembly that contains the method that called the current method. (As in the earlier "Type Values" section, AppDomain.CurrentDomain.GetAssemblies() will return an array of loaded assemblies.)

There are actually several different ways to dynamically load an assembly. Some are quite simple; others are not. I'll mention only the very simplest method—Assembly.LoadFile, which loads a library file into the current application domain—which probably covers 99% of the programs that need to dynamically load code. LoadFile is a static method that takes a filename and (assuming the file is a .NET assembly, and assuming that your code is running with sufficient permissions) loads the assembly, returning an Assembly instance.

Once you've loaded an assembly, you typically enumerate its public types, looking for classes that implement the right interface(s). (Less often, you may look for types with certain attributes.) Most applications that support plug-ins use an architecture like Figure 13-1, where both the application and (all) the plug-in(s) have a compile-time reference to the same Contract library. The Contract library defines both a services interface that the application implements, which contains all the services that the application offers its plug-ins, and a plug-in interface that the plug-ins implement, which contains all the handshaking necessary to load and host a plug-in in your application. (It's not uncommon for applications to support 'basic' and 'advanced' plug-ins, where, e.g., IAdvancedPlugin inherits from IPlugin, nor is it uncommon for a mature application to support several versions of the services or plug-in interfaces, but I'll talk about 'the' services interface and 'the' plug-in interface for simplicity.)17

Figure 13-1. The standard plug-in architecture

The point of putting the interfaces into a contract library is that this allows both the application and the plug-ins to refer to the same interface, at compile time. When your application uses Activator.CreateInstance to create an instance of a Type that your application got from a loaded plug-in, it saves the instance as that interface, in a location of that interface type. Your application can then make early-bound calls to the plug-in (just as if the interface were implemented within the application itself) and doesn't have to always Invoke a MethodInfo or SetValue aPropertyInfo.

So, when you load an Assembly, you want to find all types that support, say, IPlugin. As per the earlier "Type Details" section, typeof(IPlugin). IsAssignableFrom(ExportedType) is the best way to see if ExportedType supports IPlugin. You can get an assembly's public types with Assembly.GetExportedTypes, which returns an array with the Type of each of the public types in the assembly. Alternatively, if you have TypeInformation permission, you can get all of an assembly's types with Assembly.GetTypes, which returns an array18 with the Type of every toplevel types in the assembly, whether public or not. I hope it's obvious that you should only use Assembly.GetTypes when you need access to private or internal types, both because it's more expensive to construct and examine a larger array, and because you do need special permission to see and manipulate nonpublic types.

17. The services and plug-in interfaces are typically extremely asymmetrical, with the application offering much more in the way of services than it demands in the way of handshaking. Not only does only one assembly have to implement the services interface while many assemblies must implement the plugin interface, plug-ins are application extensions, while in many ways the services that the application provides are the application.

It can make a lot of sense to split a large services interface up into a set of properties, each of which returns a single-service interface. As with namespaces, this reduces naming conflicts, and makes code that uses the services interface easier to read and write. It also makes it easier to implement the services interface: it can be just a collection of private objects, each of which supports one single-service interface, and which are exposed to the outside world only via the interfaces they support.

18. Note that these methods both return fully populated arrays and not an enumeration-friendly collection like Regex.Matches—even if you abort a foreach on the first term, both GetExportedTypes and GetTypes have already allocated and populated their result array.

One final thing you can do with an Assembly instance is to use Assembly. GetManifestResourceStream to load resources (bitmaps, text files, and the like) that are stored in a compiled assembly as an Embedded Resource. Note that the resource type must be Embedded Resource—trying to load a resource that's not an Embedded Resource is a common cause of GetManifestResourceStream failure.

  • In Visual Studio, add an embedded resource by right-clicking a project in the Solution Explorer and selecting Add >■ Existing Item. After adding the item, right-click the new item, select Properties, and change the Build Action from Content to Embedded Resource.
  • In BDS, add an embedded resource by right-clicking a project in the Project Manager and selecting Add. It will automatically be added as an Embedded Resource.

C# stores manifest resources in the assembly using a name qualified with the assembly's default namespace—i.e., DefaultNamespace.Name—and you need to pass GetManifestResourceStream either a string containing the qualified name, or a Type defined in the default namespace, which GetManifestResourceStream can use to turn Name into DefaultNamespace.Name. That is, you call GetManifestResourceStream as either MyAssembly.GetManifestResourceStream("MyPlugin.PrettyPicture.png") or MyAssembly.GetManifestResourceStream(typeof(MyForm), "PrettyPicture.png").

Baking the default namespace into a resource name string is a risky thing to do: while the default namespace may not change very often, when it does change it may not be immediately obvious why, say, all the assembly's images are suddenly blank. Using the overload that takes a Type and uses Type. Namespace to get the default namespace is a bit safer, but not perfect either, as an assembly may contain several namespaces. The safest—if slowest—way to load resources is to use the Assembly.GetManifestResourceNames method, and use the name that EndsWith your resource's unqualified name, if there is one and only one. See the Chapter13\ ManifestResources C# project for an example.

Delphi stores a resource named MyPicture.bmp as MyPicture.bmp—no default namespace— and you can load the resource stream with code like

Assembly.GetExecutingAssembly.GetManifestResourceStreamCMyPicture.bmp')

This returns a stream that you can use to create, e.g., a Bitmap—see the Chapter13\ Delphi.ManifestResources project for an example.

Emit

.NET is script friendly

You can reflect on the code that is statically bound into your application, and you can reflect upon assemblies loaded dynamically, whether from disk or over a network. You can even emit CIL code at run time, and bake it into an assembly like any other. You can run code in this new assembly right away, and/or you can cache your new assembly for later.

The methods you create with System. Reflection. Emit are just as strongly typed as normal, compiled methods. They can create objects, call methods, and set fields and properties, just like normal, compiled methods.

You can easily pass typed data back and forth between compiled and emitted code.

I cover Emit here because you might want to construct code to test your understanding of how various bits of CIL work. Beyond that, you might need to build a very custom shim to a bit of legacy code. Or you may be interested in automatically generating scripts (tailored to your users' situations and preferences) that are compiled to CIL, which is jitted to native code not all that much different than the native code from your C# or Delphi code. Or you may be interested in implementing "latent typing," where you can do things like treat any object with a Text property as if it implements public interface IText {

Chapter13\Emit is a Delphi project group that contains two projects. The EmitBench project is a template for you to play around with CIL. The CompileTest project is a simple expression parser, which takes a string with a mathematical expression in X, and returns a pointer to a function that implements that expression.19 Both projects rely on the MidnightBeach.FnEmit unit that lets you create new assemblies containing a single, static method. You can use my code as is, or for a jumpstart to more functional code.

The first thing you need to do is create an AssemblyBuilder object, so you can Emit an assembly.

function Emitter.CreateAssemblyBuilder(const Name: string): AssemblyBuilder; var

DynamicAssembly: AssemblyName;

begin

DynamicAssembly := AssemblyName.Create; DynamicAssembly.Name := Name; DynamicAssembly.Version := Version.Create; Result := AppDomain.CurrentDomain.DefineDynamicAssembly( DynamicAssembly, AssemblyBuilderAccess.Run); end; // Emitter.CreateAssemblyBuilder

Before the CreateAssemblyBuilder method asks the current AppDomain to define a dynamic assembly, it creates and populates an AssemblyName object, which contains a Name string and Version information. Here, I just use default Version settings (version 0.0.0.0).

The AssemblyBuilderAccess parameter to the DefineDynamicAssembly method allows you to specify Run, Save, or RunAndSave behavior. If you create the assembly as Save or RunAndSave, you can use the Save method to save it to disk. A saved, emitted assembly can be used by other code just like a normal, compiled assembly can.

19. This code is interesting in four ways: 1) It's some of the first code I wrote for this book, having been originally written for the Delphi for .NET Preview compiler that came out before Delphi 8. 2) It includes what I hope is the last recursive descent parser I ever write by hand. 3) The tokenizer uses boxing (Chapter 2) in an interesting way. 4) And it emits CIL instructions that I don't mention in this chapter. If you're interested in this sort of thing, www.midnightbeach.com/ethiopia has some information about the more elaborate script compiler I built for the Midnight Beach content management system.

function Emitter.NewTypeBuilder: TypeBuilder; var

Assembly: AssemblyBuilder; Module: ModuleBuilder; begin

Assembly Module Result

= CreateAssemblyBuilder(CommonName); = Assembly.DefineDynamicModule(CommonName, True); = Module.DefineType(CommonName, TypeAttributes.Public);

end; // Emitter.NewTypeBuilder

The AssemblyBuilder class's DefineDynamicModule method creates a new instance of the ModuleBuilder class. The ModuleBuilder class's DefineType method creates a new TypeBuilder instance.

■Note In general, an assembly is a collection of modules, each of which must have a unique name. A module is simply a PE file—an EXE or a DLL. The simple code in this example generates a single module per assembly, and a single type (class) per module, and a single method per class—but you can have multiple modules per assembly, and multiple types per module, and multiple methods per class.

I use the preceding NewTypeBuilder method in the following code, which takes both a description of a function prototype (i.e., the parameter and result types) and a callback that emits the CIL, and returns a delegate that can be used to call the newly emitted code.

type

Generator = procedure (IL: ILGenerator);

GenericFn = function(A: array of TObject): TObject of object;

function Emitter.GenerateGenericFn(returnType: System.Type;

parameterTypes: array of System.Type; Generate: Generator): GenericFn;

NewType: TypeBuilder;

Method: MethodBuilder;

DynamicType: System.Type;

Info: MethodInfo;

begin

NewType := NewTypeBuilder;

Method := NewType.DefineMethod(CommonName,

MethodAttributes.Public or MethodAttributes.Static, returnType, parameterTypes );

Generate(Method.GetILGenerator);

DynamicType := NewType.CreateType();

Info := DynamicType.GetMethod(CommonName, parameterTypes);

Result := Invoker.Create(Info).generic; end; // Emitter.GenerateGenericFn

The TypeBuilder class's methods let you declare fields, properties, and methods, both public and private. DefineMethod takes a method name and signature, and creates a new MethodBuilder object that lets you generate CIL for the method.

The returnType parameter specifies the method's result type. For example, to define a function that returns an integer, pass typeof(integer) as the returnType parameter, as in MethodInfo lookup; to define a procedure that returns no result, pass typeof(void) as the returnType parameter. (If you create a void method that returns no result, the GenericFn wrapper will return Nil, just as when you Invoke the MemberInfo for a normal, precompiled void method.)

The parameterTypes parameter is an array of typeof(typeName) values that contains the types of each parameter in the new method (again, as in MethodInfo lookup). For example, [typeof(integer)] defines a method that takes an integer parameter, while [typeof(string), typeof(integer) ] defines a method that takes two parameters, a string and an integer. To define a method that takes no parameters, pass System.Type.EmptyTypes as the parameterTypes parameter.

Having created a MethodBuilder, GenerateGenericFn uses the Generate parameter to call back to a procedure that uses ILGenerator.Emit to define the body of the function. I'll walk through a simple Generate routine when I get to the end of the GenerateGenericFn method— for now, I'll just say that the only thing a CIL method has to contain is a ret instruction.

The TypeBuilder class's CreateType method 'bakes' the type definition and, if there are no errors, it returns a System.Type for the emitted type. GenerateGenericFn uses GetMethod to find the generated method, and gets a MethodInfo for the method.

While emitting code, you can emit a call to a MethodInfo. This turns into the same CIL as any other call—a 1-byte call opcode followed by a 4-byte method-table token—and jits to the same call through a method table as any other call. In normal, compiled code, you can Invoke() a MethodInfo: that is, you call code in dynamically generated assemblies in exactly the same way that you call code in dynamically loaded assemblies.

function Invoker.generic(A: array of TObject): TObject; begin

Assert(Assigned(Info));

As in the earlier "Type Details" section, the first parameter to Invoke is an instance reference, or Nil in the case of a static method. The second parameter to Invoke is an array of TObject values. Each value in the array will be checked to see that it is the same as the type declared in the method's prototype; pass an empty array (Nil) to Invoke a method that takes no formal parameters.

The GenerateGenericFn function returns a standard Delphi procedural type20 so that you can call the emitted function as if it were a normal, flat function. The Invoker class does go

20. Delphi procedural types are implemented as .NET delegates. Chapters 2 and 8 have more on delegates.

through MethodInfo.Invoke, and is slower than calls within compiled or emitted code. Invoke is the most general way to call dynamically generated dynamically loaded code, but there are a couple of significantly faster alternatives that you should use when you can:

  • If dynamic instance methods implement an interface that your application knows about at compile time, you can make early-bound calls through the interface, rather than using Invoke.
  • You can use Delegate.CreateDelegate to turn a MethodInfo that describes a method into a delegate you can call normally.21 The call to CreateDelegate checks that the parameters and result types are compatible with the MethodInfo, and returns a normal delegate that runs at normal speed. (The Emitter.GenerateRealFn method uses this technique.)

Finally, the EmitCIL routine in Chapter13\Emit\EmitBench.dpr generates a routine that does a little bit more than just return:22

procedure EmitCIL(IL: ILGenerator); var

SystemConsole: System.Type;

WriteLine: MethodInfo;

begin

SystemConsole := typeof(System.Console);

WriteLine := SystemConsole.GetMethod('WriteLine',

[ typeof(string) ]); // get the Console.WriteLine(string) overload

  1. Emit(OpCodes.Ldstr, 'This string is being printed by emitted code.');
  2. Emit(OpCodes.Call, WriteLine);
  3. Emit(OpCodes.Ret); end; // EmitCIL

This method does two different types of things. First, it reflects upon the Console class, and obtains an object describing the appropriate overload of the WriteLine method. Second, it uses this MethodInfo to emit code that will, when called, call Console.WriteLine() to write a string in a console window. When you emit code, you are continually switching mental gears from code generation to run-time operations on types and back.

In detail: I first get the Type of the Console class. The Type object gives me the MethodInfo for the Console.WriteLine(string) method. The ldstr instruction loads a string constant onto the stack; the IL. Emit call locates the string parameter in the dynamic assembly's string constant table

21. In 1.x, the Delegate.CreateDelegate overload that takes a MethodInfo can only create a delegate to a static method; in 2.0, it can also create delegates to instance methods. All versions of .NET support Delegate.CreateDelegate overloads that can create delegates to static or instance methods, given the Type of the class and the name of the method.

The Delegate.CreateDelegate method is best used with dynamically generated code; dynamically loaded code should use interfaces, as in the "Assemblies" section of this chapter. Creating an exported object and then casting it to an interface from a Contract library solves the versioning issues (and the possibilities of inadvertent conflict) that are caused by binding to dynamically loaded code by name. Dynamically generated code does not pose these problems.

22. What? You were expecting irony? The EmitCIL method emits two instructions besides ret.

(adding it, if necessary), and generates a string token for it. Finally, the IL.Emit(OpCodes.Call, WriteLine) call generates a call to the Console.WriteLine(string) method. (The ILGenerator. EmitWriteLine method will do all this for you—but calling that wouldn't be quite as interesting as the four steps in the EmitCIL method.)

0 0

Post a comment

  • Receive news updates via email from this site