Tài liệu hạn chế xem trước, để xem đầy đủ mời bạn chọn Tải xuống
1
/ 109 trang
THÔNG TIN TÀI LIỆU
Thông tin cơ bản
Định dạng
Số trang
109
Dung lượng
1,44 MB
Nội dung
Figure 17-5. The guts of the Interop.SimpleComServer.dll interop assembly CHAPTER 17 ■ COM AND .NET INTEROPERABILITY 497 As you can see, consuming a COM type from a .NET application can be a very transparent operation indeed. As you might imagine, however, a number of details are occurring behind the scenes to make this communication possible, the gory details of which you will explore throughout this chapter, beginning with taking a deeper look into the interop assembly itself. Investigating a .NET Interop Assembly As you have just seen, when you reference a COM server using the Visual Studio 2005 Add Reference dialog box, the IDE responds by generating a brand-new .NET assembly taking an Interop. prefix (such as Interop.SimpleComServer.dll). Just like an assembly that you would create yourself, interop assemblies contain type metadata, an assembly manifest, and under some circumstances may contain CIL code. As well, just like a “normal” assembly, interop assemblies can be deployed privately (e.g., within the directory of the client assembly) or assigned a strong name to be deployed to the GAC. Interop assemblies are little more than containers to hold .NET metadata descriptions of the original COM types. In many cases, interop assemblies do not contain CIL instructions to implement their methods, as the real work is taking place in the COM server itself. The only time an interop assembly contains executable CIL instructions is if the COM server contains COM objects that have the ability to fire events to the client. In this case, the CIL code within the interop assembly is used by the CLR to manage the event handing logic. At first glance, it may seem that interop assemblies are not entirely useful, given that they do not contain any implementation logic. However, the metadata descriptions within an interop assembly are extremely important, as it will be consumed by the CLR at runtime to build a runtime proxy (termed the Runtime Callable Wrapper, or simply RCW) that forms a bridge between the .NET application and the COM object it is communicating with. You’ll examine the details of the RCW in the next several sections; however, for the time being, open up the Interop.SimpleComServer.dll assembly using ildasm.exe, as you see in Figure 17-5. As you can see, although the original VB6 project only defined a single COM class (ComCalc), the interop assembly contains three types. To make things even more confusing, if you were to examine the interop assembly using Visual Studio 2005, you only see a single type named ComCalc. Rest assured that ComCalcClass and _ComCalc are within the interop assembly. To view them, you simply need to elect to view hidden types with the VS 2005 Object Browser (see Figure 17-6). 5785ch17.qxd 3/31/06 1:57 PM Page 497 CHAPTER 17 ■ COM AND .NET INTEROPERABILITY498 Simply put, each COM class is represented by three distinct .NET types. First, you have a .NET type that is identically named to the original COM type (ComCalc, in this case). Next, you have a sec- ond .NET type that takes a Class suffix (ComCalcClass). These types are very helpful when you have a COM type that implements several custom interfaces, in that the Class-suffixed types expose all members from each interface supported by the COM type. Thus, from a .NET programmer’s point of view, there is no need to manually obtain a reference to a specific COM interface before invoking its functionality. Although ComCalc did not implement multiple custom interfaces, we are able to invoke the Add() and Subtract() methods from a ComCalcClass object (rather than a ComCalc object) as follows: Module Program Sub Main() Console.WriteLine("***** The .NET COM Client App *****") ' Now using the Class-suffixed type. Dim comObj As New ComCalcClass() Console.WriteLine("COM server says 10 + 832 is {0}", _ comObj.Add(10, 832)) Console.ReadLine() End Sub End Module Finally, interop assemblies define .NET equivalents of any original COM interfaces defined within the COM server. In this case, we find a .NET interface named _ComCalc. Unless you are well versed in the mechanics of VB 6.0 COM, this is certain to appear strange, given that we never directly created an interface in our SimpleComServer project (let alone the oddly named _ComCalc interface). The role of these underscore-prefixed interfaces will become clear as you move throughout this chapter; for now, simply know that if you really wanted to, you could make use of interface-based programming techniques to invoke Add() or Subtract(): Module Program Sub Main() Console.WriteLine("***** The .NET COM Client App *****") ' Now manually obtain the hidden interface. Dim i As SimpleComServer._ComCalc Figure 17-6. Viewing hidden types within our interop assembly 5785ch17.qxd 3/31/06 1:57 PM Page 498 CHAPTER 17 ■ COM AND .NET INTEROPERABILITY 499 Figure 17-7. RCWs sit between the .NET caller and the COM object. Dim c As New ComCalc i = CType(c, _ComCalc) Console.WriteLine("COM server says 10 + 832 is {0}", _ i.Add(10, 832)) Console.ReadLine() End Sub End Module Now, do understand that invoking a method using the Class-suffixed or underscore-prefixed interface is seldom necessary (which is exactly why the Visual Studio 2005 Object Browser hides these types by default). However, as you build more complex .NET applications that need to work with COM types in more sophisticated manners, having knowledge of these types is critical. ■Source Code The VBNetSimpleComClient project is located under the Chapter 17 subdirectory. Understanding the Runtime Callable Wrapper As mentioned, at runtime the CLR will make use of the metadata contained within a .NET interop assembly to build a proxy type that will manage the process of .NET to COM communication. The proxy to which I am referring is the Runtime Callable Wrapper, which is little more than a bridge to the real COM class (officially termed a coclass). Every coclass accessed by a .NET client requires a corresponding RCW. Thus, if you have a single .NET application that uses three COM coclasses, you end up with three distinct RCWs that map .NET calls into COM requests. Figure 17-7 illustrates the big picture. ■Note There is always a single RCW per COM object, regardless of how many interfaces the .NET client has obtained from the COM type (you’ll examine a multi-interfaced VB 6.0 COM object a bit later in this chapter). Using this technique, the RCW can maintain the correct COM identity (and reference count) of the COM object. 5785ch17.qxd 3/31/06 1:57 PM Page 499 CHAPTER 17 ■ COM AND .NET INTEROPERABILITY500 Again, the good news is that the RCW is created automatically when required by the CLR. The other bit of good news is that legacy COM servers do not require any modifications to be consumed by a .NET-aware language. The intervening RCW takes care of the internal work. To see how this is achieved, let’s formalize some core responsibilities of the RCW. The RCW: Exposing COM Types As .NET Types The RCW is in charge of transforming COM data types into .NET equivalents (and vice versa). As a simple example, assume you have aVB 6.0 COM subroutine defined as follows: ' VB 6.0 COM method definition. Public Sub DisplayThisString(ByVal s as String) The interop assembly defines the method parameter as a .NET System.String: ' VB 2005 mapping of COM method. Public Sub DisplayThisString(ByVal s as System.String) When this method is invoked by the .NET code base, the RCW automatically takes the incom- ing System.String and transforms it into a VB 6.0 String data type (which, as you may know, is in fact a COM BSTR). As you would guess, all VB 6.0 COM data types have a corresponding .NET equivalent. To help you gain your bearings, Table 17-1 documents the mapping taking place between COM IDL (interface definition language) data types, the related .NET System data types, and the corresponding VB 2005 keyword. Table 17-1. Mapping Intrinsic COM Types to .NET Types COM IDL Data Type System Types Visual Basic 2005 Data Type wchar_t, short System.Int16 Short long, int System.Int32 Integer Hyper System.Int64 Long unsigned char, byte System.Byte Byte single System.Single Single double System.Double Double VARIANT_BOOL System.Boolean Boolean BSTR System.String String VARIANT System.Object Object DECIMAL System.Decimal Decimal DATE System.DateTime DateTime GUID System.Guid Guid CURRENCY System.Decimal Decimal IUnknown System.Object Object IDispatch System.Object Object ■Note You will come to understand the importance of having some knowledge of IDL data types as you progress through this chapter. 5785ch17.qxd 3/31/06 1:57 PM Page 500 CHAPTER 17 ■ COM AND .NET INTEROPERABILITY 501 The RCW: Managing a Coclass’s Reference Count Another important duty of the RCW is to manage the reference count of the COM object. As you may know from your experience with COM, the COM reference-counting scheme is a joint venture between coclass and client and revolves around the proper use of AddRef() and Release() calls. COM classes self-destruct when they detect that they have no outstanding references (thankfully, VB 6.0 would call these low-level COM methods behind the scenes). However, .NET types do not use the COM reference-counting scheme, and therefore a .NET client should not be forced to call Release() on the COM types it uses. To keep each participant happy, the RCW caches all interface references internally and triggers the final release when the type is no longer used by the .NET client. The bottom line is that similar to VB 6.0, .NET clients never explicitly call AddRef(), Release(), or QueryInterface(). ■Note If you wish to directly interact with a COM object’s reference count from a .NET application, the System.Runtime.InteropServices namespace provides a type named Marshal. This class defines a number of shared methods, many of which can be used to manually interact with a COM object’s lifetime. Although you will typically not need to make use of Marshal in most of your applications, consult the .NET Framework 2.0 SDK doc- umentation for further details. The RCW: Hiding Low-level COM Interfaces The final core service provided by the RCW is to consume a number of low-level COM interfaces. Because the RCW tries to do everything it can to fool the .NET client into thinking it is communicat- ing with a native .NET type, the RCW must hide various low-level COM interfaces from view. For example, when you build a COM class that supports IConnectionPointContainer (and maintains a subobject or two supporting IConnectionPoint), the coclass in question is able to fire events back to the COM client. VB 6.0 hides this entire process from view using the Event and RaiseEvent keywords. In the same vein, the RCW also hides such COM “goo” from the .NET client. Table 17-2 outlines the role of these hidden COM interfaces consumed by the RCW. Table 17-2. Hidden COM Interfaces Hidden COM Interface Meaning in Life IConnectionPointContainer Enable a coclass to send events back to an interested client. VB 6.0 IConnectionPoint automatically provides a default implementation of each of these interfaces. IDispatch Facilitate “late binding” to a coclass. Again, when you are building IProvideClassInfo VB 6.0 COM types, these interfaces are automatically supported by a given COM type. IErrorInfo These interfaces enable COM clients and COM objects to send and ISupportErrorInfo respond to COM errors. ICreateErrorInfo IUnknown The granddaddy of COM. Manages the reference count of the COM object and allows clients to obtain interfaces from the coclass. 5785ch17.qxd 3/31/06 1:57 PM Page 501 CHAPTER 17 ■ COM AND .NET INTEROPERABILITY502 The Role of COM IDL At this point you hopefully have a solid understanding of the role of the interop assembly and the RCW. Before you go much further into the COM to .NET conversion process, it is necessary to review some of the finer details of COM IDL. Understand, of course, that this chapter is not intended to function as a complete COM IDL tutorial; however, to better understand the interop layer, you only need to be aware of a few IDL constructs. As you saw in Chapter 14, a .NET assembly contains metadata. Formally speaking, metadata is used to describe each and every aspect of a .NET assembly, including the internal types (their mem- bers, base class, and so on), assembly version, and optional assembly-level information (strong name, culture, and so on). In many ways, .NET metadata is the big brother of an earlier metadata format used to describe classic COM servers. Classic ActiveX COM servers (*.dlls or *.exes) document their internal types using a type library, which may be realized as a stand-alone *.tlb file or bundled into the COM server as an internal resource (which is the default behavior of VB 6.0). COM type libraries are themselves created using a metadata language called the Interface Definition Language and a special compiler named midl.exe (the Microsoft IDL compiler). VB 6.0 does a fantastic job of hiding type libraries and IDL from view. In fact, many skilled VB COM programmers can live a happy and productive life ignoring the syntax of IDL altogether. Nevertheless, whenever you compile ActiveX project workspace types, VB automatically generates and embeds the type library within the physical *.dll or *.exe COM server. Furthermore, VB 6.0 ensures that the type library is automatically registered under a very particular part of the system registry: HKEY_CLASSES_ROOT\TypeLib (see Figure 17-8). Type libraries are referenced all the time by numerous IDEs. For example, whenever you access the Project ➤ References menu selection of VB 6.0, the IDE consults HKCR\TypeLib to determine each and every registered type library, as shown in Figure 17-9. ■Note In reality, COM type library browser tools will only consult HKCR\TypeLib the first time the tool is acti- vated, and cache the results for later use. This explains why the first time you load such tools, there is a noticeable delay. Figure 17-8. HKCR\TypeLib lists all registered type libraries on a given machine. 5785ch17.qxd 3/31/06 1:57 PM Page 502 CHAPTER 17 ■ COM AND .NET INTEROPERABILITY 503 Likewise, when you open the VB 6.0 Object Browser, the VB 6.0 IDE reads the type information and displays the contents of the COM server using a friendly GUI, as shown in Figure 17-10. Observing the Generated IDL for Your VB COM Server Although the VB 6.0 Object Browser displays all COM types contained within a type library, the OLE View utility (oleview.exe) allows you to view the underlying IDL syntax used to build the corresponding type library. Again, few VB 6.0 developers need to know the gory details of the IDL language; however, to better understand the interoperability layer, open OLE View (via Start ➤ All Programs ➤ Microsoft Visual Studio 6.0 ➤ Microsoft Visual Studio 6.0 Tools) and locate the SimpleComServer server under the Type Libraries node of the tree view control, as shown in Figure 17-11. Figure 17-9. Referencing COM type information from VB 6.0 Figure 17-10. Viewing type libraries using the VB 6.0 Object Browser 5785ch17.qxd 3/31/06 1:57 PM Page 503 CHAPTER 17 ■ COM AND .NET INTEROPERABILITY504 If you were to double-click the type library icon, you would open a new window that shows you all of the IDL tokens that constitute the type library generated by the VB 6.0 compiler. Here is the relevant—and slightly reformatted—IDL (your [uuid] values will differ): [uuid(8AED93CB-7832-4699-A2FC-CAE08693E720), version(1.0)] library SimpleComServer { importlib("stdole2.tlb"); interface _ComCalc; [odl, uuid(5844CD28-2075-4E77-B619-9B65AA0761A3), version(1.0), hidden, dual, nonextensible, oleautomation] interface _ComCalc : IDispatch { [id(0x60030000)] HRESULT Add([in] short x, [in] short y, [out, retval] short* ); [id(0x60030001)] HRESULT Subtract([in] short x, [in] short y, [out, retval] short* ); }; [uuid(012B1485-6834-47FF-8E53-3090FE85050C), version(1.0)] coclass ComCalc { [default] interface _ComCalc; }; }; IDL Attributes To begin parsing out this IDL, notice that IDL syntax contains blocks of code placed in square brackets ([ ]). Within these brackets is a comma-delimited set of IDL keywords, which are used to disambiguate the “very next thing” (the item to the right of the block or the item directly below the block). These blocks are IDL attributes that serve the same purpose as .NET attributes (i.e., they describe something). One key IDL attribute is [uuid], which is used to assign the globally unique identifier (GUID) of a given COM type. As you may already know, just about everything in COM is assigned a GUID (interfaces, COM classes, type libraries, and so on), which is used to uniquely identify a given item. Figure 17-11. Hunting down SimpleComServer using the OLE/COM object viewer 5785ch17.qxd 3/31/06 1:57 PM Page 504 CHAPTER 17 ■ COM AND .NET INTEROPERABILITY 505 The IDL Library Statement Starting at the top, you have the COM “library statement,” which is marked using the IDL library keyword. Contained within the library statement are each and every interface and COM class, and any enumeration (through the VB 6.0 Enum keyword) and user-defined type (through the VB 6.0 Type keyword). In the case of SimpleComServer, the type library lists exactly one COM class, ComCalc, which is marked using the coclass (i.e., COM class) keyword. The Role of the [default] Interface According to the laws of COM, the only possible way in which a COM client can communicate with a COM class is to use an interface reference (not an object reference). If you have created C++-based COM clients, you are well aware of the process of querying for a given interface, releasing the inter- face when it is no longer used, and so forth. However, when you make use of VB 6.0 to build COM clients, you receive a default interface on the COM class automatically. When you build VB 6.0 COM servers, any public member on a *.cls file (such as your Add() function) is placed onto the “default interface” of the COM class. Now, if you examine the class definition of ComCalc, you can see that the name of the default interface is _ComCalc: [uuid(012B1485-6834-47FF-8E53-3090FE85050C), version(1.0)] coclass ComCalc { [default] interface _ComCalc; }; In case you are wondering, the name of the default interface VB 6.0 constructs in the back- ground is always _NameOfTheClass (the underscore is a naming convention used to specify a hidden interface, the very interface the VS 2005 Object Browser did not show by default). Thus, if you have a class named Car, the default interface is _Car, a class named DataConnector has a default interface named _DataConnector, and so forth. Under VB 6.0, the default interface is completely hidden from view. However, when you write the following VB 6.0 code: ' VB 6.0 COM client code. Dim c As ComCalc Set c = New ComCalc ' [default] _ComCalc interface returned automatically! the VB runtime automatically queries the object for the default interface (as specified by the type library) and returns it to the client. Because VB always returns the default interface on a COM class, you can pretend that you have a true object reference. However, this is only a bit of syntactic sugar provided by VB 6.0. In COM, there is no such thing as a direct object reference. You always have an interface reference (even if it happens to be the default). The Role of IDispatch If you examine the IDL description of the default _ComCalc interface, you see that this interface derives from a standard COM interface named IDispatch. While a full discussion concerning the role of IDispatch is well outside of the scope of this chapter, simply understand that this is the interface that makes it possible to interact with COM objects on the Web from within a classic Active Server Page, as well as anywhere else where late binding is required. When you use VB proper (as opposed to VBScript), 99 percent of the time you want to avoid the use of IDispatch (it is slower, and errors are discovered at runtime rather than at compile time). However, just to illustrate, say you call the VB 6.0 CreateObject() method as follows: ' VB 6.0 late binding. Dim o As Object Set o = CreateObject("SimpleComServer.ComCalc") 5785ch17.qxd 3/31/06 1:57 PM Page 505 CHAPTER 17 ■ COM AND .NET INTEROPERABILITY506 You have actually instructed the VB runtime to query the COM type for the IDispatch interface. Note that calling CreateObject() alone does not trigger a query for IDispatch. In addition, you must store the return value in a VB 6.0 Object data type. IDL Parameter Attributes The final bit of IDL that you need to be aware of is how VB 6.0 parameters are expressed under the hood. As you know, under VB 6.0 all parameters are passed by reference, unless the ByVal keyword is used explicitly, which is represented using the IDL [in] attribute. Furthermore, a function’s return value is marked using the [out, retval] attributes. Thus, the following VB 6.0 function: ' VB 6.0 function Public Function Add(ByVal x as Integer, ByVal y as Integer) as Integer Add = x + y End Function would be expressed in IDL like so: HRESULT Add([in] short* x, [in] short* y, [out, retval] short* ); On the other hand, if you do not mark a parameter using the VB 6.0 ByVal keyword, ByRef is assumed: ' These parameters are passed ByRef under VB 6.0! Public Function Subtract(x As Integer, y As Integer) As Integer Subtract = x - y End Function ByRef parameters are marked in IDL via the [in, out] attributes: HRESULT Subtract([in, out] short x, [in, out] short y, [out, retval] short* ); Using a Type Library to Build an Interop Assembly To be sure, the VB 6.0 compiler generates many other IDL attributes under the hood, and you see additional bits and pieces where appropriate. However, at this point, I am sure you are wondering exactly why I spent the last several pages describing the COM IDL. The reason is simple: when you add a reference to a COM server using Visual Studio 2005, the IDE reads the type library to build the corresponding interop assembly. While VS 2005 does a very good job of generating an interop assem- bly, the Add Reference dialog box follows a default set of rules regarding how the interop assembly will be constructed and does not allow you to fine-tune this construction. If you require a greater level of flexibility, you have the option of generating interop assemblies at the command prompt, using a .NET tool named tlbimp.exe (the type library importer utility). Among other things, tlbimp.exe allows you to control the name of the .NET namespace that will contain the types and the name of the output file. Furthermore, if you wish to assign a strong name to your interop assembly in order to deploy it to the GAC, tlbimp.exe provides the /keyfile flag to specify the *.snk file (see Chapter 13 for details regarding strong names). To view all of your options, simply type tlbimp at a Visual Studio 2005 command prompt and hit the Enter key, as shown in Figure 17-12. 5785ch17.qxd 3/31/06 1:57 PM Page 506 [...]... type using the [default] interface of the CoCar and interact with some common programming constructs (enums and COM arrays) Go ahead and compile your sever (setting binary compatibility, once finished), and then close down your current VB 6. 0 workspace ■ Source Code The Vb6 ComCarServer project is included under the Chapter 17 subdirectory Examining the Interop Assembly Rather than making use of the tlbimp.exe... implements the outbound interface in a NET-aware sink object As you would hope, the VB 2005 language does not require you to make direct use of these types Rather, you are able to handle the incoming COM events in the same way you handle events based on the NET delegation architecture Simply declare the COM type WithEvents, and use the Handles keyword to map the event to a given method (or make use of the. .. note, click the Show All Files button on the Solution Explorer and open up the assemblyInfo .vb file located under the My Project icon By default, all Visual Studio 2005 project workspaces are provided with an assembly-level attribute used to identify the GUID of the type library generated based on the NET server (if exposed to COM) ' The following GUID is for the ID of the typelib if this project... "COM GUIDs" ' These GUIDs provide the COM identity for this class ' and its COM interfaces If you change them, existing ' clients will no longer be able to access the class Public Const ClassId As String = "ec2a6ec2-a681-41a1-a644-30c16c7409a9" Public Const InterfaceId As String = "ea905f17-5f7f-4958-b8c6-a95f419 063 a8" Public Const EventsId As String = "57c3d0e3-9e15-4b6a-a96e-b4c6736c7b6d" #End Region... Gets or sets the time the current file or directory was last accessed LastWriteTime Gets or sets the time when the current file or directory was last written to Name For files, gets the name of the file For directories, gets the name of the last directory in the hierarchy if a hierarchy exists Otherwise, the Name property gets the name of the directory The FileSystemInfo type also defines the Delete()... assembly, simply create a new console project (named VbNetCarClient) using Visual Studio 2005 and set a reference to the Vb6 ComCarServer.dll using the COM tab of the Add Reference dialog box Now, examine the interop assembly using the VS 2005 Object Browser utility, as shown in Figure 17-13 Figure 17-13 The Interop.VbComCarServer.dll assembly Assuming you have configured the Object Browser to show hidden... assuming that the CCW is a real COM type and thus must abide by the rules of AddRef() and Release() When the COM client has issued the final release, the CCW releases its reference to the real NET type, at which point it is ready to be garbage collected The CCW implements a number of COM interfaces automatically to further the illusion that the proxy represents a genuine coclass In addition to the set of... with the COM runtime, you can build some COM clients You can create a simple VB 6. 0 Standard *.exe project type (named VB6 _DotNetClient) and set a reference to the new generated type library (see Figure 17-20) Figure 17-20 Referencing your NET server from VB 6. 0 As for the GUI front end, keep things really simple A single Button object will be used to manipulate the DotNetCalc NET type Here is the code... shared members The closely related FileInfo and DirectoryInfo types expose similar functionality as instance-level methods (and therefore these types must be instantiated with the VB 2005 New keyword) In Figure 18-1, notice that the Directory and File types directly extend System.Object, while DirectoryInfo and FileInfo derive from the abstract FileSystemInfo type Figure 18-1 The File- and Directory-centric... to allow the COM client to interact with the public types 3 Deploy the assembly in the same directory as the COM client or (more typically) install it into the GAC As you will see, these steps can be performed using Visual Studio 2005 or at the command line using various tools that ship with the NET Framework 2.0 SDK The Attributes of System.Runtime.InteropServices In addition to performing these steps, . 3/31 / 06 1:57 PM Page 5 02 CHAPTER 17 ■ COM AND .NET INTEROPERABILITY 503 Likewise, when you open the VB 6 .0 Object Browser, the VB 6 .0 IDE reads the type information and displays the contents of the. { [id(0x 60 0 300 00) ] HRESULT Add([in] short x, [in] short y, [out, retval] short* ); [id(0x 60 0 300 01)] HRESULT Subtract([in] short x, [in] short y, [out, retval] short* ); }; [uuid (01 2B1485 -68 34-47FF-8E53- 309 0FE8 505 0C),. tlbimp at a Visual Studio 20 05 command prompt and hit the Enter key, as shown in Figure 17- 12. 5785ch17.qxd 3/31 / 06 1:57 PM Page 5 06 CHAPTER 17 ■ COM AND .NET INTEROPERABILITY 507 While this tool has