Chapter 13. COM and Windows Interoperability

IN THIS CHAPTER

Interoperability refers to the ability of managed and unmanaged code to invoke each other and exchange data in an organized and reproducible fashion. This chapter introduces you to the basic concepts behind code and data interoperability in C# and provides you with code samples for communicating with unmanaged code via COM or using Platform Invoke for accessing unmanaged DLLs such as the Win32 API.

Introduction to Interoperability in C#

Interoperability enables you to leverage your existing investment in unmanaged code going forward with the .NET Framework. Unmanaged code consists of all code that is not managed and executed within the Common Language Runtime (CLR). This includes COM objects, C++ DLLs, ActiveX controls, COM+ components, and any other code that is not compiled.

As you will find out, there are two different kinds of interoperability in C#, interoperability with unmanaged DLLs and interoperability with binary COM objects. Each is accomplished using a different technology, but some aspects of interoperability remain the same regardless of whether you’re working with unmanaged DLLs or COM objects.

One of the things that remain the same is the need for reliable transfer of data. A common problem among developers before managed code was not being able to write code in one language that could communicate with code written in another language. For example, a C string was stored in memory as an array of bytes terminated by the ASCII null () character. What is called a “Pascal string” is an array of bytes where the length of the string is stored in the 0th element of that array. As you can imagine, you can’t simply pass a string from Pascal to C or from C to Pascal without modification.

The same is true of managed and unmanaged code. The process by which data is transferred between managed and unmanaged environments is called marshaling. This process is what converts .NET types into types that will be recognizable by unmanaged code and vice versa. As you’ll see in the code samples that follow, there are tools that can determine how to marshal data from one environment to the other when reading and writing things like COM type libraries, but you still may need to know ahead of time how the data will need to be marshaled in order to consume unmanaged code from within .NET.

Using COM Objects from the .NET Framework

A COM object identifiesall of the interfaces, methods, and code that are exposed to outside code through the use of a type library. In order to use a COM object from within the .NET Framework, you need access to the type library so that the framework can create an appropriate wrapper around the COM object itself.

This wrapper is called a Runtime Callable Wrapper. These wrapper objects serve as proxies between the actual COM object and the C# client code. When you make a call on the wrapper, the call is to purely managed code and you send standard managed parameters like System.String, System.Int32, and so on. The wrapper then takes care of the marshaling of data between your managed code and the COM object and back again in order to process the results.

When you add a reference from within your project in Visual Studio 2005, a tab appears in the Add Reference dialog called COM, as shown in Figure 13.1.

Figure 13.1 The COM tab in the Add Reference dialog.

Image

When you add a reference to a COM object, Visual Studio will extract the type library and then create a pure managed class that interfaces directly with the COM code. This enables you to be insulated from the painful and repetitive details of things like reference counting and querying the interface list. You don’t even really need to know what a vtable is to consume COM code from C#.

If you don’t use Visual Studio to create the Runtime Callable Wrapper (RCW) around the intended COM object, you can use a command-line tool to exert more fine-grained control over the creation of the type library. This tool is called TLBIMP.EXE, and it is part of the .NET Framework SDK.

TLBIMP allows you to specify the namespace of the COM objects contained within the type library, as well as to indicate things like the output filename, the strong-name key pair with which to sign the wrapper Assembly, and whether or not the Assembly will be a Primary InterOp Assembly (discussed in the next section).

The following is an example that takes the Microsoft Fax control from a COM type library and imports it into a DLL called Microsoft.Fax.dll:

tlbimp fxsocm.dll /namespace:Microsoft.Fax /out:Microsoft.Fax.dll

For a complete list of all the options available in the TLBIMP tool, consult the .NET Framework SDK, or you can simply type TLBIMP in your Visual Studio Command Prompt with no arguments and the list of all options will be displayed.

Using Primary InterOp Assemblies

When consuming a vendor’s COM objects, they will often have created Primary InterOp Assemblies. What this means is that the vendor has already done the work of creating the wrapper around the type library, and could possibly have done some extra work to make the library easier to use such as defining custom marshaling rules, and so on. When using a Primary InterOp Assembly (PIA), you reference the PIA the same way you would reference any other managed assembly (for example, you either browse to the file or you find it in the .NET tab of your Add Reference dialog).

Typically, a component or application vendor will install the PIAs when the application is installed to allow developers immediate access to the exposed components whether they’re using COM or .NET. For example, when you install Microsoft Office 2003, you can include the .NET Programmability option for each of the Office products like Microsoft Word or Microsoft Outlook.

To create an application that consumes a PIA (or any other COM object, for that matter), first create your project. In this case, we’ll be creating a console application because we haven’t covered working with Windows Forms yet.

After you’ve created your project, right-click the project, choose References, and then add a reference. The Primary InterOp Assemblies for Microsoft Office are located in the Global Assembly Cache. You cannot reference Assemblies directly from the GAC; instead you have to reference them from their home directories and the system will use the GAC Assembly at runtime. The same is true for the PIAs.

In the COM tab, browse all the way down to the Microsoft Word 11.0 Type Library (assuming you have Office 2003 installed on your PC). Select it and click OK to add the reference to your application.

If there is a PIA for that type library in the GAC, Visual Studio will automatically reference that PIA and will not generate its own private Runtime Callable Wrapper Assembly. Because there is a PIA for Word, the reference in your application is a PIA reference to the assembly in the GAC. You can prove this by hovering the mouse over the Path property in the Word reference. You will see that it’s pointing to a path beneath the windowsAssembly directory, which is where all of the GAC Assemblies are stored.

When you have a reference to Microsoft Word, you can feel free to use the Word object model to manipulate Word documents, as shown in Listing 13.1.

Listing 13.1 Creating a Word Document Using a COM PIA

Image

Image

When you run the preceding application, assuming Office 2003 is installed properly, you will get a Word document like the one shown in Figure 13.2.

Figure 13.2 Word document created via COM InterOp.

Image

What you might notice from the preceding sample is that there are a lot of object references and very little strong typing. This kind of code is typical of COM programming where the original COM API wasn’t strictly typed and used a lot of open object references.

The idea behind this sample is that the mechanics of creating the Runtime Callable Wrapper around a COM type library aren’t the difficult part: the difficult part is using the code exposed by the COM object to begin with.

Using .NET Classes from COM

Exposing your .NET classes to COM is easy. All you have to do is decorate the class, member, or assembly with the ComVisible attribute and that will be enough for the registration tool to determine what information to include in the type library when it is created. To follow along with the samples you are about to see, just create a new class library called ComVisibleLibrary.

The easiest way to expose your code to COM is to expose your entire assembly to COM. You can do this by manually editing your AssemblyInfo.cs file, or you can right-click your project, choose Properties, and then click the Assembly Information button. You will see a dialog like the one shown in Figure 13.3. Just check the checkbox indicating that the assembly is visible to COM.

Figure 13.3 Exposing an entire assembly to COM.

Image

To test this out, change the Class1.cs class definition in the ComVisibleLibrary class library from the stock definition to the following:

Image

After you have compiled an assembly that is visible to COM, it isn’t automatically made available to COM clients. To make your code available to COM clients, you need to register your COM objects in the registry in a method similar to the way you would register your unmanaged C++ or VB6 COM objects. To register a .NET-hosted COM object, you use the regasm command-line SDK tool, as shown in the following example:

regasm ComVisibleLibrary.dll /tlb:ComVisibleLibrary.tlb

This will not only register your library with all the other COM objects on the machine, but it will also export a usable type library based on your assembly.

You might think that when you have a .NET COM object registered, you can consume that .NET COM object from managed code. If you try to reference your own COM object from within .NET, as shown in Figure 13.4, you will receive an error message indicating that you cannot create a COM reference to a .NET assembly. This is, of course, for a good reason. If there is a .NET assembly available, there is no need for managed code to use COM, as it would add a lot of unnecessary overhead.

Figure 13.4 Referencing a COM object from a .NET project.

Image

To reference your .NET-hosted COM object from an unmanaged language, simply use whatever tools you would normally use to consume unmanaged COM components to reference the .NET-hosted component. The COM-Callable Wrapper that .NET places between the unmanaged COM code and your managed code abstracts the fact that it is a .NET object. The benefit of this is that unmanaged COM clients can use .NET-hosted COM objects and regular/legacy COM objects interchangeably without having to do any additional work.

Accessing Code in Unmanaged DLLs

Accessing code in unmanaged DLLs is similar to accessing code using COM InterOp in that your code is reaching across AppDomain boundaries and from managed to unmanaged code. This type of access requires the SecurityPermission of UnmanagedCode. Don’t confuse the phrase “unmanaged DLLs” with “COM DLLs.” There is a distinct difference. The contents of a COM DLL conform to a specific binary format with specific functions used to query the list of interfaces exposed by the components contained within that DLL. A standard unmanaged DLL has no such standard.

A normal unmanaged DLL has no way of telling consuming clients what functions are available, so clients need to know ahead of time what functions they can use and the size, type, and direction of all function parameters.

You might remember way back in the days when you couldn’t expose true object-oriented components—the best you could do was export functions within a DLL.

To tell the CLR that you are using a method from an unmanaged DLL instead of a native managed method, you decorate a method declaration with the DllImport attribute. This attribute takes the name of the DLL in which the CLR can find the associated extern function, which is declared right under the DllImport attribute, as shown in the following example:

[DllImport("myFuncs.dll")]
public static extern void MyFunction();

One of the most common uses for consuming functions from unmanaged DLLs is to gain direct access to the Win32 API functions. The code in Listing 13.2 shows you a common way of consuming unmanaged code in the Win32 API.

Listing 13.2 Using DllImport

Image

Before you decide to use unmanaged code directly, make sure that the same functionality is not already available within the .NET Framework. As mentioned earlier, the cost of marshaling data and invoking unmanaged code via wrappers is expensive.

You can also use some functionality that is new to .NET 2.0 to create an instance of a delegate that is really a function pointer to unmanaged code. You can then invoke that delegate instance just as you would any other managed delegate, as shown in Listing 13.3. This provides a far easier interface to calling unmanaged code such as the Win32 API than trying to do it “the hard way.”

Listing 13.3 Obtaining and Invoking Unmanaged Function Pointers

Image

Image

There are a couple of new things in the preceding code. The first is the use of the Marshal.GetDelegateFromFunctionPointer() method. This method is incredibly powerful. It allows you to take any function pointer represented by an IntPtr retrieved using the GetProcAddress function from the kernel32 library and turn that function pointer into a managed delegate. After you’ve obtained the delegate from the function pointer, you can create events based on that delegate, you can pass the delegate as a parameter to other functions, and you can invoke the delegate directly.

There is also a new attribute in the preceding code, MarshalAs. This attribute allows you to define, for any method parameter, how that data type should be marshaled. For example, if you know that the function you want to call using a managed delegate takes an LPWSTR (long-pointer to a wide Unicode string), you can marshal a managed string as an UnmanagedType.LPWStr. The marshaler will then take care of the details of converting a managed string into a LPWSTR.

Summary

Now that the .NET Framework has been around for over four years, the need to mix managed and unmanaged code has greatly diminished. However, there are still plenty of cases where developers need to access COM objects from within the .NET Framework as well as a need to access .NET components from COM clients. In addition to COM InterOperability, this chapter covered some of the details involved in working with unmanaged DLLs using the Platform Invoke features such as the DllImport attribute and the Marshal.GetDelegateFromFunctionPointer method that enable easy and straightforward access to unmanaged APIs such as the Win32 API. This chapter isn’t the complete source of information on COM and Windows InterOp; however, after completing this chapter you should feel comfortable enough with the concepts and code surrounding Interoperability that you can make use of it in your own projects.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.17.62.34