Exposing a Managed Class Library as a COM Component

Exposing a Managed Class Library as a COM Component

The principles of COM Interop work in both directions: Just as new managed code can access a COM component through a Runtime-Callable Wrapper, so unmanaged code can access a .NET component through a COM-Callable Wrapper, or CCW. However, it can't access any and all .NET codethere are conditions the .NET object must meet to be exposed as a COM component:

  • The library must contain at least one interface, and a class that implements that interface.

  • You must generate a type library from the library that COM can use.

  • You should put the library into the Global Assembly Cache, which imposes some more requirements on you, most notably that the library have a strong name .

  • Information to enable COM to find the component must be added to the Registry.

In this section, you learn how to modify a library so that it meets all of these conditions. The library in question is the sample from Chapter 6, "Writing a Class Library in Managed C++." You can make a copy or work with your original; the changes required here won't affect how managed applications interact with the managed library.

Adding Interfaces to the Library

The ManagedMathLibrary namespace contains two classes: DLL , which exposes static methods to initialize the C runtime libraries, and Arithmetic , which has the familiar Add() , Subtract() , Multiply() , and Divide() methods. The corresponding interfaces could have any names , but IDll and IArithmetic are logical choices.

To ManagedMathLibrary.h, add the two interface definitions, each just before the corresponding class. Make sure they are inside the namespace definition. Each interface definition just lists the methods that are already in the class. Add these lines to define IDll :

 
 public __gc __interface IDll {       void Initialize();       void Cleanup(); }; 

THE __INTERFACE KEYWORD

The __interface keyword defines a .NET interface. This is a class containing only public, pure virtual methods. Don't forget the semicolon after the closing brace , just as for a class declaration.


Because an interface cannot contain any static methods, change the DLL member functions, removing the static keyword. The calling code will need to create an instance of the class in order to initialize the libraries. Edit the companion test harness, ManagedHarness , accordingly . Find this line:

 
 ManagedMathLibrary::DLL::Initialize(); 

Replace it with these:

 
 ManagedMathLibrary::DLL* dll = new ManagedMathLibrary::DLL(); dll->Initialize(); 

Find this line:

 
 ManagedMathLibrary::DLL::Cleanup(); 

Replace it with this line:

 
 dll->Cleanup(); 

Add these lines to define IArithmetic :

 
 public __gc __interface IArithmetic {       System::Double Add(System::Double num1, System::Double num2);       System::Double Subtract(System::Double num1, System::Double num2);       System::Double Divide(System::Double num1, System::Double num2);       System::Double Multiply(System::Double num1, System::Double num2); }; 

Change each of the class declarations to indicate that it implements the corresponding interface:

 
 public __gc class DLL: public IDll public __gc class Arithmetic : public IArithmetic 

There's no need to make any changes to the implementation of the class or the methodsthe interfaces only list methods that were already implemented in the classes anyway.

Generating a Type Library

A type library is a file that holds all the information the COM runtime needs about a particular COM component: the interfaces and their methods, the parameters and return types, and so on. A generic COM-Callable Wrapper, mscoree.dll, and this type library are the keys to making the .NET component available to COM.

Build the revised library, and then open a Visual Studio .NET 2003 command prompt and change directories to the location of your freshly built library. Enter this command:

 
 tlbexp ManagedMathLibrary.dll 

A file called ManagedMathLibrary.tlb appears in the directory. Keep the command prompt open; there are more command-line utilities involved in meeting all the conditions for .NET Interop.

Adding the Library to the GAC

One of the appeals of COM was the universality of a component: As long as a Registry entry existed to tell the COM runtime where the component was, the programmer using the component didn't need to know where it was, or make a copy in a local directory. The .NET equivalent is the Global Assembly Cache, or GAC. Although it is possible to access a .NET component from a COM application without putting the .NET component in the GAC, it's not recommended. Not only does it make deploying the combined application more difficult, it can lead to some very strange errors when you make seemingly small changes to your calling application.

Putting an assembly into the GAC is straightforward, but there are some prerequisites: All assemblies in the GAC must have a strong name . A strong name pulls together a simple name (such as ManagedMathLibrary ) with a version number, localization information, a public key provided by the developer, and a digital signature. Two assemblies may have the same simple name, but be distinguished within the GAC if one of the other components of their strong names are different.

At the same Visual Studio .NET 2003 command prompt you used earlier, change directories up to the project folder and issue this command:

 
 sn -k mathlibrary.snk 

It actually doesn't matter what you name your key fileeven what extension you usebut it helps other developers if you use the.snk extension.

Back in Visual Studio, open the file AssemblyInfo.cpp for the ManagedMathLibrary project. At the bottom of the file, you'll find this line:

 
 [assembly:AssemblyKeyFileAttribute("")]; 

Edit the line, inserting the name of the key file you created with the sn utility between the quotes. Build the project. If you get an error about Visual Studio being unable to open the key file, try closing Visual Studio and then opening the solution again.

Finally, add the signed assembly to the GAC. Using the command prompt, change directories to the Debug or Release folder (whichever you just built) and issue this command:

 
 gacutil -i managedmathlibrary.dll 

To check that you have successfully added the assembly to the GAC, browse to the Assembly folder under C:\Windows or C:\Winnt (depending on your Windows version) and you will see the GAC itself. Make sure that ManagedMathLibrary is in the GAC.

Creating Registry Entries

The final step in making this .NET component available to COM is adding an entry to the Registry that points to the generic CCW, mscoree.dll. In the same command prompt you've been using all along, enter this command:

 
 regasm managedmathlibrary.dll 

Interestingly, this doesn't record the location of the assembly in the Registry. It points the COM runtime at mscoree , the "execution engine" of the .NET runtime (the original working name was Common Object Runtime, hence the COR in the DLL name) that is used by all .NET assemblies being accessed from COM. The loader looks first in the GAC, and then in the executable path for the application that is using the assembly, and then in a Codebase Registry entry if you used one. By default, regasm doesn't store the codebase information, and because this sample assembly is in the GAC, there's no need tothe assembly is waiting right where the loader will look for it first.

Testing the Component

A console application is the simplest test harness for this component. Create a Win32 Console application called Interop. Copy the generated type library, ManagedMathLibrary.tlb, to the project folder.

Add this code for the main() function:

 
 int _tmain(int argc, _TCHAR* argv[]) {     ::CoInitialize(NULL);     ManagedMathLibrary::IDllPtr dll("ManagedMathLibrary.DLL");     dll->Initialize();     ManagedMathLibrary::IArithmeticPtr a("ManagedMathLibrary.Arithmetic");     cout << "2.3 + 4.5 is " <<  a->Add(2.3, 4.5) << endl;     dll->Cleanup();     ::CoUninitialize();    return 0; } 

This code initializes COM with CoInitialize() , and later cleans it up with CoUninitialize() . It uses the Initialize() and Cleanup() methods provided by the .NET component to ensure that the CRT is initialized and cleaned up. Between the initialization and the cleanup, it uses the Add() method of the Arithmetic class. The code is made much simpler by using smart pointers that are generated by a #import statement, like the COM client shown earlier in this chapter, PhoneTest .

The #import statement in this client cannot import a DLL; instead it imports the type library, managedmathlibrary.tlb, generated by the tlbexp utility. There is a small problem importing this particular library, and it's worth exploring here in case you meet a similar situation in libraries of your own.

Add these lines to the top of the file so that the code that writes to cout will compile:

 
 #include <iostream> using namespace std; 

Add this line after those, but before the start of the main() function:

 
 #import "ManagedMathLibrary.tlb" 

Build the project, and you'll see a really strange error:

 
[View full width]
 
[View full width]
fatal error C1196: 'allocator<void>' : identifier found in type library graphics/ccc.gif 'ManagedMathLibrary.tlb' is not a valid C++ identifier

This error is strange for two reasons: First, a template declaration is a valid C++ identifier, and second, what is a template declaration doing in your type library? The second one is easiest to answer. The tlbexp utility exports all the types defined in the .NET assembly. That assembly, written in managed C++, used IJW to access methods of the CRT, and included the <iostream> header. That header in turn included other headers, and all sorts of typedefs , structs, and classes were defined within the assembly. The tlbexp utility exported them all. You can confirm this in a command prompt by re-issuing the tlbexp command with the verbose option:

 
 tlbexp ManagedMathLibrary.dll /verbose 

This produces output like this:

 
 Microsoft (R) .NET Framework Assembly to Type Library Converter 1.1.4322.573 Copyright (C) Microsoft Corporation 1998-2002.  All rights reserved. Type bad_exception exported. Type bad_alloc exported. Type _iobuf exported. Type _Scalar_ptr_iterator_tag exported. Type allocator<void> exported. Type basic_string<char,std::char_traits<char>,std::allocator<char> > exported. Type logic_error exported. Type domain_error exported. Type invalid_argument exported. Type length_error exported. Type out_of_range exported. Type runtime_error exported. Type overflow_error exported. Type underflow_error exported. Type range_error exported. Type _DebugHeapString exported. Type _Timevec exported. Type _Locinfo exported. Type _Collvec exported. Type _Ctypevec exported. Type _Cvtvec exported. Type locale exported. Type id exported. Type _Lockit exported. Type facet exported. Type _Locimp exported. Type codecvt_base exported. Type codecvt<unsigned short,char,int> exported. Type ctype_base exported. Type ctype<char> exported. Type ctype<unsigned short> exported. Type ios_base exported. Type failure exported. Type _Iosarray exported. Type _Fnarray exported. Type basic_ostream<unsigned short,std::char_traits<unsigned short> > exported. Type sentry exported. Type basic_ostream<char,std::char_traits<char> > exported. Type basic_istream<unsigned short,std::char_traits<unsigned short> > exported. Type sentry exported. Type basic_istream<char,std::char_traits<char> > exported. Type sentry exported. Type _GUID exported. Type tagPROPVARIANT exported. Type tagVARIANT exported.  Type IDll exported.   Type DLL exported.   Type IArithmetic exported.   Type Arithmetic exported.   Type Simple exported.  Type allocator<char> exported. Type _DebugHeapAllocator<char> exported. Type basic_string<char,std::char_traits<char>,std::_DebugHeapAllocator<char> > exported. Type basic_ios<unsigned short,std::char_traits<unsigned short> > exported. Type basic_streambuf<unsigned short,std::char_traits<unsigned short> > exported Type fpos<int> exported. Type ostreambuf_iterator<char,std::char_traits<char> > exported. Type sentry exported. Type num_put<char,std::ostreambuf_iterator<char,std::char_traits<char> > > exported. Type basic_ios<char,std::char_traits<char> > exported. Type _Sentry_base exported. Type _Sentry_base exported. Type basic_streambuf<char,std::char_traits<char> > exported. Type _String_val<char,std::allocator<char> > exported. Type _String_val<char,std::_DebugHeapAllocator<char> > exported. Type _Sentry_base exported. Type numpunct<char> exported. Type _Sentry_base exported. Type bad_cast exported. Type lconv exported. Type exception exported. Type _s__CatchableTypeArray exported. Type $_s__CatchableTypeArray$_extraBytes_8 exported. Type _s__CatchableType exported. Type _TypeDescriptor exported. Type $_TypeDescriptor$_extraBytes_16 exported. Type $_TypeDescriptor$_extraBytes_15 exported. Type _s__ThrowInfo exported. Type _Mutex exported. Type _String_base exported. Type _IMAGE_DOS_HEADER exported. Type _DebugHeapTag_t exported.  Type Multiple exported.  Assembly exported to C:\ManagedMathLibrary\Debug\ManagedMathLibrary.tlb 

Of all these types, only the ones displayed in bold are actually public types defined by your own code. The rest come from headers that were included to make your code work. This would be harmless if none of them were templated types. The presence of these templated types causes problems when you bring in the type library with #import .

The solution is to use the exclude attribute on the #import directive. This is a little tedious , but it solves the problem. When the build fails, it complains about allocator<void> , so change the import directive to read like this:

 
 #import "ManagedMathLibrary.tlb"  exclude("allocator<void>") 

Build again and you'll get the same error message about a different type. Keep adding entries to the comma-separated list inside the parentheses (use the \ character to spread your #import directive over several lines) until the compiler errors stop. The final statement should resemble this one:

 
 #import "ManagedMathLibrary.tlb"  exclude("allocator<void>", \    "basic_string<char,std::char_traits<char>,std::allocator<char> >", \    "codecvt<unsigned short,char,int>", \    "ctype<char>", \    "ctype<unsigned short>", \    "basic_ostream<unsigned short,std::char_traits<unsigned short> >", \    "basic_ostream<char,std::char_traits<char> >", \    "basic_istream<unsigned short,std::char_traits<unsigned short> >", \    "basic_istream<char,std::char_traits<char> >", \    "allocator<char>", \    "_DebugHeapAllocator<char>", \  "basic_string<char,std::char_traits<char>,std::_DebugHeapAllocator<char> >", \    "basic_ios<unsigned short,std::char_traits<unsigned short> >", \    "basic_streambuf<unsigned short,std::char_traits<unsigned short> >", \    "fpos<int>", \    "basic_ios<char,std::char_traits<char> >", \    "basic_streambuf<char,std::char_traits<char> >", \    "_String_val<char,std::allocator<char> >", \    "_String_val<char,std::_DebugHeapAllocator<char> >", \     "ostreambuf_iterator<char,std::char_traits<char> >", \     "num_put<char,std::ostreambuf_iterator<char,std::char_traits<char> > >", \     "numpunct<char>") 

You can speed things up by using the type names from the tlbexp verbose report, if you want. When you build a different project using these techniques, you might need to exclude different templated types, depending on the headers included by the managed project.

The project should now build without errors (ignore any warnings about duplicate types) and run successfully. You are likely to have to go through this same procedure whenever you use a CCW to access a .NET component that uses unmanaged code (such as the CRT). It's tedious, but you only have to do it once.



Microsoft Visual C++. NET 2003 Kick Start
Microsoft Visual C++ .NET 2003 Kick Start
ISBN: 0672326000
EAN: 2147483647
Year: 2002
Pages: 141
Authors: Kate Gregory

Similar book on Amazon

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