Managed Methods as Unmanaged Exports

Managed Methods as Unmanaged Exports

Exposing managed methods as unmanaged exports provides a way for unmanaged, non-COM clients to consume the managed services. In fact, this technique opens the managed world in all its glory—with its secure and type-safe computing and with all the wealth of its class libraries—to unmanaged clients.

Of course, the managed methods are not exposed as such. Instead, the inverse P/Invoke thunks, automatically created by the common language runtime, are exported. These thunks provide the same marshaling functions as “conventional” P/Invoke thunks, but in the opposite direction.

In order to expose managed methods as unmanaged exports, the ILAsm compiler builds a v-table, a v-table fixup (VTableFixup) table, and a group of unmanaged export tables, which include the Export Address table, the Name Pointer table, the Ordinal table, the Export Name table, and the Export Directory table. All of these tables, their structure, and their positioning within a managed PE file are discussed in Chapter 3, “The Structure of a Managed Executable File.”

The VTableFixup table is an array of VTableFixup descriptors, with each descriptor carrying the RVA of a v-table entry, the number of slots in the entry, and the binary flags indicating the size of each slot (32-bit or 64-bit) and any special features of the entry. One special feature is creation of the marshaling thunk to be exposed to the unmanaged client.

The v-table and the VTableFixup table of a managed module serve two purposes. One purpose—relevant only to the MC++ compiler, the only compiler that produces mixed-code modules—is to provide the managed/unmanaged linking capabilities for mixed-code modules. Another purpose is to facilitate the unmanaged export of managed methods.

Each slot of a v-table in a PE file carries the token of the method the slot represents. At run time, the v-table fixups are executed, replacing the method tokens with actual method addresses.

The ILAsm syntax for a v-table fixup definition is as follows:

.vtfixup [<num_slots><flags> at <data_label>

where square brackets are part of the definition and do not mean that <num_slots> is optional. <num_slots> is an integer constant, indicating the number of v-table slots grouped into one entry because their flags are identical. This grouping has no effect other than saving some space—you can emit a single slot per entry, but then you’ll have to emit as many v-table fixups as there are slots.

The flags specified in the definition can be those that are described in the following list.

  • int32  Each slot in this v-table entry is 4 bytes wide.

  • int64  Each slot in this v-table entry is 8 bytes wide. The int32 and int64 flags are mutually exclusive.

  • fromunmanaged  The entry is to be called from the unmanaged code, so the marshaling thunk must be created by the runtime.

  • callmostderived  This flag is not currently used.

The order of appearance of .vtfixup declarations defines the order of the respective VTableFixup descriptors in the VTableFixup table.

The v-table entries are defined simply as data entries. Note that the v-table must be contiguous—in other words, the data definitions for the v-table entries must immediately follow one another.

For example:

 .vtfixup [1] int32 fromunmanaged at VT_01  .vtfixup [1] int32 at VT_02  .data VT_01 = int32(0x0600001A) .data VT_02 = int32(0x0600001B) 

The actual data representing the method tokens is automatically generated by the ILAsm compiler and placed in designated v-table slots. To achieve that, it is necessary to indicate which method is represented by which v-table slot. ILAsm provides the .vtentry directive for this purpose:

.vtentry <entry_number> <slot_number>

where <entry_number> and <slot_number> are one-based integer constants. The .vtentry directive is placed within the respective method’s scope, as shown in the following code:

 .vtfixup [1] int32 fromunmanaged at VT_01  .method public static void Foo() {    .vtentry 1:1 // Entry 1, slot 1     }  .data VT_01 = int32(0) // The slot will be filled automatically. 

The export table group consists of five tables:

  • The Export Address table (EAT), containing the RVA of the exported unmanaged functions

  • The Export Name table (ENT), containing the names of the exported functions

  • The Name Pointer table (NPT) and the Ordinal table (OT), together forming a lookup table that rearranges the exported functions in lexical order of their names

  • The Export Directory table, containing the location and size information about the other four tables

Location and size information concerning the Export Directory table itself resides in the first of 16 data directories in the PE header. The structure of the export table group is shown in Figure 15-2.

Figure 15-2 The structure of the export table group.

In an unmanaged PE file, the EAT contains the RVA of the exported unmanaged methods. In a managed PE file, the picture is more complicated. The EAT cannot contain the RVA of the managed methods because it’s not the managed methods that are exported—rather, it’s their marshaling thunks, generated at run time.

The only way to address a yet-to-be-created thunk is to define a slot in a v-table entry for the exported managed method and a VTableFixup descriptor for this entry, carrying the fromunmanaged flag. In this case, the contents of the v-table slot (a token of the exported method) are replaced at run time with the address of the marshaling thunk. (If the fromunmanaged flag is not specified, the thunk is not created, and the method token is replaced with this method’s address; but this is outside the scenario being discussed.)

For each exported method, the ILAsm compiler creates a tiny native stub—yes, you’ve caught me: the ILAsm compiler does produce embedded native code after all—consisting of the x86 command jump indirect (0x25FF) followed by the RVA of the v-table slot allocated for the exported method. The EAT contains the RVA of these tiny stubs.

The tiny stubs are necessary because the EAT must contain solid addresses of the exported methods as soon as the operating system loads the PE file. Otherwise, the unmanaged client won’t be able to match the entries of its Import Address table (IAT) to the entries of the managed module’s EAT. The addresses of the methods or their thunks don’t exist at the moment the file is loaded. But the tiny stubs exist and have solid addresses. It’s true that at that moment they cannot perform any meaningful jumps, because the v-table slots they are referencing contain method tokens instead of addresses. But by the time the stubs are called, the methods and thunks will have been generated and the v-table slots will be fixed up, the method tokens replaced with thunk addresses.

The diagram shown in Figure 15-3 illustrates this scenario.

Figure 15-3 Indirect referencing of v-table entries from the EAT.

The unmanaged exports require that relocation fixups are executed at the module load time. When a program runs under the Microsoft Windows XP operating system, this requirement can create a problem similar to those encountered with thread local storage (TLS) data and data-on-data. As described in Chapter 3, if the common language runtime header flag COMIMAGE_FLAGS_ILONLY is set, the loader of Windows XP ignores the .reloc section, and the fixups are not executed. To avoid this, the ILAsm compiler automatically replaces the COMIMAGE_FLAGS_ILONLY flag with COM- IMAGE_FLAGS_32BITREQUIRED whenever the source code specifies TLS data or data-on-data. Unfortunately, the compiler neglects to do this automatically when unmanaged exports are specified in the source code, and it is thus necessary to explicitly set the runtime header flags using the directive .corflags 0x00000002.

The ILAsm syntax for declaring a method as an unmanaged export is very simple:

.export [<ordinal>] as <export_name> 

where <ordinal> is an integer constant. The <export_name> provides an alias for the exported method. It is necessary to specify <export name> even if the method is exported under its own name.

The .export directive is placed within the scope of the respective method together with the .vtentry directive, as shown in this example:

 .corflags 0x00000002  .vtfixup [1] int32 fromunmanaged at VT_01  .method public static void Foo() {    .vtentry 1:1     // Entry 1, slot 1    .export [1] as Bar // Export #1, Name="Bar"     }  .data VT_01 = int32(0) // The slot will be filled automatically. 

The source code for the small sample described earlier in Figure 15-2 could look like the following, which is taken from the sample file on the companion CD:

.assembly extern mscorlib { } .assembly YDD { } .module YDD.dll .corflags 0x00000002 .vtfixup [1] int32 fromunmanaged at VT_01 // First v-table fixup .vtfixup [1] int32 fromunmanaged at VT_02 // Second v-table fixup .vtfixup [1] int32 fromunmanaged at VT_03 // Third v-table fixup .data VT_01 = int32(0)         // First v-table entry .data VT_02 = int32(0)         // Second v-table entry .data VT_03 = int32(0)         // Third v-table entry .method public static void Yabba() {    .vtentry 1:1    .export [1] as Yabba    ldstr "Yabba"    call void [mscorlib]System.Console::WriteLine(string)    ret } .method public static void Dabba() {    .vtentry 2:1    .export [2] as Dabba    ldstr "Dabba"    call void [mscorlib]System.Console::WriteLine(string)    ret} .method public static void Doo() {    .vtentry 3:1    .export [3] as Doo    ldstr "Doo!"    call void [mscorlib]System.Console::WriteLine(string)    ret}

Now you can compile the sample to a managed DLL, remembering to use the /DLL command-line option of the ILAsm compiler, and then write a small unmanaged program that calls the methods from this DLL. This unmanaged program can be built with any unmanaged compiler—for example, Microsoft Visual C++ 6—but don’t forget that YDD.dll cannot run unless the .NET Framework is installed. It’s still a managed assembly, even if your unmanaged program does not know about it.

As you’ve probably noticed, all .vtfixup directives of the sample sport identical flags. This means that three single-slot v-table entries can be grouped into one three-slot entry:

.vtfixup [3] int32 fromunmanaged at VT_01 .data VT_01 = int32[3]

Then the .vtentry directives of the Dabba and Doo methods must be changed to .vtentry 1:2 and .vtentry 1:3, respectively.

It’s worth making a few additional points about the sample. First, it’s a good practice to define all VTableFixup and v-table entries in the beginning of the source code, before any methods or other data constants are defined. This ensures that you will not attempt to assign a nonexistent v-table slot to a method and that the v-table will be contiguous.

Second, in the sample, the export ordinals correspond to v-table entry numbers. In fact, no such correspondence is necessary. But if you’re using the v-table only for the purpose of unmanaged export, it might not be a bad idea to maintain this correspondence simply to keep track of your v-table slots. It won’t do you any good to assign the same v-table slot or the same export ordinal to two different methods.

Third, you should remember that the export ordinals are relative. The Export Directory table has a Base entry, which contains the base value for the export ordinals. The ILAsm compiler simply finds the lowest ordinal used in the .export directives throughout the source code and assigns this ordinal to the Base entry. If you start numbering your exports from 5, it does not mean that the first four entries in the EAT will be undefined. The common practice is to use one-based export ordinals.

Inside Microsoft. NET IL Assembler
Inside Microsoft .NET IL Assembler
ISBN: 0735615470
EAN: 2147483647
Year: 2005
Pages: 147

Similar book on Amazon © 2008-2017.
If you may any questions please contact us: