- Referencing a COM Component in Visual Studio .NET
- Referencing a COM Component Using Only the .NET Framework SDK
- Example: A Spoken Hello, World Using the Microsoft Speech API
- The Type Library Importer
- Using COM Objects in ASP.NET Pages
- An Introduction to Interop Marshaling
- Common Interactions with COM Objects
- Using ActiveX Controls in .NET Applications
- Deploying a .NET Application That Uses COM
- Example: Using Microsoft Word to Check Spelling
- Conclusion
The Type Library Importer
COM components such as the Microsoft Speech API expose type information using type libraries, but .NET compilers (excluding Visual C++ .NET) don't understand type libraries. They only understand .NET metadata as a source of type information. Therefore, the key to making a COM component readily available to the .NET world is a mechanism that takes a type library and produces equivalent metadata. The Common Language Runtime's execution engine contains this functionality, which is called the type library importer.
The term type library importer was introduced previously when describing TLBIMP.EXE, but the type library importer is exposed in other ways as well. In fact, when you add a reference to a COM component in Visual Studio .NET, you are really invoking the type library importer. This creates a new assembly, and this assemblynot the type libraryis what your project is really referencing. This assembly (SpeechLib.dll in the previous example) is the same file that would have been generated by TLBIMP.EXE. It's always a single file with a .dll extension, and is placed in a directory specific to your project when generated by Visual Studio .NET.
Interop Assemblies
An assembly produced by the type library importer is known as an Interop Assembly because it contains definitions of COM types that can be used from managed code via COM Interoperability. The metadata inside an Interop Assembly enables .NET compilers to resolve calls, and enables the Common Language Runtime to generate a Runtime-Callable Wrapper (RCW) at run time.
You can think of importing a type library as providing a second view of the same COM componentone for unmanaged clients and another for managed clients. This relationship is shown in Figure 3.5.
Figure 3.5 Two views of the same COM component.
The actual implementation of the COM component remains in the original COM binary file(s). In other words, nothing magical happens to transform unmanaged code into the MSIL produced by a .NET compiler. Unlike normal assemblies, which contain an abundance of both metadata and MSIL, Interop Assemblies consist mostly of metadata. The signatures have special custom attributes and pseudo-custom attributes that instruct the CLR to delegate calls to the original COM component at run time.
There are three ways of using the type library importer to create an Interop Assembly:
Referencing a type library in Visual Studio .NET.
Using TLBIMP.EXE, the command-line utility that is part of the .NET Framework SDK.
Using the TypeLibConverter class in the System.Runtime.InteropServices namespace.
All three of these methods produce the exact same output, although each option gives more flexibility than the previous one. TLBIMP.EXE has several options to customize the import process, but Visual Studio .NET doesn't expose these customizations when referencing a type library. Using TLBIMP.EXE to precisely mimic the behavior of Visual Studio .NET when it imports a type library, you'd need to run it as follows:
TlbImp InputFile /out:Interop.LibraryName.dll /namespace:LibraryName /sysarray
where InputFile is the file containing the type library (such as SAPI.DLL) and LibraryName is the name found inside the type library that can be viewed with OLEVIEW.EXE (such as SpeechLib). All of TLBIMP.EXE's options are covered in Appendix B, "SDK Tools Reference."
The TypeLibConverter class enables programmatic access to type library importing, and has one additional capability when compared to TLBIMP.EXEimporting an in-memory type library. The use of this class is demonstrated in Chapter 22, "Using APIs Instead of SDK Tools."
Some of the transformations made by the type library importer are non-intuitive. For example, every COM class (such as SpVoice) is converted to an interface, then an additional class with the name ClassNameClass (such as SpVoiceClass) is generated. More information about this transformation is given in the next chapter, "An In-Depth Look at Imported Assemblies," but this is why SpVoiceClass had to be used in the C++ Hello, World example. The C# and VB .NET compilers perform a little magic to enable instantiating one of these special interfaces such as SpVoice, and treats it as if you're instantiating SpVoiceClass instead.
Whereas the process of converting type library information to metadata is called importing, and the process of converting metadata to type library information is called exporting. Type library export is introduced in Chapter 8, "The Essentials for Using .NET Components from COM."
Tip
Although TLBIMP.EXE has several options that Visual Studio .NET doesn't allow you to configure within the IDE, you can reference an Interop Assembly in two steps in order to gain the desired customizations within Visual Studio .NET. First, produce the Interop Assembly exactly as you wish using TLBIMP.EXE. Then, reference this assembly within Visual Studio .NET using the .NET tab instead of the COM tab. Simply browse to the assembly and add it to your project, just as you would add any other assembly.
Everything so far assumes that a type library is available for the COM component you want to use. For several existing COM components, this is simply not the case. If the COM component has one or more IDL files, you could create a type library from them using the MIDL compiler. (Unfortunately, there's no such tool as IDLIMP that directly converts from IDL to .NET metadata.) As mentioned previously in Figure 3.5, however, .NET compilers can produce the same kind of metadata that the type library importer produces, so you can create an Interop Assembly directly from .NET source code. This advanced technique is covered in Chapter 21, "Manually Defining COM Types in Source Code." An easy solution to a lack of type information is to perform late binding, if the COM objects you wish to use support the IDispatch interface. See the "Common Interactions with COM Objects" section for more information.
Primary Interop Assemblies
The previously described process of generating Interop Assemblies has an undesirable side effect due to the differences in the definition of identity between COM and the .NET Framework. Therefore, extra support exists to map .NET and COM identities more appropriately.
In COM, a type is identified by its GUID. It doesn't matter where you get the definition; if the numeric value of the GUID is correct, then everything works. You might find a common COM interface (such as IFont) defined in ten different type libraries, rather than each library referencing the official definition in the OLE Automation type library (STDOLE2.TLB). This duplication doesn't matter in the world of COM.
On the other hand, if ten different assemblies each have a definition of IFont, these are considered ten unrelated interfaces in managed code simply because they reside in different assemblies. The containing assembly is part of a managed type's identity, so it doesn't matter if multiple interfaces have the same name or look identical in all other respects.
This is the root of the identity problem. Suppose ten software companies write .NET applications that use definitions in the OLE Automation type library (STDOLE2.TLB). Each company uses Visual Studio .NET and adds a reference to the type library, causing a new assembly to be generated called stdole.dll. Each company digitally signs the assemblies that comprise the application, including the stdole Interop Assembly, as shown in Figure 3.6.
Figure 3.6 Applications from ten different companieseach using their own Interop Assembly for the OLE Automation type library.
Now imagine a user's computer with all ten of these programs installed. One problem is that the Global Assembly Cache is cluttered with Interop Assemblies that have no differences except for the publisher's identity, shown in Figure 3.7. The difference in publishers can be seen as differences in the public key tokens.
Figure 3.7 The opposite of DLL Hella cluttered Global Assembly Cache with multiple Interop Assemblies for the same type library.
Even if all these Interop Assemblies aren't installed in the GAC, the computer could be cluttered in other places, such as local application directories. This is the opposite of DLL Hellapplication isolation taken to an extreme. There is no sharing whatsoever, not even when it's safe for the applications to do so.
Besides clutter, the main problem is that none of these applications can easily communicate as they could if they were unmanaged COM applications. If the Payroll assembly from Figure 3.6 exposes a method with an IFont parameter and the Search assembly wants to call this, it would try passing a stdole.IFont signed by Donna's Antiques. This doesn't work because the method expects a stdole.IFont signed by Rick's Aviation. As far as the CLR is concerned, these are completely different types.
What we really want is a single managed identity for a COM component's type definitions. Such "blessed" Interop Assemblies do exist, and are known as Primary Interop Assemblies (PIAs). A Primary Interop Assembly is digitally signed by the publisher of the original COM component. In the case of the OLE Automation type library, the publisher is Microsoft. A Primary Interop Assembly is not much different from any other Interop Assembly. Besides being digitally signed by the COM component's author, it is marked with a PIA-specific custom attribute (System.Runtime.InteropServices.PrimaryInteropAssemblyAttribute), and usually registered specially.
Figure 3.8 shows what the ten applications would look like if each used the Primary Interop Assembly for the OLE Automation Type Library rather than custom Interop Assemblies.
Figure 3.8 Applications from ten different companieseach using the Primary Interop Assembly for the OLE Automation type library.
Nothing forces .NET applications to make use of a PIA when one exists. The notion of Primary Interop Assemblies is just a convention that can be used by tools, such as Visual Studio .NET, to help guide software developers in the right direction by referencing common types.
To make use of Primary Interop Assemblies, adding a reference to a type library in Visual Studio .NET is a little more complicated than what was first stated. Visual Studio .NET tries to avoid invoking the type library importer if at all possible, since it generates a new assembly with its own identity. Instead, Visual Studio .NET automatically adds a copy of a PIA to a project's references if one is registered for a type library that the user references on the COM tab of the Add Reference dialog. (A copy of an assembly is just as good as the original, since the internal assembly identity is the same.) The TLBIMP.EXE utility, on the other hand, simply warns the user when attempting to create an Interop Assembly for a type library whose PIA is registered on the current computer; it still creates a new Interop Assembly. TLBIMP.EXE does make use of registered PIAs for dependent type libraries, described in the next chapter.
As Primary Interop Assemblies are created for existing COM components, they will likely be available for download from MSDN Online or component vendor Web sites. At the time of writing, no PIAs other than the handful that ship with Visual Studio .NET exist.
Digging Deeper
It's possible to work around the identity problem caused by multiple Interop Assemblies without a notion of Primary Interop Assemblies. Due to the way in which COM interfaces are handled by the CLR, it's possible to cast from a COM interface definition in one assembly to a COM interface definition in another assembly if they have the same IID. (This does not work for regular .NET interfaces.) It's also possible to convert from a COM class defined in one assembly to a COM class defined in another assembly using the Marshal.CreateWrapperOfType method. Nothing enables converting between instances of duplicate structure definitions, but a structure's fields could be copied one-by-one.
But Primary Interop Assemblies have another important use. Interop Assemblies often need modifications, such as the ones shown in Chapter 7 to be completely usable in managed code. When you have a PIA with such customizations registered on your computer, you can benefit from these customizations simply by referencing the type library for the COM component you wish to use inside Visual Studio .NET. For example, the PIA for Microsoft ActiveX Data Objects (ADO), which ships with Visual Studio .NET, contains some customizations to handle object lifetime issues. If you created your own Interop Assembly for ADO using TLBIMP.EXE, you would not benefit from these customizations.
The process of creating and registering Primary Interop Assemblies is discussed in Chapter 15, "Creating and Deploying Useful Primary Interop Assemblies."