第10章 用未操控的代码进行内部操作(CHAPTER 10 Interoperating with Unmanaged Code)

.NET is definitely a cool technology. But a cool technology isn’t worth a dime if it doesn’t allow you to use the (unmanaged) code that already exists, whether the code is in the form of COM components or functions implemented in C DLLs. Furthermore, sometimes managed code might get into the way of writing high-performance code—you must be able to write unmanaged,unsafe code.

.NET and C# offer you the following techniques to interoperate with unmanaged code:

  • COM Interoperability
  • Platform Invocation Services
  • Unsafe code

10.1 COM互用性(COM Interoperability)

The first and most interesting interoperability technique is interoperability with COM. The reason is that for a long time to come, COM and .NET must coexist. Your .NET clients must be able to call your legacy COM components, and COM clients must make use of new .NET components.

The following two sections deal with both issues:

  • Exposing .NET objects to COM
  • Exposing COM objects to .NET objects

Though the interoperability discussion is centered around C#, please note that you could replace C# with VB or managed C++. It is an interoperability feature provided by the .NET framework to all programming languages emitting managed code.

10.1.1 向COM展示.NET对象(Exposing .NET Objects to COM)

One way to interoperate is to allow a COM client to use an .NET component component (written in C#, for example). To prove the feasibility, the examples presented use the namespaced version of the RequestWebPage and WhoisLookup classes’ .NET component created in Chapter 8, “Writing Components in C#.”

The various tasks involved in making an .NET component work with a COM client are presented in the following two sections:

  • Registering a .NET object
  • Invoking a .NET object

Registering a .NET Object

In COM, you first have to register an object before it can be used. When registering a COM object, you use the regsvr32 application, which you obviously can’t use for a COM+ 2.0 application. However, there is a similar tool for .NET components: regasm.exe. The regasm tool enables you to register an .NET component in the Registry (including all classes that are contained, given that they are publicly accessible), and it also creates a Registry file for you when you request it. The latter is useful when you want to examine what entries are added to the Registry.

The command is as follows:

regasm csharp.dll /reg:csharp.reg

The output file (csharp.reg) that is generated is shown in Listing 10.1. When you are used to COM programming, you’ll recognize the entries that are being made to the Registry. Note that the ProgId is composed of the namespace and class names.

Listing 10.1 The Registry File Generated by regasm.exe

REGEDIT4

[HKEY_CLASS_ROOT\Presenting.CSharp.RequestWebPage]
@=”COM+ class: Presenting.CSharp.RequestWebPage”

[HKEY_CLASS_ROOT\Presenting.CSharp.RequestWebPage\CLSID]
@=”{6B74AC4D-4489-3714-BB2E-58F9F5ADEEA3}”

[HKEY_CLASS_ROOT\CLSID\{6B74AC4D-4489-3714-BB2E-58F9F5ADEEA3}]
@=”COM+ class: Presenting.CSharp.RequestWebPage”

[HKEY_CLASS_ROOT\CLSID\{6B74AC4D-4489-3714-BB2E-58F9F5ADEEA3}\InprocServer32]
@=”D:\WINNT\System32\MSCorEE.dll”
“ThreadingModel”=”Both”
“Class”=”Presenting.CSharp.RequestWebPage”
“Assembly”=”csharp, Ver=1.0.1.0”

[HKEY_CLASS_ROOT\CLSID\{6B74AC4D-4489-3714-BB2E-58F9F5ADEEA3}\ProgId]
@=”Presenting.CSharp.RequestWebPage”

[HKEY_CLASS_ROOT\Presenting.CSharp.WhoisLookup]
@=”COM+ class: Presenting.CSharp.WhoisLookup”

[HKEY_CLASS_ROOT\Presenting.CSharp.WhoisLookup\CLSID]
@=”{8B5D2461-07DB-3B5C-A8F9-8539A4B9BE34}”

[HKEY_CLASS_ROOT\CLSID\{8B5D2461-07DB-3B5C-A8F9-8539A4B9BE34}]
@=”COM+ class: Presenting.CSharp.WhoisLookup”

[HKEY_CLASS_ROOT\CLSID\{8B5D2461-07DB-3B5C-A8F9-8539A4B9BE34}\InprocServer32]
@=”D:\WINNT\System32\MSCorEE.dll”
“ThreadingModel”=”Both”
“Class”=”Presenting.CSharp.WhoisLookup”
“Assembly”=”csharp, Ver=1.0.1.0”

[HKEY_CLASS_ROOT\CLSID\{8B5D2461-07DB-3B5C-A8F9-8539A4B9BE34}\ProgId]
@=”Presenting.CSharp.WhoisLookup”

Take a closer look at lines 30–34. As you can see, the execution engine (MSCorEE.dll) is called when an instance of your object is requested, not your library itself. The execution engine is responsible for providing the CCW (COM Callable Wrapper) for your object. If you want to register the component without a Registry file, all you have to do is issue this command:

regasm csharp.dll

Now the component can be used in programming languages that support late binding. If you are not content with late binding (and you shouldn’t be), the tlbexp utility enables you to generate a type library for your .NET component:

tlbexp csharp.dll /out:csharp.tlb

This type library can be used in programming languages that support early binding. Now your .NET component is a good citizen in COM society.

Now that we are in the COM world, I want to dive right into the type library and point out a few important things. I have used the OLE View application, which comes with Visual Studio, to open the type library and extract the IDL (Interface Description Language) of the classes contained in the .NET component. Listing 10.2 shows the results I obtained.

Listing 10.2 The IDL File for the WhoisLookup and RequestWebPage Classes

// Generated .IDL file (by the OLE/COM Object Viewer)
// 
// typelib filename: <could not determine filename>

[
uuid(A4466FD5-EB56-3C07-A0D8-43153AC4FD06),
version(1.0)
]
library csharp
{
// TLib : // TLib : : {BED7F4EA-1A96-11D2-8F08-00A0C9A6186D}
importlib("mscorlib.tlb");
// TLib : OLE Automation : {00020430-0000-0000-C000-000000000046}
importlib("stdole2.tlb");

// Forward declare all types defined in this typelib
interface _RequestWebPage;
interface _WhoisLookup;

[
uuid(6B74AC4D-4489-3714-BB2E-58F9F5ADEEA3),
custom({0F21F359-AB84-41E8-9A78-36D110E6D2F9}, "Presenting.CSharp.RequestWebPage")
]
coclass RequestWebPage {
[default] interface _RequestWebPage;
interface _Object;
};

[
odl,
uuid(1E8F7AAB-FA6C-315B-9DFE-59C80C6483A9),
hidden,
dual,
nonextensible,
oleautomation,
custom({0F21F359-AB84-41E8-9A78-36D110E6D2F9}, "Presenting.CSharp.RequestWebPage") 

]
interface _RequestWebPage : IDispatch {
[id(00000000), propget]
HRESULT ToString([out, retval] BSTR* pRetVal);
[id(0x60020001)]
HRESULT Equals(
[in] VARIANT obj, 
[out, retval] VARIANT_BOOL* pRetVal);
[id(0x60020002)]
HRESULT GetHashCode([out, retval] long* pRetVal);
[id(0x60020003)]
HRESULT GetType([out, retval] _Type** pRetVal);
[id(0x60020004), propget]
HRESULT URL([out, retval] BSTR* pRetVal);
[id(0x60020004), propput]
HRESULT URL([in] BSTR pRetVal);
[id(0x60020006)]
HRESULT GetContent([out] BSTR* strContent);
};

[
uuid(8B5D2461-07DB-3B5C-A8F9-8539A4B9BE34),
custom({0F21F359-AB84-41E8-9A78-36D110E6D2F9}, "Presenting.CSharp.WhoisLookup")
]
coclass WhoisLookup {
[default] interface _WhoisLookup;
interface _Object;
};

[
odl,
uuid(07255177-A6E5-3E9F-BAB3-1B3E9833A39E),
hidden,
dual,
nonextensible,
oleautomation,
custom({0F21F359-AB84-41E8-9A78-36D110E6D2F9}, "Presenting.CSharp.WhoisLookup")

]
interface _WhoisLookup : IDispatch {
[id(00000000), propget]
HRESULT ToString([out, retval] BSTR* pRetVal);
[id(0x60020001)]
HRESULT Equals(
[in] VARIANT obj, 
[out, retval] VARIANT_BOOL* pRetVal);
[id(0x60020002)]
HRESULT GetHashCode([out, retval] long* pRetVal);
[id(0x60020003)]
HRESULT GetType([out, retval] _Type** pRetVal);
};
};

If you are a C++ programmer, you are used to writing and maintaining such monsters. As a VB programmer, looking at such an IDL file might be a first for you.

Note that both co-classes have one IDispatch-derived interface, as well as an interface named Object (lines 24 and 62). The IDispatch default interface contains the methods you implemented in your object, plus those from the Object interface. You will also notice that now everything is using the BSTR and VARIANTs that we all know and love.

Now let’s look at the interfaces in more detail. First, I want to pick the RequestWebPage interface. Figure 10.1 shows it expanded in the OLE View application.

Figure 10.1
Figure 10.1 The RequestWebPage method exposes its URL property, as well as the GetContent method.

The URL property is exposed (via get and set methods), as well as the GetContent method. There are also four methods that belong to the Object interface. It looks just like it would in C# directly.

The WhoisLookup interface is a little bit different. It shows the four Object methods, but where is the Query method? (See Figure 10.2.)

Figure 10.2
Figure 10.2 Static methods do not show up. This object cannot be used in COM.

The reason the Query method is not shown is that static methods do not show up in COM. You cannot use this object in COM unless you rewrite Query to an instance method. Therefore, if you plan to use objects outside the CLR, decide wisely which methods are static and which are instance methods.

Invoking an .NET Object

The .NET component and all classes are is registered, and you have a type library for environments that prefer early binding—you are all set. To demonstrate that the component works as expected, I choose Excel as the environment to script it.

To be able to use early binding in Excel, you must reference the type library. In the VBA Editor, run the References command in the Tools menu. Choose Browse in the References dialog box and then select the type library in the Add Reference dialog box (see Figure 10.3).

Figure 10.3
Figure 10.3 Importing the type library for the component.

The only task left is coding the retrieve operation. As you can see from Listing 10.3, it isn’t complicated. Note that I added an On Error GoTo statement to perform the necessary COM error handling.

Listing 10.3 Using the RequestWebPage Class in an Excel Module

Option Explicit

Sub GetSomeInfo()
On Error GoTo Err_GetSomeInfo
Dim wrq As New csharp.RequestWebPage
Dim strResult As String

wrq.URL = “http://www.alphasierrapapa.com/iisdev/”
wrq.GetContent strResult
Debug.Print strResult

Exit Sub
Err_GetSomeInfo:
MsgBox Err.Description
Exit Sub
End Sub

.NET exceptions are translated to HRESULTs, and the exception information is passed via the error information interfaces. Excel then raises an error based on this information.

When you run the code in Listing 10.3, the output is written to the immediate window. Try entering an invalid URL to see how the exceptions are propagated from the CLR to a COM client.

10.1.2 向.NET对象展示COM对象(Exposing COM Objects to the .NET Object)

Interoperation also works the other way around—.NET clients can interoperate with classic COM objects. Accessing legacy objects is the more likely scenario during the transition period from COM to .NET.

There are two ways to access COM objects from an .NET client application:

  • Invoking early-bound objects
  • Invoking late-bound objects

For the examples presented in this section I chose the AspTouch component, which can change the file date of a given file. AspTouch has a dual interface and a type library, and it is free. If you want to follow the examples in this section, you can download AspTouch from http://www.alphasierrapapa.com/iisdev/components/open in new window.

Invoking Early-Bound Objects

For a component to be used early-bound in COM, it must have a type library. For the CLR, this ttranslates to the metadata that is stored with the types. But wait—metadata is associated with a type, but what is the .NET type for the COM component?

To be able to call the COM component from a .NET object, you need a wrapper around the unsafe code. Such a wrapper is called an RCW (Runtime Callable Wrapper), and it is built from the type library information. A tool generates the wrapper code for you, based on the information obtained from the type library.

The tool to use is tlbimp (type library import). Its command line is simple:

tlbimp asptouch.dll /out:asptouchlib.dll

This command imports the COM type library from asptouch.dll (it is contained in the DLL as a resource), and creates and stores an RCW that can be used in the CLR in the file asptouchlib.dll. You can use ildasm.exe to view the metadata for the RCW (see Figure 10.4). Chapter 11, “Debugging C# Code,” covers the use of ILDasm at greater length.

Figure 10.4
Figure 10.4 Using ILDasm to view the metadata of asptouchlib.dll.

When you look at the ILDasm output, you can see that ASPTOUCHlib is the namespace (it was the name of the type library), and TouchIt is the class name of the proxy that was generated for the original COM object. With this information, you can write a .NET object that uses the COM component (see Listing 10.4).

Listing 10.4 Using a COM Component in C# via an RCW

using System;
using ASPTOUCHLib;

class TouchFile
{
    public static void Main()
    {
        TouchIt ti = new TouchIt();
        bool bResult = false;
        try
        {
            bResult = ti.SetToCurrentTime("asptouch.cs");
        }
        catch (Exception e)
        {
            Console.WriteLine(e);
        }
        finally
        {
            if (true == bResult)
            {
                Console.WriteLine("Successfully changed file time!");
            }
        }
    }
}

This code looks and feels just like any other C# code that uses a class. There is a using statement, method invocation, and exception handling (this time, the HRESULTs are wrapped as exceptions). Even the compilation command is familiar to you:

csc /r:asptouchlib.dll /out:touch.exe asptouch.cs

It works just like with any other .NET component. After you have created the RCW, working with COM components is a walk in the park.

Invoking Late-Bound Objects

If you have a component without a type library, or you have to call it on-the-fly without prior generation of an RCW, you aren’t lost at all. A cool feature of the CLR will help you out: reflection. Now you can find out all about a component at runtime.

Reflection is the way to go when dealing with late-bound objects. The code in Listing 10.5 uses reflection to create the object and to invoke its methods. It performs the same actions as the previous script, but it doesn’t have a wrapper class.

Listing 10.5 Accessing a COM Component Using Reflection

using System;
using System.Reflection;

class TestLateBound
{
    public static void Main()
    {
        Type tTouch;
        tTouch = Type.GetTypeFromProgID("AspTouch.TouchIt");

        Object objTouch;
        objTouch = Activator.CreateInstance(tTouch);

        Object[] parameters = new Object[1];
        parameters[0] = "noway.txt";
        bool bResult = false;

        try
        {
            bResult = (bool)tTouch.InvokeMember("SetToCurrentTime",
            BindingFlags.InvokeMethod,
            null, objTouch, parameters);
        }
        catch (Exception e)
        {
            Console.WriteLine(e);
        }

        if (bResult)
            Console.WriteLine("Changed successfully!");
    }
}

The class to use for reflection is Type, which is included in the System.Reflection namespace. Line 9 then calls GetTypeFromProgID with the ProgId of the COM component in question to get the component’s type. Although I don’t check for an exception, you should do so; an exception is thrown if the type could not be loaded.

Now that the type is loaded, I can create an instance of it by using the CreateInstance static method of the Activator class. The TouchIt object is ready to be used. But the really ugly part of late-bound programming has just begun—invoking methods.

If you loved late-bound programming with C++ and COM, you’ll find yourself at home with this code immediately. All parameters—in this case, the name of the file—must be packaged in an array (lines 14–15), and the call to the method is performed indirectly via the InvokeMember of the Type object (lines 20–22). You have to pass it the name of the method, the binding flags, a binder, the object, and finally, the parameters. The result returned by the invocation must be cast to the appropriate type of C#/CLR.

Looks and feels ugly, doesn’t it? And the call I use in this example is not even the most complicated one you can come up with. Passing parameters by reference is much more fun, I promise.

Although the complexity of working with late-bound objects is manageable after all, there is exactly one reason why you always should work with RCWs instead: speed. Late-bound invocation is a magnitude slower than working with early-bound objects.

10.2 平台请求服务(Platform Invocation Services)

Even with all the .NET framework classes and COM Interoperability, you sometimes might feel the need to call a single function provided by WIN32 or some other unmanaged DLL. This is the time when you might want to use the Platform Invocation Services (PInvoke). PInvoke takes care of finding and invoking the correct function, as well as marshaling its managed arguments to and from their unmanaged counterparts.

All you have to do to is use the sysimport attribute when defining an extern method in C#:

[sysimport(
dll=dllname,
name=entrypoint,
charset=character set
)]

Only the dll argument is mandatory; both other arguments are optional. If you omit the name attribute, the name of the externally implemented function must match the name of the internal static method.

Listing 10.6 demonstrates how to invoke the message box function of WIN32 using PInvoke.

Listing 10.6 Using PInvoke to Call WIN32 Functions

using System;

class TestPInvoke
{
    [sysimport(dll="user32.dll")]
    public static extern int MessageBoxA(int hWnd, string strMsg,
    string strCaption, int nType);

    public static void Main()
    {
        int nMsgBoxResult;
        nMsgBoxResult = MessageBoxA(0, "Hello C#", "PInvoke", 0);
    }
}

Line 5 uses the sysimport attribute to specify that the function I am going to call is declared in user32.dll. Because I do not specify a name argument, the following definition for the extern method must exactly match the name of the function I want to call: MessageBoxA, where A is for the ANSI version of this function. The output of this simple application is a message box with a "Hello C#" message.

Listing 10.7 demonstrates that by using the name argument, you can rename the extern method to your liking.

Listing 10.7 Modifying the sysimport Attribute Still Yields the Desired Result

using System;

class TestPInvoke
{
    [sysimport(dll="user32.dll", name="MessageBoxA")]
    public static extern int PopupBox(int h, string m, string c, int type);

    public static void Main()
    {
        int nMsgBoxResult;
        nMsgBoxResult = PopupBox(0, "Hello C#", "PInvoke", 0);
    }
}

Although I demonstrated only a very straightforward and simple WIN32 method, you can invoke any method that comes to your mind. If you get extremely fancy, you can access WIN32 resource data or implement your own data marshaling. For this, however, you have to take a look into the SDK documentation of the .NET SDK.

10.3 不安全代码(Unsafe Code)

Programming unsafe code yourself is definitely not a task you will perform every day when using C#. However, it is good to know that you can use pointers when you have to do so. C# supports you with two keywords for writing unsafe code:

  • unsafe—This keyword denotes an unsafe context. When you want to perform unsafe actions, you must wrap the corresponding code with this modifier. It can be applied to constructors, methods, and properties.

  • fixed—Declaring a variable as fixed prevents the garbage collector from relocating it.

Unless you really need to work with raw blocks of memory—with pointers, that is—COM Interoperability and the Platform Invocation Services should cover almost all your needs to talk to COM or WIN32 functions.

To give you an idea what unsafe code might look like, take a look at Listing 10.8. It shows how to use the unsafe and fixed keywords to create a program that performs the square calculation just a little bit differently. To learn more about writing unsafe code, please take a look at the C# reference.

Listing 10.8 Working with Unsafe Code

using System;

public class SquareSampleUnsafe
{
    unsafe public void CalcSquare(int nSideLength, int* pResult)
    {
        *pResult = nSideLength * nSideLength;
    }
}

class TestUnsafe
{
    public static void Main()
    {
        int nResult = 0;

        unsafe
        {
            fixed (int* pResult = &nResult)
            {
                SquareSampleUnsafe sqsu = new SquareSampleUnsafe();
                sqsu.CalcSquare(15, pResult);
                Console.WriteLine(nResult);
            }
        }
    }
}

10.4 小结(Summary)

This chapter was entirely about how managed code can interoperate with unmanaged code. At first, you learned how COM Interoperability can make .NET components work with COM clients, as well as how you can use COM components in .NET clients. You learned about the differences of calling an object with late binding or early binding, and what metadata and type libraries look like for the conversion process.

A further interoperability service is the Platform Invocation Service PInvoke. It enables you to call WIN32 functions, and it takes care of the data marshaling for you. However, if you want to do it on your own, PInvoke allows you to do so.

The last feature presented is unsafe code. Although C# prefers managed code, you still can work with pointers, pin blocks of memory to a specific location, and do all the stuff you always wanted to do but that managed C# didn’t allow.

Contributors: FHL