Generating and Using COM Callable Wrappers

team lib

There are several steps involved in exporting a .NET component so that it can be used from COM:

  1. Add any necessary attributes to the .NET code.

  2. Build the assembly.

  3. Optionally, create a type library.

  4. Sign it with a strong name .

  5. Provide the requisite COM- related registry entries.

Each of these steps is discussed in the sections that follow.

Using COM-Related Attributes

This section discusses how attributes are used to control the way in which .NET types are exposed as COM components .

The ClassInterface Attribute

The ClassInterface attribute governs how the class interface is generated for a type. The ClassInterface attribute takes a ClassInterfaceType as its one parameter. The ClassInterfaceType member used determines the type of class interface created. The members of the ClassInterfaceType enumeration and their associated class interfaces are listed in Table 4-3.

Table 4-3: Members of the ClassInterfaceType Enumeration

Member

Description

AutoDispatch

A pure dispatch interface will be used as the class interface. This type of interface supports only late binding from COM clients .

AutoDual

A dual interface will be used as the class interface. This type of interface can support both early and late binding from COM clients.

None

No class interface will be created when the class is exported.

The Auto prefix to AutoDispatch and AutoDual emphasizes that the runtime automatically generates the interface details.

When you generate a dual class interface by specifying ClassInterfaceType.AutoDual , a full interface definition is placed in the type library, complete with autogenerated dispIds. For example, consider this class in Visual Basic .NET:

 'AVB.NETclassthatinheritsfromSystem.Objectbydefault ImportsSystem.Runtime.InteropServices <ClassInterface(ClassInterfaceType.AutoDual)>_ PublicClassClass1 PublicFunctionSquare(ByValnAsInteger)AsLong Returnn*n EndFunction PublicSubSub1() EndSub EndClass 

The class interface generated for this class will contain all the class members:

 [ odl,uuid(FEDF4CC0-E0ED-3DC4-ABE7-E5B4BBC78D84), hidden,dual,nonextensible,oleautomation, custom(0F21F359-AB84-41E8-9A78-36D110E6D2F9,ExportVb.Class1) ] interface_Class1:IDispatch{ [id(00000000),propget, custom(54FC8F55-38DE-4703-9C4E-250351302B1C,1)] HRESULTToString([out,retval]BSTR*pRetVal); [id(0x60020001)] HRESULTEquals([in]VARIANTobj, [out,retval]VARIANT_BOOL*pRetVal); [id(0x60020002)] HRESULTGetHashCode([out,retval]long*pRetVal); [id(0x60020003)] HRESULTGetType([out,retval]_Type**pRetVal); [id(0x60020004)] HRESULTSquare([in]longn,[out,retval]int64*pRetVal); [id(0x60020005)] HRESULTSub1(); }; 

Note how the interface definition also includes all the members of the System.Object base class. Remember that including the dispIds in the type library might be problematical if the class layout changes and client code caches dispIds.

The coclass generated for Class1 uses the class interface as the default interface and also contains a class interface for the Object base class. As the following code shows, all coclasses that represent .NET types will contain the _Object interface because they all derive from System.Object :

 [ uuid(3FB70B8A-01EE-3728-AF80-802D1E3A2726), version(1.0), custom(0F21F359-AB84-41E8-9A78-36D110E6D2F9,ExportVb.Class1) ] coclassClass1{ [default]interface_Class1; interface_Object; }; 

ClassInterfaceType.AutoDispatch creates a dispinterface in the type library, but it omits the definitions of the interface methods . This means the interface can only be used late bound, using the GetIdsOfNames and Invoke methods on IDispatch to discover and invoke methods. As an example, if the ClassInterfaceType is changed to AutoDispatch on the Visual Basic .NET class in the previous example, the following class interface definition is generated:

 [ odl,uuid(FEDF4CC0-E0ED-3DC4-ABE7-E5B4BBC78D84), hidden,dual,oleautomation, custom(0F21F359-AB84-41E8-9A78-36D110E6D2F9,ExportVb.Class1) ] interface_Class1:IDispatch{ }; 

ClassInterfaceType.None prevents a class interface from being generated for a class. This is useful when all methods are exposed via implemented interfaces, in which case there is no requirement for an interface to represent the class itself. Consider the following Visual Basic .NET class, which exposes one function via an interface:

 PublicInterfaceIMyInterface FunctionSquare(ByValnAsInteger)AsLong EndInterface <ClassInterface(ClassInterfaceType.None)>_ PublicClassClass2 ImplementsIMyInterface PublicFunctionSquare(ByValnAsInteger)AsLong_ ImplementsIMyInterface.Square Returnn*n EndFunction EndClass 

The export process will create a coclass and interface definition as follows :

 [ odl,uuid(1C60158D-625D-3699-9F90-22653CF8F6F3), version(1.0),dual,oleautomation, custom(0F21F359-AB84-41E8-9A78-36D110E6D2F9,ExportVb.IMyInterface) ] interfaceIMyInterface:IDispatch{ [id(0x60020000)] HRESULTSquare([in]longn,[out,retval]int64*pRetVal); }; [ uuid(0D499842-1CF8-30A5-9504-8EBDA1E5C055), version(1.0), custom(0F21F359-AB84-41E8-9A78-36D110E6D2F9,ExportVb.Class2) ] coclassClass2{ interface_Object; [default]interfaceIMyInterface; }; 

Note how the coclass does not have a class interface, and how the coclasss default interface is now IMyInterface rather than the class interface.

The InterfaceType Attribute

The InterfaceType attribute controls how a .NET interface is exposed to COM as a dispinterface, a dual interface, or an IUnknown -derived custom interface. The use of this attribute is covered in the section Exporting Interfaces later in this chapter.

The Guid and ProgId Attributes

These two attributes can be used to override the generation of default GUID and progID values for exported .NET types. GUIDs will automatically be generated for classes and interfaces that are exposed to COM, unless you chose to specify a particular GUID using the Guid attribute, as shown in the following Visual Basic .NET code fragment:

 <Guid(f7291cfb-6b3a-40e7-a9a7-fc4c444a0a08)>_ PublicClassMyExportedClass ... EndClass 

A progID will also normally be generated, based on the namespace and typename, but there are restrictions on COM progIDs. For instance, they must be fewer than 40 characters long and not contain any punctuation other than periods. If the names of the namespaces and types you want to export would generate invalid progIDs, you can use the ProgId attribute to specify a valid alternative:

 <Guid(f7291cfb-6b3a-40e7-a9a7-fc4c444a0a08),ProgId("MyStuff.Exported")>_ PublicClassMyExportedClass ... EndClass 

The ComVisible Attribute

The ComVisible attribute is used to control the visibility of .NET assemblies, interfaces, classes, and class members when a type library is produced. By default, public classes and class members are exported to the type library. By using ComVisible , the type library exporter can be prevented from exporting selected symbols or classes, as shown in the following Visual Basic .NET example:

 'ThismethodwillnotbevisibletoCOMclients <ComVisible(False)>_ PublicSubSub1() ... EndSub 

Applying ComVisible(false) to an assembly hides all the types within that assembly. If you do this, you can then use ComVisible to make selected types within the assembly visible. Applying ComVisible(false) to a type hides all the members of that type, and you cannot selectively make members visible using ComVisible(true). Applying ComVisible(false) to an interface means that the type doesnt support it as far as COM is concerned (that is, QueryInterface calls for the interface will fail).

The AutomationProxy Attribute

Although many COM components will use a custom proxy/stub dynamic-link library (DLL) for marshaling, you can delegate this task to a built-in system marshaler known as the Automation marshaler or Universal marshaler . The AutomationProxy attribute can be applied to assemblies as well as classes and interfaces, and it governs whether the Automation marshaler is used. The default is for a custom proxy to be used, but by using AutomationProxy with a true argument, the Automation marshaler can be chosen :

 'ThisVisualBasic.NETclasswillusetheAutomationmarshaler <AutomationProxy(True)>_ PublicClassMyClass ... EndClass 

The IDispatchImpl Attribute

When youre exposing .NET types to COM using dual interfaces or dispinterfaces, something has to provide an implementation for the standard IDispatch interface methods. There are two alternatives: COM can be asked to supply one via the CreateStdDispatch API, or the runtime can use its own implementation. The internal implementation uses reflection where possible, so a type library is seldom required. Note that there are some minor differences in operation between the internal implementation and the COM implementation, which is designed for maximum compatibility with existing COM client code. As an example, the internal implementation will let you pass a signed integer where an unsigned one is expected, which the COM version will not do.

The default behavior is to use the internal implementation provided by the runtime, but if you find problems using late-bound Automation interfaces that you suspect are due to the IDispatch implementation, you can use this attribute to try using the COM IDispatch implementation.

By using the IDispatchImpl attribute, you can control which implementation is used by providing one of the following values as the parameter to IDispatchImpl :

  • IDispatchImplType.CompatibleImpl means that a COM-supplied implementation will be used. The runtime will pass the type information for the object to the CreateStdDispatch API.

  • IDispatchImplType.InternalImpl means that the runtimes own implementation will be used. This is the default.

  • IDispatchImplType.SystemDefinedImpl means that the runtime will choose which implementation will be used, and has the same effect as specifying InternalImpl .

Heres an example showing how to use this attribute with a Visual C# class:

 //ThisclasswillusetheCOMIDispatchimplementation [IDispatchImpl(IDispatchImplType.CompatibleImpl)] publicclassMyClass { ... } 

The ComRegisterFunction and ComUnregisterFunction Attributes

When registering a .NET assembly for COM use, the export process adds a standard set of COM-related registry entries. If you need to place custom information in the registry when a .NET class is exported, you can use the ComRegisterFunction attribute to specify a shared function (static function in C++ and C#), which will be called when the assembly is registered. This gives a .NET type a way to participate in the registration process. Heres an example in Visual Basic .NET:

 PublicClassExportedToCom <ComRegisterFunction()>_ PublicSharedSubTheRegistrationFunction(tAsType) ... EndSub EndClass 

The function has a single argument of type Type , which is used to denote the type of object for which registration is required. As you will see later in the section Hosting Windows Forms Controls in ActiveX Containers, this argument is used for obtaining details of the assembly to be registered. You can manipulate the registry using the Registry and RegistryKey classes in the Microsoft.Win32 namespace.

It is a requirement that COM types handle both the insertion and removal of their registry data, so if you implement a registration function using ComRegisterFunction , you must also implement a matching method that removes the registry entries and tag it with the ComUnregisterFunction attribute. Once again, the function is shared and takes a Type as its argument:

 PublicClassExportedToCom <ComRegisterFunction()>_ PublicSharedSubTheRegistrationFunction(tAsType) ... EndSub <ComUnregisterFunction()>_ PublicSharedSubTheUnregistrationFunction(tAsType) ... EndSub EndClass 

Creating a Type Library

Creating a type library for a .NET component is an optional step because some COM components dont provide one. There are three ways to create a type library:

  • Use the type library exporter tool, TlbExp.exe.

  • Use the assembly registration tool, RegAsm.exe.

  • Use the System.Runtime.InteropServices.TypeLibConverter class.

Command-Line Tools

The TlbExp.exe tool is run from the command line to generate type libraries from .NET assemblies. The command line has the general form

 tlbexpassemblyFile[options] 

where assemblyFile is the name of a file containing an assembly. Table 4-4 shows the options that can be used with this command.

Table 4-4: Flags Supported by TlbExp.exe

Option

Description

/help or /?

Provides help on the options supported by the tool.

/names: file

Allows the user to control the capitalization of names in the generated type library by specifying a text file. Each line of the file should contain a name that occurs in the type library.

/nologo

Runs the tool without a startup banner.

/out: file

Specifies the name of the output type library file. If omitted, the tool uses the assembly name with a .tlb extension. Note that the assembly name might not be the same as the name of the file containing the assembly.

/silent

Suppresses the output of informational messages.

/verbose

Displays a list of referenced assemblies for which type libraries also need to be generated.

If the file myfile.dll contains an assembly called MyType , the following command line will generate a type library called MyType.tlb :

 tlbexpmyfile.dll 

The /out option can be used to specify the output filename:

 tlbexpmyfile.dll/out:myfile.tlb 

RegAsm.exe is used to register .NET components as COM objects, and it can also be used to generate a type library. The command line has the general form

 regasmassemblyFile[options] 

where assemblyFile is the name of a file containing an assembly. Table 4-5 shows the options that can be used with the RegAsm.exe command.

Table 4-5: Flags Supported by RegAsm.exe

Option

Description

/ codebase or /c

Creates a codebase entry in the registry. This can be used to locate assemblies that arent installed in the Global Assembly Cache (GAC). If the assembly is installed in the GAC, you dont need to use this option. Note that /codebase can be used only on assemblies that have strong names.

/help or /?

Provides help on the options supported by the tool.

/nologo

Runs the tool without a startup banner.

/regfile: file or /r: file

Causes RegAsm.exe to generate a .reg file that contains the registry entries for the class. No changes will be made to the registry when this flag is used. Note that this option cannot be used in conjunction with the /tlb or /u option.

/silent or /s

Suppresses the output of informational messages.

/tlb[: file ] or /t[: file ]

Causes RegAsm.exe to generate a type library for the assembly. If no filename is specified, the assembly name will be used as the base for the type library filename.

/unregister or /u

Causes RegAsm.exe to remove the registry entries for the classes found in the assembly.

/verbose

When used in conjunction with the /tlb option, RegAsm.exe displays a list of referenced assemblies for which type libraries also need to be generated.

As an example of RegAsm.exe usage, if the file myfile.dll contains an assembly called MyType , the following command line will register the assembly and generate a type library called MyType.tlb :

 regasmmyfile.dll/tlb 

The /tlb option can be used to specify the output filename:

 regasmmyfile.dll/tlb:myfile.tlb 

Using TypeLibConverter

The System.Runtime.InteropServices.TypeLibConverter class can be used to generate a type library from within code, using the ConvertAssemblyToTypeLib method. The Visual C# example program in Listing 4-1 shows how this method can be used to create a type library from a simple console application. You can find this code in the Chapter04\Converter folder of the companion content. The companion content can be downloaded from the Web at http://www.microsoft.com/mspress/books/6426.asp .

Listing 4-1: Converter.cs
start example
 usingSystem; usingSystem.Reflection; usingSystem.Reflection.Emit; usingSystem.Runtime.InteropServices; //DefineamanagedequivalentfortheITypeLibinterface [ ComImport, Guid("00020406-0000-0000-C000-000000000046"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown), ComVisible(false) ] publicinterfaceITypeLibInterface{ voidCreateTypeInfo(); voidSetName(); voidSetVersion(); voidSetGuid(); voidSetDocString(); voidSetHelpFileName(); voidSetHelpContext(); voidSetLcid(); voidSetLibFlags(); voidSaveAllChanges(); } publicclassConverter{ publicstaticvoidMain(){ //Createassembly,converterandcallbackobjects Assemblyasm=Assembly.LoadFrom("Account.dll"); TypeLibConverterconverter=newTypeLibConverter(); ConversionEventseventHandler=newConversionEvents(); //Dotheconversion ITypeLibInterfacetypeLib= (ITypeLibInterface)converter.ConvertAssemblyToTypeLib(asm, "MyTypeLib.tlb",0,eventHandler); //Savethelibrary typeLib.SaveAllChanges(); } } publicclassConversionEvents:ITypeLibExporterNotifySink{ publicvoidReportEvent(ExporterEventKindeventKind, inteventCode,stringeventMsg){ Console.WriteLine("Event: " +eventMsg); } publicObjectResolveRef(Assemblyasm){ //Resolvethereferencehereandreturnacorrecttypelibrary returnnull; } } 
end example
 

If you have read Chapter 3, youll notice that using ConvertAssemblyToTypeLib parallels the use of the ConvertTypeLibToAssembly method to import COM type libraries for use in .NET code. The function returns a reference to an object that implements the COM ITypeLib interface, so you need to define an equivalent .NET interface to act as the return type. Note that the name of the interface isnt significant, as long as it has the correct interface ID. Note also the attributes on this interface:

  • ComImport shows this interface has been previously defined in COM so that .NET doesnt regard it as a completely new .NET interface definition.

  • Guid specifies the interface ID for the ITypeLib interface.

  • InterfaceType defines the interface as being derived from IUnknown .

  • ComVisible says this interface should not be visible to COM in the event that the assembly containing the interface definition is exported as a type library.

The TypeLibConverter class will fire events during conversions, and you need to supply an object to act as a sink for these events. Note that you cannot pass a null reference for this parameter. The callback class must implement the ITypeLibExporterNotifySink interface, with its two members ReportEvent and ResolveRef . ReportEvent will be called whenever an event is generated by the conversion process, and in this example simply prints a message. If a reference is found to another assembly, ResolveRef will be called so that the callback object can return a reference to the correct type library for the assembly. In this example, there are no embedded references, so this function can return null.

The ConvertAssemblyToTypeLib method takes four arguments: an Assembly object representing the assembly, a string representing the name of the generated type library, optional flags to control the export process, and a reference to a callback handler. Note that the conversion process doesnt save the type library information in the file, so it is necessary to call SaveAllChanges once conversion has finished.

Signing the Assembly

.NET component assemblies must be signed with a strong name if they are to be used as COM components. Signing assemblies with strong names has been covered in detail in Chapter 3, in the section Assemblies and the GAC.

Registering the Component

COM uses entries in the registry to obtain information about component location and capability. For a .NET component to be usable by COM, you need to ensure that the correct COM-related registry entries are available. There are three ways you can do this:

  • If you are using Visual Studio .NET, you can use the Register For COM Interop option in the Project Settings dialog to automatically register a component. This option is shown in Figure 4-3.

  • Use RegAsm.exe, from the command line. This option was discussed in the last section.

  • Use the System.Runtime.InteropServices.RegistrationServices class to register the assembly from code.

    click to expand
    Figure 4-3: Setting the Register For COM Interop option to True will cause a component to be registered for use by COM clients.

The System.Runtime.InteropServices.RegistrationServices class can be used to register and unregister components by means of its RegisterAssembly and UnregisterAssembly methods. These two methods are simple to use because they only need to be passed a reference to an Assembly object. The RegisterAssembly method takes a second argument that can be used to create a codebase entry in the registry, which enables the CLR to locate components that arent registered in the GAC. The following code fragments show how these methods can be called in Visual Basic .NET:

 'Loadanassembly DimasmAs[Assembly] asm=[Assembly].LoadFrom("MyAssembly.dll") 'Createaregistrationobjectandregistertheassembly DimrsAsnewRegistrationServices() rs.RegisterAssembly(asm,AssemblyRegistrationFlags.None) 

Using .NET Components from COM Client Code

From the perspective of the programmer, using .NET components in unmanaged code is exactly the same as using other COM components: the same mechanisms are used to access the COM type information and create instances of coclasses.

The difference between using COM components and .NET components lies in the way the .NET components have to be registered before use. The following example will show how to register a .NET component, and then how to use it in COM client code.

The Sample Component

The LittleString class exposes a simple string type to COM clients. The class stores a string internally, and implements the ToUpper , ToLower , and SubString methods, delegating the operations to the members of System.String . Listing 4-2 contains an example in Visual C#. You can find this sample in the Chapter04\LittleString folder in the books companion content.

Listing 4-2: LittleString.cs
start example
 usingSystem; usingSystem.Runtime.InteropServices; //Auto-generatedGUID,dualinterface [InterfaceType(ComInterfaceType.InterfaceIsDual)] publicinterfaceIString{ stringText{get;set;} //Auto-generatedGUID,noclassinterface stringToUpper(); stringToLower(); stringSubString(intstart,intlength); } [ClassInterface(ClassInterfaceType.None)] publicclassLittleString:IString { privatestringtext; //DefaultconstructorneededbyCOM publicLittleString() { } //Accessorforthetext publicstringText{ get{ returntext; } set{ text=value; } } publicstringToLower(){ returntext.ToLower(); } publicstringToUpper(){ returntext.ToUpper(); } publicstringSubString(intstart,intlength){ returntext.Substring(start,length); } } 
end example
 

There are several things to notice about this code:

  • The methods are defined in the IString interface, which is implemented by the LittleString class. Since all the COM-visible methods are implemented using an interface, the class doesnt need to have a class interface.

  • The class has a default constructor, which is necessary if instances are to be created by COM.

  • The class provides a property with a setter, which enables the text of the string to be set after the instance has been created.

  • A dual class interface has been provided so that early-bound client code can use the type.

  • By default, the version information is specified as 1.0.* in the AssemblyInfo.cs file, which leaves it up to Visual Studio .NET to choose the last two parts of the version and generate a new pair of values each time the assembly is recompiled. To prevent this, Ive fixed the version number to be 1.0.1.1 . This will make it easier for clients that specify a CLSID when creating instances.

Installing the Component for COM Use

.NET types that are to be used from COM need to be installed in the GAC, which requires the component to have a strong name. Use the SN.exe tool to create a keypair file, and put it in the project directory:

 snktheKey.snk 

Open AssemblyInfo.cs, and edit the AssemblyKeyFile attribute to contain the filename. Remember to set the path to point two levels above, to the project directory:

 [assembly:AssemblyKeyFile(@"..\..\theKey.snk")] 

Now build the project to create the assembly DLL. From a Visual Studio .NET Command Prompt window, use RegAsm.exe to register the component and create a type library:

 regasmLittleString.dll/tlb:LittleString.tlb 

Then use the GACUTIL.EXE tool to install the assembly in the GAC:

 gacutiliLittleString.dll 

The assembly is now registered and installed in the GAC.

Note 

If you rebuild the assembly, youll need to reinstall the component in the GAC. If you forget to do this, client code will end up using an old version of the component.

Looking at the Type Library

Opening the type library in the COM/OLE Object Viewer shows that the type library contains the following entries for the coclass and IString interface:

 [ odl,uuid(A1B26B91-9565-3D0D-9DEC-DC3B2AE594FA), version(1.0),dual,oleautomation, custom(0F21F359-AB84-41E8-9A78-36D110E6D2F9,IString) ] interfaceIString:IDispatch{ [id(0x60020000),propget] HRESULTText([out,retval]BSTR*pRetVal); [id(0x60020000),propput] HRESULTText([in]BSTRpRetVal); [id(0x60020002)] HRESULTToUpper([out,retval]BSTR*pRetVal); [id(0x60020003)] HRESULTToLower([out,retval]BSTR*pRetVal); [id(0x60020004)] HRESULTSubString([in]longstart,[in]longlength, [out,retval]BSTR*pRetVal); }; [ uuid(BE9CE00E-2318-3B47-AC22-CA58C97EEAA9), version(1.0), custom(0F21F359-AB84-41E8-9A78-36D110E6D2F9, LittleString.LittleString) ] coclassLittleString{ interface_Object; [default]dispinterfaceIString; }; 

You can see how the LittleString coclass doesnt have a class interface and uses IString as the default interface. It also exposes the _Object class interface for the System.Object class. Strings are passed as BSTR s so that they can easily be used by COM clients.

Using the Class in COM Code

The sample program in Listing 4-3 shows how this class can be used from unmanaged Visual C++ code, using the COM compiler support included with Visual C++ 6. Youll find this code in the Chapter04\UseNetControl folder in the books companion content.

Listing 4-3: UseNetControl.cpp
start example
 #define_WIN32_DCOM #include<iostream> usingnamespacestd; //IncludedefinitionsofbasicWindowstypes #include<wtypes.h> //Importthetypelibrary #import "littlestring.tlb" no_namespacenamed_guids intmain() { CoInitializeEx(0,COINIT_MULTITHREADED); try { //Createthesmartpointer IStringPtrsp(__uuidof(LittleString)); cout<< "Createdobject" <<endl; //Setthetext sp->PutText("hello"); //Convertittouppercase _bstr_tbs=sp->ToUpper(); cout<< "Textis " <<bs<<endl; } catch(_com_error&ce) { cout<< "comerror: " <<ce.ErrorMessage()<<endl; } CoUninitialize(); return0; } 
end example
 

The important part of this code is the #import directive, which tells the compiler to read a type library and create a wrapper class that can be used to access a COM object. In this example, Ive copied the type library to the C++ project directory, but you can use a relative or absolute path instead. The IStringPtr object creates a COM object based on the CLSID it is given as its argument: here, it is actually an interface pointer on the CCW that is being returned. Once the object has been created, it can be used like any other COM object.

 
team lib


COM Programming with Microsoft .NET
COM Programming with Microsoft .NET
ISBN: 0735618755
EAN: 2147483647
Year: 2006
Pages: 140

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