Calling COM Components from Managed Code

for RuBoard

Calling COM Components from Managed Code

The first interoperability scenario we will look at is managed code calling COM components. The .NET Framework makes it easy to create a Runtime Callable Wrapper (RCW), which acts as a bridge between managed and unmanaged code. The RCW is illustrated in Figure 14-1.

Figure 14-1. A Runtime Callable Wrapper between managed and unmanaged code.

graphics/14fig01.gif

You could implement an RCW assembly yourself, using the PInvoke facility (described in a later section) to call into the necessary APIs, such as CoCreateInstance and the IUnknown methods directly. But that is not necessary, because the Tlbimp.exe tool can read type library information, and automatically generate the appropriate RCW for you. Visual Studio.NET makes it even easier when you add a reference to a COM object in Solution Explorer. We will examine both of these facilities, as we look at some examples of COM components and .NET clients .

The Tlbimp.exe Utility

The Tlbimp.exe utility (Type Library to .NET Assembly Converter) program is provided in the \Program Files\Microsoft.NET\FrameworkSDK\Bin directory. It is used to generate managed classes that wrap unmanaged COM classes. The resulting RCW is a .NET component (i.e., a managed DLL assembly) that managed client code can use to access the COM interface methods that are implemented in the COM component. The Tlbimp tool is a command line program that reads COM type library information, and generates a managed wrapper class along with the associated metadata, and places the result into the RCW assembly. You can view the resulting contents in this assembly using the Ildasm tool. The command line syntax for Tlbimp is shown below.

 Tlbimp TypeLibName [options]  Where options may contain the following:      /out:FileName            Assembly file name      /namespace:Namespace     Assembly Namespace      /asmversion:Version      Assembly version number      /reference:FileName      Reference assembly      /publickey:FileName      Public key file      /keyfile:FileName        Key pair file      /keycontainer:FileName   Key pair key container      /delaysign               Delay digital signing      /unsafe                  Suppress security checks      /nologo                  Suppress displaying logo      /silent                  Suppress output except errors      /verbose                 Display extra information      /primary                 Make primary interop assembly      /sysarray                SAFEARRAY as System.Array      /strictref               Only /reference assemblies      /? or /help              Display help information 

When the Tlbimp tool imports a COM type library, it creates a .NET namespace with the same name as the library defined in the type library (that is the name of the actual library, not the name of the type library file that contains it). Tlbimp converts each COM coclass defined in the type library into a managed .NET wrapper class in the resulting .NET assembly that has one constructor with no parameters. Each COM interface defined in the type library is converted into a .NET interface in the resulting .NET assembly.

Consider the typical COM IDL file library statement shown below that would be used to create a type library using Midl.exe . The resulting type library (TLB) or DLL file would cause Tlbimp.exe to generate an assembly containing metadata, including the namespace BANKDUALLib , a managed wrapper class named Account2 , and a managed interface named IAccount2 .

 library BANKDUALLib  {     importlib("stdole32.tlb");     importlib("stdole2.tlb");     [        uuid(04519632-39C5-4A7E-AA3C-3A7D814AC91C),        helpstring("Account2 Class")     ]     coclass Account2     {        [default] interface IAccount2;     };  }; 

Once you have used Tlbimp.exe to generate the wrapper assembly, you can view its contents using the Ildasm tool, as shown in Figure 14-2. Note that the namespace shown by Ildasm.exe is BANKDUALLib , the name of the interface is IAccount2 , and the wrapper class is named Account2.

Figure 14-2. Ildasm.exe showing contents of a COM wrapper assembly.

graphics/14fig02.gif

Demonstration: Wrapping a Legacy COM Server

The best way to get a feel for how this wrapping process works is to perform the operations yourself. The .NET client program is in the directory NetClient . The directory LegacyComServer contains the following files:

 BankDual.dll         COM server DLL  BankDual.tlb         Type library  reg_bankdual.bat     Batch file to register the server  unreg_bankdual.bat   Batch file to unregister the server  BankConsole.exe      Client executable file 

The source code for the client and server are in the directories ClientSource and ServerSource respectively. Both programs are written in Visual C++, and project files are provided for Visual C++ 6.0. Unless you have Visual C++ 6.0 installed on your system in addition to Visual Studio.NET, you will not be able to build these projects, but that will not prevent you from running the program and creating an .NET client.

This COM server implements a simple bank account class that has Deposit and Withdraw methods and a Balance property. The simple code [2] is shown in Account2.cpp in the ServerSource directory.

[2] We will not discuss the somewhat intricate infrastructure code provided by this ATLbased COM server. Such "plumbing" is much easier with .NET. Our focus is on calling COM components, not implementing them.

 STDMETHODIMP CAccount2::get_Balance(long *pVal)  {        *pVal = m_nBalance;        return S_OK;  }  STDMETHODIMP CAccount2::Deposit(long amount)  {        m_nBalance += amount;        return S_OK;  }  STDMETHODIMP CAccount2::Withdraw(long amount)  {        m_nBalance -= amount;        return S_OK;  } 
Register the COM Server

The first step is to register the COM server. You can do that by running the batch file reg_bankdual.bat , which executes the command,

 regsvr32 bankdual.dll 

You can now see the registration entries using the Registry Editor ( regedit.exe ) or the OLE/COM Object Viewer ( oleview.exe ). The latter program is provided on the Tools menu of Visual Studio.NET. It groups related registry entries together, providing a convenient display. You can also perform other operations, such as instantiating objects. Figure 14-3 shows the entries for the Account2 class that is implemented by this server. We have clicked the little "+" in the left-hand pane, which instantiates an object and queries for the standard interfaces. You can release the object by right-clicking over the class and choosing Release Instance from the context menu.

Figure 14-3. OLE/COM Object Viewer showing registry entries.

graphics/14fig03.gif

Run the COM Client

You can now run the COM client by double-clicking on BankConsole.exe in Windows Explorer. The starting balance is shown, followed by a withdrawal of 25, and the balance is shown again. Here is the source code, in the file BankConsole.cpp in ClientSource:

 // BankConsole.cpp  #include <stdio.h>  #include <stdlib.h>  #include <objbase.h>  #include "bankdual.h"  #include "bankdual_i.c"  IAccount2* g_pAccount;  void ShowBalance()  {          long balance;         HRESULT hr = g_pAccount->get_Balance(&balance);         printf("balance = %d\n", balance);  }  int main(int argc, char* argv[])  {         // Initialize COM         HRESULT hr = CoInitializeEx(NULL,            COINIT_APARTMENTTHREADED);         // Instantiate Account object, obtaining interface         // pointer         hr = CoCreateInstance(CLSID_Account2, NULL,         CLSCTX_SERVER, IID_IAccount2, (void **) &g_pAccount);         // First obtain and display initial balance         ShowBalance();         // Deposit 25 and show balance         hr = g_pAccount->Deposit(25);         ShowBalance();         // Clean up         g_pAccount->Release();         CoUninitialize();         printf("Press enter to quit: ");         char buf[10];         gets(buf);         return 0;  } 

For simplicity, no error checking is done. Robust code should check the HRESULT that is returned from each of the COM calls. Here is the output from running the client program:

 balance = 150  balance = 125  Press Enter to quit: 
Import the Type Library (TlbImp.exe)

In order to call the COM component from managed code, we must create an RCW. We can do that by running the TlbImp.exe utility that we have discussed. We will run this utility from the command line, in the directory NetClient , where we want the RCW assembly to wind up. We provide a relative path to the type library file [3] BankDual.tlb in the directory LegacyComServer . What we have to type is shown in bold.

[3] The file BankDual.dll also contains the type library and could have been used in place of BankDual.tlb .

  tlbimp ..\legacycomserver\bankdual.tlb  TlbImp - Type Library to .NET Assembly Converter Version  1.0.2914.16  Copyright (C) Microsoft Corp. 2001.  All rights reserved.  Type library imported to BANKDUALLib.dll 

The RCW assembly that is created is BANKDUALLib.dll , taking its name from the name of the type library, as discussed earlier.

Implement the .NET Client Program

It is now easy to implement the .NET client program. The code is in the file NetClient.cs in the directory NetClient .

 // NetClient.cs  using System;  using BANKDUALLib;  class NetClient  {     public static void Main()     {  Account2 acc;   acc = new Account2();  Console.WriteLine("balance = {0}",  acc.Balance  );  acc.Withdraw(25);  Console.WriteLine("balance = {0}",  acc.Balance  );     }  } 

As with the COM client program, for simplicity we do no error checking. In the .NET version we should use exception handling to check for errors. The RCW uses the namespace BANKDUALLib , based on the name of the type library.

You must add a reference to BANKDUALLib.dll . In the Visual Studio Solution Explorer you can right-click over References, choose "Add Reference," and use the ordinary .NET tab of the Add Reference dialog.

Build and run the project inside of Visual Studio. You should see the following output:

 balance = 150  balance = 125  Press any key to continue 

Once you have added a reference to a RCW, you have all the features of the IDE available for .NET assemblies, including Intellisense and the Object Browser. You can bring up the Object Browser from View Other Windows Object Browser. Figure 14-4 illustrates the information shown.

Figure 14-4. Object Browser showing information about the RCW.

graphics/14fig04.gif

Import a Type Library Using Visual Studio

When you are using Visual Studio you can import a COM type library directly, without first running TlbImp.exe . To see how to do this, use Solution Explorer to delete the reference to BANKDUALLib.dll . In fact, delete the file itself, and delete the bin and obj directories of NetClient . Now right-click over References, choose "Add Reference," and this time select the COM tab from the Add Reference dialog. The listbox will show all the COM components with a registered type library. Select "BankDual 1.0 Type Library," as illustrated in Figure 14-5.

Figure 14-5. Add a reference to a COM component in Visual Studio.

graphics/14fig05.gif

Now click OK. You will see a message telling you that a "primary interop assembly" is not registered for this type library. You will be invited to have a wrapper generated for you, as illustrated in Figure 14-6. Click "Yes." The generated RCW is the file Interop.BANKDUALLib_1_0.dll in the directory bin\Debug . You should be able to build and run the .NET client program.

Figure 14-6. Visual Studio will create a primary interop assembly.

graphics/14fig06.gif

The primary interop assembly that was created by Visual Studio is normally created by the publisher of the COM component. This can be done using the TlbImp.exe utility with the /primary option.

Wrapping a COM Component with a Pure Vtable Interface

Dual Interfaces

Our example legacy COM component BankDual.dll had a dual interface IAccount2 . This means that the interface could be called by both an early-binding COM client using the vtable and also by a late-binding client using IDispatch . The IDL file BankDual.idl specifies the interface IAccount2 as dual.

 [     object,     uuid(AAA19CDE-C091-47BF-8C96-C80A00989796),  dual,  helpstring("IAccount2 Interface"),     pointer_default(unique)  ]  interface IAccount2  : IDispatch  {     [propget, id(1), helpstring("property Balance")] HRESULT  Balance([out, retval] long *pVal);     [id(2), helpstring("method Deposit")] HRESULT  Deposit([in] long amount);     [id(3), helpstring("method Withdraw")] HRESULT  Withdraw([in] long amount);  }; 

An example of late-binding is VBSCript code for client-side scripting on a Web page. The directory BankHtml contains the file Bank.htm with an HTML form and VBScript code to exercise our bank account server.

 <!-- bank.htm -->  <HTML>  <HEAD>  <TITLE>Bank test page for Account object</TITLE>  <SCRIPT LANGUAGE="VBScript">  <!-- dim account  Sub btnCreate_OnClick  set account = createobject("BankDual.Account2.1")  Document.Form1.txtAmount.Value = 25        Document.Form1.txtBalance.Value = account.Balance  End Sub  Sub btnDestroy_OnClick        set account = Nothing        Document.Form1.txtAmount.Value = ""         Document.Form1.txtBalance.Value = ""  End Sub  Sub btnDeposit_OnClick        account.Deposit(Document.Form1.txtAmount.Value)        Document.Form1.txtBalance.Value = account.Balance  End Sub  Sub btnWithdraw_OnClick        account.Withdraw(Document.Form1.txtAmount.Value)        Document.Form1.txtBalance.Value = account.Balance  End Sub  -->  </SCRIPT>  <FORM NAME = "Form1" >  Amount <INPUT NAME="txtAmount" VALUE="" SIZE=8>  <P>  Balance <INPUT NAME="txtBalance" VALUE="" SIZE=8>  <P>  <INPUT NAME="btnCreate" TYPE=BUTTON VALUE="Create">   <INPUT NAME="btnDestroy" TYPE=BUTTON VALUE="Destroy">   <INPUT NAME="btnDeposit" TYPE=BUTTON VALUE="Deposit">   <INPUT NAME="btnWithdraw" TYPE=BUTTON VALUE="Withdraw">  </FORM>  </BODY>  </HTML> 

The createobject function instantiates a COM object using late binding, referencing a program ID rather than a CLSID. This is perfectly legitimate , because BankDual.dll implements a dual interface on the Account2 object. Since this is client-side script, we can exercise it locally in Internet Explorer, simply double-clicking on bank.htm in Windows Explorer. This will bring up Internet Explorer and show the form. You can click the Create button and instantiate an object, [4] as shown in Figure 14-7. The starting balance of 150 is shown. You can then exercise Deposit and Withdraw, and when you are done, you can click Destroy.

[4] Depending on your security settings, you may get a warning message about an ActiveX control on the page. Click Yes to allow the interaction. If you have trouble running the ActiveX control at all, check your security settings in Internet Explorer.

Figure 14-7. Accessing a late-bound COM object in Internet Explorer.

graphics/14fig07.gif

Pure Vtable Interface

Dual interfaces are very common. The default in an ATL wizard generated COM component is dual interface. Visual Basic 6.0 also creates COM components with dual interfaces. However, if there is no occasion for a COM component to be called by a late-binding client, it is more efficient to implement only a pure vtable interface.

There is a slight issue in generating wrappers for COM components with a pure vtable interface. To see the problem, consider the COM component in VtableComServer . As with our LegacyComServer example, the top-level directory contains the DLL, the type library file, batch files to register and unregister the server, and a client test program. Source code for the COM server and client is provided in ServerSource and ClientSource respectively. We want to implement a managed client program VtableNetClient .

First, verify that the COM client and server work. All you have to do is run the batch file reg_bank.bat to register the server, and you can double-click on BankConsole.exe in Windows Explorer to run the client.

Next, open up the solution VtableNetClient.sln in Visual Studio. Add a reference to the COM type library "Bank 1.0 Type Library." You should get a clean build. But when you run the program, you get an exception:

 Unhandled Exception: System.InvalidCastException:  QueryInterface for interface BANKLib.IAccount failed.     at BANKLib.Account.GetBalance(Int32& pBalance)     at VtableNetClient.ShowBalance() in   C:\OI\NetCs\Chap14\VtableNetClient\Vtable  NetClient.cs:line 14     at VtableNetClient.Main() in  C:\OI\NetCs\Chap14\VtableNetClient\  VtableNetClient.cs:line 33 

The problem is that the .NET client is in a separate apartment, and it needs marshaling. You can use any of the following solutions:

  1. Mark the IDL for the interface as dual.

  2. Mark the IDL for the interface as oleautomation , and limit types used to oleautomation friendly types.

  3. Build and register the proxy/stub DLL for the interface.

  4. Mark the Main method in the C# client with the [STAThread] or [MTAThread] attribute (appropriate to the situation), to place it into the same threading model as the COM server.

Examining the source code for VtableNetClient.cs , we see that we commented out the attribute [STAThread] in front of Main . Uncomment, build, and run again. This time it should work!

As an alternate solution, comment out [STAThread] again. Now in the server directory VtableComServer run the batch file reg_bankps.bat to register the proxy/stub DLL. Build and run the .NET client. Again, it should work!

Notice another feature of this .NET client program. Rather than calling methods on a class object, we go through interface references. We obtain the interface references using the C# as operator, as we discussed in Chapter 5. This use of the as operator is the analog in .NET of QueryInterface in COM.

for RuBoard


Application Development Using C# and .NET
Application Development Using C# and .NET
ISBN: 013093383X
EAN: 2147483647
Year: 2001
Pages: 158

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net