Using the .NET Framework SDK Interoperability Tools

 

Sam Gentile
February 2004

Applies to:
   COM Interop
   Microsoft® .NET Framework
   C# language
   Microsoft Visual Studio® .NET

Summary: Sam Gentile dives deeply into the .NET Framework SDK interoperability tools, including TLBIMP, SN, TLBEXP, and REGASM. To go beyond (COM) Add Reference, you need to fully understand how to use these tools. (17 printed pages)

Prerequisites:

  • Basic knowledge of core Microsoft .NET concepts (assemblies, attributes, reflection, classes, properties, and events)
  • Ability to create Windows Forms applications in C# with Visual Studio .NET 2003
  • Ability to use the language compiler to build managed applications
  • Material in my first article, Beyond (COM) Add Reference: Has Anyone Seen the Bridge?

Download the associated Article2Code-Final.exe code sample.

Contents

Introduction
Metadata
The Type Library Importer
TLBIMP and Signing Assemblies
Time Keeper Sample
Generating the Primary Interop Assembly
COM Callable Wrapper (CCW)
TLBEXP
REGASM
SimpleCalculator
Where Are We?

Introduction

I introduced COM Interop through the notions of "bridges" in my first article, and I looked at why those bridges, the Runtime Callable Wrapper (RCW) and the COM Callable Wrapper (CCW), were necessary. There are vast differences between the world of COM and the world of .NET, and the bridges help to hide those from the programmer, and they allow interoperation. I also introduced the Type Library Importer tool, Tlbimp.exe, that ships both with the .NET Framework SDK and Visual Studio .NET. In this article, I am going to spend much more time with TLBIMP and show you some of its more advanced uses. I will also introduce you to "siblings" of TLBIMP in the .NET Framework SDK that pertain to COM Interop; Tlbexp.exe, Regasm.exe, and Sn.exe. These tools offer far more flexibility than (COM) Add Reference. ((COM) Add Reference can be considered a very particular subset of TLBIMP, or more properly, System.Runtime.InteropServices.TypeLibConverter.) First, before diving deeper into TLBIMP, we need to take a closer look at .NET metadata.

Metadata

The common language runtime (CLR) has no idea what to do with COM type data. It instead expects and works with .NET metadata. Metadata, along with the Common Type System (CTS) is really the heart of the CLR. If you take a read through the ECMA CLI Partition Specs (available at the ECMA and ISO/IEC C# and Common Language Infrastructure Standards site), which I highly recommend, you won't get very far before you come across metadata. Metadata is explained on Page 6 of the Introduction, and the whole of the ECMA Partition III specification is devoted to metadata. The key thing about .NET metadata is that it is not optional. All .NET language compilers are required to emit full metadata in addition to MSIL. As you may recall, COM type libraries are entirely optional and not complete in their descriptions. Moreover, COM does not enforce the correctness of the information within. If that weren't enough, the format is not extensible, nor does it make any attempt to describe component dependencies.

What about .NET metadata? Well, it completely describes the types that the code defines and the types that the code references externally. It is this metadata contained in assemblies that make assemblies "self-describing." The CLR then uses this metadata to manage all aspects of the code (load, execute, garbage collection, etc.), as well as provide nifty services and features, such as code installation, versioning, security, and cross-language interoperation, to name a few.

In particular, metadata provides the following information:

  • A description of the assembly in the manifest.
  • A description of all the types in each module.
  • Signatures of all methods in each module.
  • Custom attributes in each module.

For more details on assembly manifests and contents, see Applied Microsoft .NET Framework Programming, by Jeffrey Richter.

For our purposes, we need some mechanism to get COM type data from type library files "imported" into .NET assemblies, but "converted" from COM types to .NET types and metadata. That's the job of the Type Library Importer.

The Type Library Importer

When I talk about the Type Library Importer, it is actually a more general facility than TLBIMP. TLBIMP is just one way it is exposed. The actual facility is the TypeLibConverter class that lives in the System.Runtime.InteropServices namespace. This wonderful class contains a method, appropriately named ConvertTypeLibToAssembly. You can programmatically use this class and method to do your own custom import and/or export process (which we will examine in a later article of this series), but at this point, please note that TLBIMP is built on this class.

Interop Assemblies

The Runtime Callable Wrapper (RCW) is dynamically instantiated at runtime by the CLR from metadata contained in an Interop assembly. It is the Interop assembly where the "results" of the import and conversion process are stored on disk. Simply put, the Type Library Importer takes COM type data, converts it to CLR metadata, and stores it in the Interop assembly. Upon successful import, this metadata can be used by .NET compilers to resolve calls, and by the CLR to dynamically create the RCW.

One thing that confuses many people is that the import process is non-invasive. In other words, the underlying unmanaged COM component does not change at all. It stays intact. The Interop assembly can be thought of as a second view of the same COM component.

Importing a Type Library

There is not one but three different ways to import a COM type library. They are (in increasing levels of flexibility and effort):

  • Using Visual Studio .NET (COM) Add Reference
  • Using Tlbimp.exe, the command-line tool
  • Programmatically using the TypeLibConverter class in the System.Runtime.InteropServices namespace

Our focus right now is on Tlbimp.exe. There is a whole host of options that we are going to explore in the next section. For now, I will simply mention one important one. You can certainly use TLBIMP in its simplest form by merely specifying the name of the COM file you wish to convert. Unfortunately, if you do this, TLBIMP will overwrite that particular file with the Interop assembly without warning. (Note: This will be corrected in the next version of Visual Studio .NET, code-named "Whidbey".) What you want to do instead is specify the /out option. This allows you to specify the output file name that you wish. Your company may have particular standards for this, but a convention that I like to use is to precede the name of the file with "Interop." Thus "foo.dll" becomes "Interop.foo.dll." Given this, our simplest form of TLBIMP becomes:

TLBIMP foo.dll /out:Interop.foo.dll

The complete set of command line options (from MSDN) is shown in Table 1.

Table 1. TLBIMP options

Option Description
/asmversion:versionNumber Specifies the version number of the assembly to produce. Specify versionNumber in the format major.minor.build.revision.
/delaysign Specifies to Tlbimp.exe to sign the resulting assembly with a strong name using delayed signing. You must specify this option with either the /keycontainer:, /keyfile:, or /publickey: option. For more information on the delayed signing process, see Delay Signing an Assembly in the .NET Framework Developer's Guide.
/help Displays command syntax and options for the tool.
/keycontainer:containername Signs the resulting assembly with a strong name using the public/private key pair found in the key container specified by containername.
/keyfile:filename Signs the resulting assembly with a strong name using the publisher's official public/private key pair found in filename.
/namespace:namespace Specifies the namespace in which to produce the assembly.
/nologo Suppresses the Microsoft startup banner display.
/out:filename Specifies the name of the output file, assembly, and namespace in which to write the metadata definitions. The /out option has no effect on the assembly's namespace if the type library specifies the Interface Definition Language (IDL) custom attribute that explicitly controls the assembly's namespace. If you do not specify this option, Tlbimp.exe writes the metadata to a file with the same name as the actual type library defined within the input file and assigns it a .dll extension. If the output file is the same name as the input file, the tool generates an error to prevent overwriting the type library.
/primary Produces a primary Interop assembly for the specified type library. Information is added to the assembly indicating that the publisher of the type library produced the assembly. By specifying a primary Interop assembly, you differentiate a publisher's assembly from any other assemblies that are created from the type library using Tlbimp.exe. You should only use the /primary option if you are the publisher of the type library that you are importing with Tlbimp.exe. Note that you must sign a primary Interop assembly with a strong name. For more information, see Primary Interop Assemblies in the .NET Framework Developer's Guide.
/publickey:filename Specifies the file containing the public key to use to sign the resulting assembly. If you specify the /keyfile: or /keycontainer: option instead of /publickey:, Tlbimp.exe generates the public key from the public/private key pair supplied with /keyfile: or /keycontainer:. The /publickey: option supports test key and delay signing scenarios. The file is in the format generated by Sn.exe. For more information, see the -p option of Sn.exe in Strong Name Tool (Sn.exe).
/reference:filename Specifies the assembly file to use to resolve references to types defined outside the current type library. If you do not specify the /reference option, Tlbimp.exe automatically recursively imports any external type library that the type library being imported references. If you specify the /reference option, the tool attempts to resolve external types in the referenced assemblies before it imports other type libraries.
/silent Suppresses the display of success messages.
/strictref Does not import a type library if the tool cannot resolve all references within the current assembly or the assemblies specified with the /reference option.
/sysarray Specifies to the tool to import a COM style SafeArray as a managed System.Array Class type.
/transform:transformName Transforms metadata as specified by the transformName parameter.

Specify dispret for the transformName parameter to transform [out, retval] parameters of methods on dispatch-only interfaces (dispinterfaces) into return values.

For more information about this option, see the examples that follow.

/unsafe Produces interfaces without .NET Framework security checks. Calling a method that is exposed in this way might pose a security risk. You should not use this option unless you are aware of the risks of exposing such code.
/verbose Specifies verbose mode; displays additional information about the imported type library.
/? Displays command syntax and options for the tool.

I will be discussing most of these options shortly but lets look at one important use case for TLBIMP.

TLBIMP and Signing Assemblies

I have mentioned that TLBIMP offers far greater flexibility for importing COM types. The question you may be asking now is when would I need to make use of such flexibility and options? One of the most important times you would want to use TLBIMP is generating strongly named and signed Interop assemblies. TLBIMP includes options for supplying information necessary to generate strongly named assemblies. As a reminder, the CLR uses strong names with assemblies to protect the integrity of that assembly from tampering, as well as to guarantee uniqueness. Of course, a strong name is also required to make use of assembly versioning.

I will briefly review the process of giving an assembly a strong name. A strong name consists of four parts: a simple name (the filename sans extension), the version number, the culture identifier, and a public key token. To create a strongly named assembly, you must generate a cryptographic key pair (a private/public key pair) and sign the assembly with a private key. Each of the files that is added to the manifest has its contents hashed and the hash value stored with the file name. The entire contents of the assembly PE file are then hashed with SHA-1 and signed with your private key; the resulting RSA digital signature is stored in the assembly. The public key is also embedded into the assembly metadata. (For a comprehensive treatment of strong names and security see Keith Brown's excellent article, Security Briefs: Strong Names and Security in the .NET Framework.)

Cryptographic key pairs are generated with the Strong Name Tool (Sn.exe), which can be used to create a new, random key-pair:

SN –k keyPair.snk

You can then use the resulting key pair to sign the Interop assembly. The next issue to consider is when to sign the assembly.

Delay Signing an Interop Assembly

An Interop assembly can either be fully signed immediately or delay-signed. The process of signing is done at compilation time, but since most companies do not give their developers immediate access to the company's private key, the .NET Framework supports the notion of partially signed assemblies and delay signing. TLBIMP has command line options to support both scenarios. In the case of full immediate signing, one could do the following:

  1. Use the Strong Name tool to generate an RSA key pair

    SN –k keyPair.snk

  2. Use the /keyfile option of the tlbimp command

    C:\Code\MSDN\Article2\DelaySignPIA>tlbimp SimpleTimeKeeper.dll /keyfile:keyPair.snk /out:Interop.SimpleTimeKeeper.dll

Partially signed assemblies contain only the public key in the manifest, but do not yet contain hashes of the file contents (which require the private key). This allows development to proceed. At deployment time, you complete the signature with the company or your own private key as a final step. Many companies hold private keys in the hands of a few individuals and have explicit policies on their use. The .NET Framework allows for this kind of scenario and the application of the private key at a later time.

The steps for delay-signing Interop assemblies are (you can try these steps out on your machine with the sample download by working with the DelaySignPIA subdirectory):

  1. Generate an RSA key pair:

    C:\Code\MSDN\Article2\DelaySignPIA>SN –k keyPair.snk

  2. Extract the public key (Note: The company you work for will probably only allow you access to the public key and this step essentially has been done):

    C:\Code\MSDN\Article2\DelaySignPIA>sn -p keyPair.snk keyPublic.pub

  3. Specify the /publickey and /delaysign options on the tlbimp command line:

    C:\Code\MSDN\Article2\DelaySignPIA>tlbimp SimpleTimeKeeper.dll /delaysign /publickey:keyPublic.pub /out:Interop.SimpleTimeKeeper.dll

Strangely enough, my testing with the .NET Framework SDK 1.1 showed that it was possible to specify the /keyfile option with the /delaysign option. The MSDN Help for the tlbimp /publickey option says "If you specify the /keyfile: or /keycontainer: option instead of /publickey:, Tlbimp.exe generates the public key from the public/private key pair supplied with /keyfile: or /keycontainer:." This situation has been fixed in Whidbey.

A very important thing to note is that delay signing only partially signs the assembly permitting development work to go on, particularly in Interop. However, the assembly is still not a full strong name assembly and will fail Strong Name Verification if you attempt to place the assembly in the Global Assembly Cache (GAC) or verify the assembly with the Strong Name Tool (Sn.exe). When you are ready to ship your assembly outside development, most notably at deployment time, you will need someone in you company holding the private key, and using a secure machine, to sign the assembly, filling in the space that has been left by the delay-signing process, like so:

C:\Code\MSDN\Article2\DelaySignPIA>SN -R Interop.SimpleTimeKeeper.dll keyPair.snk

Before we look at an example, we need to look at primary Interop assemblies (PIA).

Primary Interop Assemblies (PIA)

We have just seen that Tlbimp.exe allows you to build an Interop assembly from any registered COM component that you might have (well, at least those with a type library). The next thing to ponder is, if you are in the business of selling COM class libraries, or you are a systems integrator who deploys significant COM components for purchase from the outside, what's the problem with the TLBIMP and Interop assembly story I have outlined so far? Well, if several companies produced Interop assemblies for a commonly used COM component, a client machine could end up with several Interop assemblies that differ only in the details of who produced them. On the surface, this seems only to clutter machines, and especially the GAC, with multiple copies of code that is functionally identical (some people have labeled this the "inverse of DLL Hell"), but it can introduce more serious problems.

Consider the following scenario: Suppose two different managed applications (Foo and Bar) both make use of the same COM component and have generated their own Interop assemblies. Inside that COM component is the exposed ILoveTheCLR interface. Foo exposes a method that needs to be passed an ILoveTheCLR interface reference. If Bar calls the method, it will pass a reference to its own Interop assembly. ("Danger, danger, Will Robinson!") The Lost in Space reference aside, why is this dangerous? Remember that the two Interop assemblies are each signed with their own keys, and therefore the CLR treats them as two entirely different entities. The fact that they are functionally identical is irrelevant.

How do we solve this problem? We need to have one "blessed," "official" Interop assembly for a given COM type. That's what a primary Interop assembly (PIA) is. It's a unique, master Interop assembly that represents a COM component, and is digitally signed by the publisher of the original COM component. An example of this is the OfficeXP PIAs created by Microsoft that contain the official description of commonly used Microsoft Office XP type libraries for applications such as Microsoft® Access 2002, Microsoft® Excel 2002, Microsoft® FrontPage® 2002, and more. See Office XP PIAs.

What's different about a PIA as compared to any other Interop assembly? There is an additional custom attribute in the metadata (System.Runtime.InteropServices.PrimaryInteropAssemblyAttribute) as well as the digital signature of the publisher, and some special registration. In terms of our discussion at hand, the tlbimp command contains a command line option /primary to produce a PIA. This is one of the places that TLBIMP provides functionality that (COM) Add Reference does not. You cannot use (COM) Add Reference to produce a PIA.

The time has come for an example to put this altogether.

Time Keeper Sample

I have created a somewhat trivial example for the sample COM component to interoperate with: an ATL C++ COM component exposing one interface, IMyTimeKeeper with one method, CurrentTime, which does exactly what it says—returns a string containing the current date and time. Its implementation is fairly standard to what you might find in any COM book, so I will not consider it here, as we will be using it purely for feeding into TLBIMP. You can download the sample code. Please note that the sample code does not perform any form of error checking for the sake of keeping the samples simple. In your code that you develop, you will obviously want to do this.

Also included in the downloadable sample code is a Visual Basic 6.0 application, TimeTester, to instantiate the COM object and exercise it.

Generating the Primary Interop Assembly

Our objective is to create a delay-signed primary Interop assembly for the SimpleTimeKeeper COM component with our new friend, TLBIMP. Again, to use this command, on the Programs menu, click Visual Studio .NET Tools | Visual Studio .NET 2003 Command Line Prompt. In this way, the correct path and environment variables will be set. Navigate to a directory that contains the built MSDNLinkedList.dll. From the sample provided, this is C:\Code\MSDN\Article2\DelaySignPIA.

We are also making use of one more command line option, /namespace. By default, TLBIMP creates a namespace based on the name of the output filename. Using the /namespace option allows you to specify the namespace in which to produce the assembly.

Our steps to produce the PIA then become:

  1. C:\Code\MSDN\Article2\DelaySignPIA>sn -k keyPair.snk
  2. C:\Code\MSDN\Article2\DelaySignPIA>sn -p keyPair.snk keyPublic.pub
  3. C:\Code\MSDN\Article2\DelaySignPIA>tlbimp SimpleTimeKeeper.dll /primary /delaysign /publickey:keyPublic.pub /out:Interop.SimpleTimeKeeper.dll /namespace:MSDN.Interop.TimeKeeper

To summarize what I have discussed so far: first, I created an RSA key pair stored in the file keyPair.snk, and then I extracted the public key into the file keyPublic.pub. After this step is done, follow the excellent advice in Keith Brown's article, Security Briefs: Strong Names and Security in the .NET Framework. Copy keyPublic.pub onto removable media, move it onto and remove keyPair.snk from the machine, place it in the company vault, and destroy the hard drive of the machine used to create them. Then I create the Interop assembly, delay-signed with the public key, and use the namespace MSDN.Interop.TimeKeeper.

To take a look at the "managed view," we can use Ildasm.exe. This tool, which also ships with both the .NET Framework SDK and Visual Studio .NET, allows us to look at the metadata and IL that is contained within a managed assembly. You will find this tool indispensable in your interoperability work as you customize the type library import and export process. Upon invoking ILDASM on our Interop assembly, our top-level view looks like the following:

Figure 1. Results of Type Library Importer process

This time around, we are particularly interested in the Manifest to see the additional metadata that TLBIMP has produced.

Click for larger image.

Figure 2. Metadata added to the manifest (click for larger image)

Inside the .assembly Interop.SimpleTimeKeeper section, you will notice the presence of the System.Runtime.InteropServices.PrimaryInteropAssemblyAttribute. You can also see the public key.

One More Thing on Primary Interop Assemblies

There is one more thing to say on PIAs. If you do not deploy PIAs in the GAC, there is an extra step required for proper registration. Visual Studio .NET and the Type Lib Importer will search for PIAs in the registry. When you generate an Interop assembly, the Importer will look in the type library's entry in the registry for special entries that indicate that a PIA exists for the type's library. These registry settings are easy to add with the Assembly Registration Tool (REGASM.exe). Please note, however, that the PIA must be fully signed with a private and public key pair to use REGASM. Again, this step looks like:

C:\Code\MSDN\Article2\DelaySignPIA>SN -R Interop.SimpleTimeKeeper.dll keyPair.snk

Then the generation of the registry key settings is accomplished with:

C:\Code\MSDN\Article2\DelaySignPIA>regasm Interop.SimpleTimeKeeper.dll

REGASM adds the following value to the registry:

HKEY_CLASSES_ROOT\TypeLib\{lib-id}\major.minor\PrimaryInteropAssemblyName

On my machine, this is:

HKEY_CLASSES_ROOT\TypeLib\{ 33A7E163-98DC-4C07-A9B1-2542F0E2795C}\1.0\PrimaryInteropAssemblyName and its value is:

Interop.SimpleTimeKeeper, Version=1.0.0.0, Culture=neutral, PublicKeyToken=7895fc5698804a32

Now, place the PIA in the GAC, so that it can be found.

C:\Code\MSDN\Article2\DelaySignPIA>gacutil -i Interop.SimpleTimeKeeper.dll

.NET Test Client for PIA

Now that we have our PIA, we can use it in a C# Windows Forms client project. The sample includes ManagedTimeKeeper, a forms-based project in C# to call the functionality of the COM component. The project adds a reference to the Interop assembly, Interop.SimpleTimeKeeper.dll. With the PIA set up in the registry, when Visual Studio .NET adds the project reference, it will automatically look in the registry to see if there is a valid primary Interop assembly that can be used. If it finds one, Visual Studio .NET will not generate a new Interop assembly. As long as it is installed in the GAC, we can be sure that it is using the "official" Interop assembly.

From that point, the code looks exactly the same as using any RCW:

private void button1_Click(object sender, System.EventArgs e)
{
MSDN.Interop.TimeKeeper.MyTimeKeeperClass timeClass = new MyTimeKeeperClass();
   System.String currentTime = timeClass.CurrentTime;
   label1.Text = currentTime;
}

Figure 3 shows the successful results.

Figure 3. ManagedTimeKeeper running

It is now time to look at two other Interop tools included with the .NET Framework SDK: TLBEXP.exe and REGASM.exe. To do that, I need to first review the "other side" of things—calling .NET components from COM clients.

COM Callable Wrapper (CCW)

As a matter of review from the first article, the COM Callable Wrapper (CCW) performs a role similar to the RCW in the other direction. It also acts as a bridge or proxy, this time when a COM client wishes to talk to a .NET object. The main job of the CCW is to forward calls to the .NET object from COM clients who are under the illusion that they are talking to another COM object. In that regard, the CCW implements standard COM interfaces like IUnknown, IDispatch, and quite a few others. There is a single CCW per managed object that can then have multiple COM clients with interface pointers to it.

The CCW performs the following functions:

  • Transforms COM data types into CLR equivalents (marshaling)
  • Simulates COM reference counting
  • Provides canned implementations of standard COM interfaces

Implications of the CCW

The CCW itself is not a managed class and has to live in the COM unmanaged world. The implications of this are that the CLR garbage collector does not control the lifetime of the CCW, but instead uses the reference-counting semantics employed by all COM components. As we will see in a later article, this has some very interesting consequences when the two lifetime systems clash.

As I mentioned, there is a single CCW per managed object that holds one internal reference to the CLR type being wrapped. The CCW destroys itself and releases the internal reference when the reference count reaches zero. This then makes the managed object eligible for garbage collection. This means that a .NET object is guaranteed to live as long as the CCW does.

There are two distinct differences when using the CCW for the RCW. The first of these is that the CCW does not need or use an Interop assembly in its creation, and the generation of a COM type library is completely optional in COM (but highly recommended for COM clients to have .NET types readily available). The second is that usually, but not always, the CCW needs to be registered in the Windows® registry to be callable from COM clients and live by the rules of COM. This is not always the case. Starting with Microsoft Windows XP and above, you can use the Win32® Side-by-Side assembly feature, a manifest can be generated for the managed assembly, and it can be referenced directly from the client application's manifest. This removes the need to register the managed assembly. For more information, see Registration-Free COM Interop in the .NET Framework Developer's Guide.

For earlier operating systems and the majority of COM Interop situations that I have been involved with, registry settings are needed, and that is the job of the Assembly Registration Tool (REGASM.exe).

TLBEXP

Before, I get to REGASM; I want to spend a brief amount of time on TLBEXP. I say "brief" because the tool is generally not that useful, as type library export is not needed for CCW creation nor required for COM operation. Furthermore, REGASM has an option to generate a type library, as well as generate registry settings. Therefore, TLBEXP is pretty unexciting and has very few options, unlike its sibling, TLBIMP.

What does TLBEXP do? TLBEXP is a command line tool that is the public face of the Type Library Exporter. It is the opposite of TLBIMP; it takes an assembly as input and generates a standalone type library (a *.tlb file). There are actually four ways to use the Type Library Exporter:

  • Using the Register for COM Interop project option in Visual Studio .NET (not considered in this series)
  • Using the REGASM.exe command-line tool with its /tlb option (see the next section)
  • Using TLBEXP.exe, a command line tool that is the focus of this section
  • Programmatically using the TypeLibConverter in System.Runtime.InteropServices (considered in a later article)

TLBEXP, unlike TLBIMP, has relatively few command line options, and the only one that is really any use is the /out option to control the name of the exported type library. Running the tool is as simple as:

TLBEXP myAssembly.dll

However, running this tool in isolation only produces a type library and is not sufficient to have interoperability from COM to .NET.

REGASM

The Assembly Registration Tool (REGASM.exe) parses the .NET assembly's metadata and generates the necessary registry settings that COM requires. As I mentioned before, the /tlb option generates and registers the type library as well, eliminating the need to use TLBEXP separately. REGASM is used on the command line as follows:

REGASM myAssembly.dll

or

REGASM myAssembly.dll /tlb

Strictly speaking, the assembly does not need to be signed for COM to call it, as it is possible to place it in the same directory as the COM client and it will work. However, it is highly recommended that you sign the assembly, and moreover, Microsoft recommends placing signed CCWs in the GAC. There are a series of steps to be followed and this is best illustrated with another example.

SimpleCalculator

The download includes a trivial .NET component, MSDNSimpleCalculator, which performs the four basic operations of a calculator. The code is simple and looks like this:

using System;
using System.Runtime.InteropServices;

public interface ISimpleCalulator
{
      Double Add(Double x, Double y);
      Double Subtract(Double x, Double y);
      Double Multiply(Double x, Double y);
      Double Divide(Double x, Double y);
}

   [ClassInterface(ClassInterfaceType.None)]
   public class SimpleCalculator : ISimpleCalulator
   {
      public SimpleCalculator() {}

      public Double Add(Double x, Double y)
      {
         return x + y;
      }
      public Double Subtract(Double x, Double y)
      {
         return x - y;
      }
      public Double Multiply(Double x, Double y)
      {
         return x * y;
      }
      public Double Divide(Double x, Double y)
      {
         return x / y;
      }
   }

There isn't much to discuss here except the ClassInterface attribute. The generation of class interfaces is one of the most confusing areas for aspiring COM Interop programmers, and will be explored fully in a future article. Essentially, the type lib export process generates a late-bound auto-dispatch interface with "empty" interfaces (no type information), by default. I will never forget the first time I encountered this strange behavior almost three years ago; I thought it was a bug! Suffice it to say, when most programmers first encounter using a CCW from a COM client, they are frequently very confused because there is no type information, and their only option is to make late-bound calls (without any type information). Why? The reason is actually quite reasonable if you understand the differences in versioning schemes between .NET and COM. The reason that type information is not exposed by default is that exposing it would not be safe for versioning. Remember, .NET classes are not like COM interfaces and co-classes; they don't have a rigid v-table layout. The class loader is free to choose any memory layout at runtime and, furthermore, as a true object-oriented (OO) system, programmers are free to add members to a .NET class whenever they feel like it. This would easily break existing COM clients dependent on the layout of the class interface. So by restricting COM clients to late binding, they don't break.

However, this is not usually what you want, and Microsoft recommends not generating a class interface, setting the ClassInterfaceType to None, and implementing a real .NET interface via the interface keyword. I will show you how to do this now, and I will cover this topic in more detail in article 5 of this series.

Notice the [ClassInterface(ClassInterfaceType.None)] attribute. This will suppress the generation of a class interface, as we have a real typed interface for COM to use. We will look further at this in the future; in this article I focus mainly on the tools.

Deployment of CCWs

Looking at the registry settings generated by REGASM will spur an interest as to where the CCW should be placed for deployment. You can certainly troll through the registry on your own, but a simpler approach is to use the REGASM /regfile option and look at what the Exporter is generating for registry settings.

This interesting little command line option generates a *.reg file that contains all the necessary registry information. There are two important things to know about the /regfile option. It is mutually exclusive with the /tlb file and does not update the registry, but instead generates a new .reg file in the current directory. So, the command is quite simple:

C:\Code\MSDN\Article2\MSDNSimpleCalculator\bin\Debug>regasm MSDNSimpleCalculator.dll /regfile

Next, open MSDNSimpleCalculator in your favorite editor to examine the generated *.reg file. After the COM ProgID, and a pointer from CLSID to GUID, notice the value for the InProcServer32 key. You may recall that this is the key that COM uses to locate the COM class when instantiating it. However, the key is not the filename of the assembly, but mscoreee.dll, the shim for the CLR execution engine. On my machine, this looks like:

[HKEY_CLASSES_ROOT\CLSID\{01C0A7D8-302C-364B-BD6D-3CA2C3011E89}\InprocServer32]

@="mscoree.dll"

"ThreadingModel"="Both"

"Class"="MSDN.BeyondAddReference.SimpleCalculator"

"Assembly"="MSDNSimpleCalculator, Version=1.0.0.2, Culture=neutral, PublicKeyToken=e4247f15f476738e"

"RuntimeVersion"="v1.1.4322"

To understand all this, you may have to recall a little bit about how traditional COM instantiates a COM co-class in a DLL. Recall, that COM objects implemented in a DLL expose an entry point, DllGetClassObject, to create class factories, and the class factory locates the object via the CLSID in the registry and instantiates it via CoCreateInstance(Ex) or similar mechanism. What's going on in Interop is that the CLR exposes a DllGetClassObject entry point for CCWs in MSCOREE.dll that the COM runtime calls. When this method is called, the Assembly value listed under \InProcServer32 is checked to figure out what .NET assembly to load. From here, Fusion (the CLR class loader) uses its elaborate rules to locate the assembly and load it, as it does for any other .NET assembly.

So what does this mean for CCWs and how to deploy them? The assembly must be placed somewhere where the CLR and Fusion can find it. There are three possible locations: in the application directory where the client executable is, in the GAC, or an arbitrary location specified via a /codebase attribute. The recommended place for CCWs is the GAC, since all assemblies are effectively shared once they are registered in the registry, and the GAC is where shared assemblies go.

Preparing for Interop Using TLBEXP and REGASM

We are now ready to prepare our assembly for COM Interop using the command line tools. The steps are as follows:

  • Create an RSA key pair C:\Code\MSDN\Article2\MSDNSimpleCalculator>sn -k MSDNKey.snk

  • Add the following attribute to the bottom of the AssemblyInfo.cs file:

    [assembly: AssemblyKeyFile(@"..\..\MSDNKey.snk")]
    
  • Rebuild the project

  • (Optional) You can try TLBEXP separately C:\Code\MSDN\Article2\MSDNSimpleCalculator\bin\Debug>tlbexp /out:MSDNSimpleCalculator.tlb MSDNSimpleCalculator.dll

  • Place the CCW in the GAC:

    C:\Code\MSDN\Article2\MSDNSimpleCalculator\bin\Debug>gacutil -i MSDNSimpleCalculator.dll

  • You can generate both the type library and the necessary registry settings with C:\Code\MSDN\Article2\MSDNSimpleCalculator\bin\Debug>regasm /tlb:MSDNSimpleCalcuator.tlb MSDNSimpleCalculator.dll

We can now start from any COM client.

Using the CCW from Visual Basic 6.0

Since the focus of this article is not on unmanaged COM, I am going to take the easy way out and generate a Visual Basic 6.0 project to call through our CCW to our .NET type. The sample download includes a Visual Basic 6.0 project that is a basic form to call the methods in the CCW. The code looks like this for the Add button:

Dim objCalculator As MSDNSimpleCalculator.SimpleCalculator
Private Sub Command1_Click()

    Dim x As Double
    Dim y As Double
    Dim result As Double
    
    x = Text1.Text
    y = Text2.Text
    Set objCalculator = CreateObject("MSDN.BeyondAddReference.SimpleCalculator")
    result = objCalculator.Add(x, y)
    Text3.Text = result
End Sub

Where Are We?

Knowing how to use the command line Interop tools of the .NET Framework SDK is a very important skill set for the enterprising interoperability developer. The tools are powerful and flexible, and are command-line-based, which allows their usage to be scripted. In addition, as we have seen, there are things that the tools do that Visual Studio .NET cannot.

We are now in the position to dive deeply into the depths of the Interop marshaling process, looking at the intimate details of the default transformations that the Marshaler performs, and how to customize it to produce the results you need. This will be the focus of my next article, so come back soon.