• Avoiding Registration
• Hosting Windows Forms Controls in Any ActiveX Container
• Working Around COM-Invisibility
• Using Reflection to Invoke Static Members
• Handling .NET Events
• Unexpected Casing in Type Libraries
• Advanced Shutdown Topics
Just as Chapter 6, “Advanced Topics for Using COM Components,” focused on a handful of advanced topics for using COM components in .NET applications, this chapter focuses on a handful of advanced topics for using .NET components in COM applications. These topics include
• Avoiding registration
• Hosting Windows Forms controls in any ActiveX container
• Working around COM-invisibility
• Using reflection to invoke static members
• Handling .NET events
• Unexpected casing in type libraries
• Advanced shutdown topics
The standard mechanisms for using .NET components in COM applications require the registration of the .NET components just as if they were COM components. In the .NET world of xcopy deployment, avoiding this registration is desirable. For example, Chapter 8, “The Essentials for Using .NET Components from COM,” demonstrated that running Windows Forms Controls in Internet Explorer requires no registration. Two approaches can be used with any COM application to avoid the registration of .NET components:
• Hosting the Common Language Runtime (CLR)
• Using the ClrCreateManagedInstance
API
The CLR supports being hosted inside an application that controls its behavior. The host application is responsible for creating application domains, creating objects, and running managed user code. For example, the .NET Framework ships with a CLR host used by ASP.NET and a CLR host used by Internet Explorer (which enables the creation of .NET objects using the new <object>
tag syntax without registration). It’s also unmanaged hosting code that initializes the CLR in traditional .NET console or Windows applications.
The CLR provides a hosting API that consists of static entry points in MSCOREE.DLL
, plus COM classes and interfaces described in MSCOREE.TLB
. The centerpiece of these APIs is the CorRuntimeHost
coclass, which implements the following COM interfaces:
• ICorRuntimeHost
—Provides control of core services such as threads and application domains.
• IGCHost
—Provides control of garbage collection.
• ICorConfiguration
—Enables the configuration of debugging and garbage collection.
• IValidator
—Enables validation of an assembly, to determine whether it contains unverifiable code. Note that this is a completely different interface than the .NET System.Web.UI.IValidator
interface!
• IDebuggerInfo
—Enables a host to discover if a debugger is currently attached to the process.
Listing 10.1 demonstrates Visual Basic 6 code that makes use of the hosting API to create and use a System.Collections.SortedList
instance from the mscorlib
assembly. Unlike the use of mscorlib
types in Chapter 8, the mscorlib
assembly does not need to be registered with REGASM.EXE
in order for the code in the listing to run.
Listing 10.1. Hosting the CLR and Using .NET Objects in a Visual Basic 6 COM Client
This code can be inserted in a Visual Basic 6 project that references MSCOREE.TLB
(“Common Language Runtime Execution Engine 1.0 Library”) and MSCORLIB.DLL
(“Common Language Runtime Library”). MSCOREE.TLB
contains the definition of CorRuntimeHost
and MSCORLIB.DLL
contains the definition of AppDomain
, SortedList
, and IEnumerable
.
Lines 1–7 contain the code to initialize the CLR and get a reference to the default application domain. Alternatively, the CorBindToRuntimeEx
API exported by MSCOREE.DLL
could have been used to load the CLR into a process, but instantiating the CorRuntimeHost
coclass is easier—especially for Visual Basic 6 programs.
Lines 11–12 create an instance of the System.Collections.SortedList
class in the mscorlib
assembly using AppDomain.CreateInstance
. This method returns a System.Runtime.Remoting.ObjectHandle
instance, so its Unwrap
method must be called in order to obtain a reference to the desired instance. The returned instance is a COM-Callable Wrapper, just like the one that would be returned using the techniques in Chapter 8.
Lines 15–20 add the words from this chapter’s title to the list, using the same string for each item’s key and value since the code is interested in sorting the words alphabetically. Line 23 calls SortedList.GetValueList
to get an IList
reference, Lines 28–30 enumerate over the list and append each value to a string, then Line 31 prints the value in a message box. The resulting string is “.NET Advanced Components for Topics Using.” Finally, Line 32 calls CorRuntimeHost
’s Stop
method because we’re finished with it. If we were finished with an application domain other than the default domain, we would call the CorRuntimeHost.UnloadDomain
at this point. The default application domain cannot be unloaded, however.
Notice that although SortedList.GetValueList
returns an IList
interface, Line 22 declares the returned type as an IEnumerable
variable. IList
derives from IEnumerable
, but the For Each
statement in Line 28 would not work if list
were declared as an IList
type instead. This is due to the fact that interface inheritance is not exposed to COM, as discussed in the previous chapter. Had GetValueList
returned a .NET class type that implements IList
(exposed as a class interface), the return type could be declared as the class type in Visual Basic 6 and For Each
would work since class interfaces contain inherited members.
The assembly name passed as the first parameter to AppDomain.CreateInstance
must be a complete specification if the assembly is to be loaded from the Global Assembly Cache, for example:
System, Version=1.0.3300.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
The mscorlib
assembly is the only exception to this rule. (Private assemblies, however, can be loaded with a partial specification.) The string that specifies the assembly name is case-insensitive, but the string that specifies the type name is case-sensitive (and must be qualified with its namespace). Figure 10.1 demonstrates the error raised from Listing 10.1 if the type name was given in an incorrect case. You should recognize the HRESULT
value as COR_E_TYPELOAD
(from System.TypeLoadException
).
Figure 10.1. Error raised with incorrect use of Activator.CreateInstance
.
Listing 10.2 demonstrates hosting code similar to Listing 10.1, but in unmanaged C++. This listing takes advantage of an AppDomain.CreateInstance
overload (exported as CreateInstance_3
) to instantiate a .NET object using a parameterized constructor! This example creates a System.Collections.ArrayList
instance with its constructor that accepts an initial capacity.
Listing 10.2. Hosting the CLR and Using .NET Objects in an Unmanaged Visual C++ COM Client
As with Listing 10.1, this listing references two .NET type libraries: MSCOREE.TLB
and MSCORLIB.TLB
. The paths used in Lines 6 and 7 are two different directories, and vary based on your computer’s settings.
Although MSCOREE.DLL
resides in the Windows system directory (for example, C:Windowssystem32
), MSCOREE.TLB
resides in the .NET Framework folder where assemblies such as MSCORLIB.DLL
exist. For example, such a folder may look as follows:
C:WindowsMicrosoft.NETFrameworkv1.0.3300
Lines 51–52 instantiate the CorRuntimeHost
class, Line 71 calls its Start
method, and Line 80 calls its GetDefaultDomain
method, as was done in Listing 10.1 (but with much fewer lines of code). The difference here is in Lines 118–119, which call the CreateInstance
overloaded method that’s exposed to COM as CreateInstance_3
. _AppDomain
is a real .NET interface exposed as a dual interface, so the CreateInstance_3
method can be called without using IDispatch
. The array of parameters to pass to this method is prepared in Lines 103–115. This array contains a single integer element with the value 128, representing the desired initial capacity of the ArrayList
instance.
After the object has been instantiated, Line 129 calls Unwrap
on the returned ObjectHandle
. Then, to prove that the ArrayList
was created with an initial capacity of 128, Lines 138–172 handle all the steps necessary to invoke the get accessor for the object’s Capacity
property. This involves querying for IDispatch
(since the property is not defined on any .NET interface), retrieving the DISPID for the property (Lines 149–158), then invoking using that DISPID (Lines 161–172). Finally, Line 174 prints the capacity value, so running this program prints the following to the console:
ArrayList Capacity: 128
By hosting the runtime and calling _AppDomain::CreateInstance_3
in unmanaged C++, a COM client can instantiate .NET objects using a parameterized constructor.
ClrCreateManagedInstance
APIMSCOREE.DLL
exports a static entry point called ClrCreateManagedInstance
that enables unmanaged code to create .NET objects without writing any code to host the CLR. This method is declared in MSCOREE.IDL
, which ships with the .NET Framework SDK. This file simply echoes the declaration to MSCOREE.H
, which looks like the following:
STDAPI ClrCreateManagedInstance(LPCWSTR pTypeName, REFIID riid,
void **ppObject);
This method works much like CoCreateInstance
, but with a string representing a .NET class rather than a CLSID representing a COM class. The string passed as pTypeName
must be an assembly-qualified class name unless the class is defined in the mscorlib
assembly. The riid
parameter is the IID of the interface to be returned, and the ppObject
parameter is the returned interface pointer. As with CoCreateInstance
, IID_IUnknown
is often passed for the riid
parameter. COM-visibility rules are enforced with the object returned from this method, as when hosting the CLR in the previous two listings. That’s because you interact with the same CCWs you’d be using with standard COM Interoperability.
Listing 10.3 demonstrates the use of ClrCreateManagedInstance
in unmanaged C++ code to instantiate a System.CodeDom.CodeObject
object then call its UserData
property via late binding. As with the previous two listings, no registration is required for this code to work.
Listing 10.3. Using ClrCreateManagedInstance
to Create and Use .NET Objects from COM Without Registration
Line 6 includes MSCOREE.H
, whose location depends on your computer’s settings. For a Visual Studio .NET user, the file resides in the Program FilesMicrosoft Visual Studio .NETFrameworkSDKinclude
directory. The code must be linked with MSCOREE.LIB
to resolve the ClrCreateManagedInstance
call. Visual Studio .NET users can find this file in the Program FilesMicrosoft Visual Studio .NETFrameworkSDKLib
directory.
In this example, no type libraries needed to be referenced, but if you need to reference MSCORLIB.TLB
in addition to including MSCOREE.H
, you should use #import
’s exclude
directive as follows:
#import "path\mscorlib.tlb" no_namespace named_guids raw_interfaces_only exclude("IObjectHandle")
This is needed because the IObjectHandle
interface is defined in both MSCOREE.H
and MSCORLIB.TLB
.
Line 25 initializes COM, which is strictly not necessary because the call to ClrCreateManagedInstance
initializes COM if it isn’t already. Lines 34–36 call ClrCreateManagedInstance
with the assembly-qualified string for System.CodeDom.CodeObject
. Line 45 queries for the IDispatch
interface so we can late bind to the object, and Lines 55–77 handle the invocation of the UserData
property.
Before delving into this section, I want to make it clear that the only supported ActiveX container for hosting .NET Windows Forms is Internet Explorer. And there’s a good reason for this—almost every ActiveX container behaves slightly differently and causes incompatibilities for the controls being hosted. Windows Forms controls are tuned and tested for Internet Explorer, and hosting them in any other ActiveX container may not completely work. This is why there is no automatic support for exposing Windows Forms controls as generic ActiveX controls.
That said, it is possible to use Windows Forms controls as ActiveX controls for any container (such as a Visual Basic 6 form) at your own risk. The only additional work that enables this is the addition of some Windows Registry keys and values. These additions are outlined in the following steps:
1. Under HKEY_CLASSES_ROOTCLSID{
clsid
}
, where clsid is the CLSID of the Windows Forms control class, add a subkey named Control
.
2. Under HKEY_CLASSES_ROOTCLSID{
clsid
}Implemented Categories
, add a subkey named {40FC6ED4-2438-11CF-A3DB-080036F12502}
. This GUID is CATID_Control
, the COM component category that identifies the class as an ActiveX control. Unlike Step 1, this is optional but could be useful for some ActiveX containers.
3. Under HKEY_CLASSES_ROOTCLSID{
clsid
}
, add a subkey named MiscStatus
. This key’s default value should be set to a value of bitwise-OR
ed members of the OLEMISC
enumeration, described after these steps.
4. Under HKEY_CLASSES_ROOTCLSID{
clsid
}
, add a subkey named TypeLib
. This key’s default value should be set to the LIBID of the type library exported for the assembly containing the Windows Forms control.
5. Under HKEY_CLASSES_ROOTCLSID{
clsid
}
, add a subkey named Version
. This key’s default value should be set to the two-part version of the type library exported for the assembly containing the Windows Forms control, such as “1.0”.
If you don’t add the TypeLib
and Version
subkeys, and if the exported type library is not registered, Visual Basic 6 does not show the control on the Control
tab of its Components
dialog.
The OLEMISC
enumeration, whose values must be used with the MiscStatus
subkey, is defined in the Windows Platform SDK as shown in Listing 10.4.
Listing 10.4. The OLEMISC
Enumeration Used When Registering ActiveX Controls
The various values of the enumeration are described in the Platform SDK documentation (available on MSDN Online) but a sensible MiscStatus
value for Windows Forms controls is OLEMISC_RECOMPOSEONRESIZE | OLEMISC_INSIDEOUT | OLEMISC_ACTIVATEWHENVISIBLE | OLEMISC_SETCLIENTSITEFIRST
(131457). You could also include flags such as OLEMISC_ACTSLIKEBUTTON
or OLEMISC_ACTSLIKELABEL
, depending upon the nature of your control.
Listing 10.5 contains portions of an update to the LabeledTextBox
control from Listing 8.6 in Chapter 8, to perform the five registration steps listed previously. It does this with the use of custom registration functions that are invoked by REGASM.EXE
. See Chapter 12, “Customizing COM’s View of .NET Components,” for more information about custom registration functions.
Listing 10.5. The LabeledTextBox
Control That Registers Itself As a Generic ActiveX Control
Lines 32–35 handle the type library version number specially because it doesn’t equal Major.Minor if the assembly version number is 0.0. For the omitted parts of the listing, check out this book’s Web site or the corresponding listing in Chapter 8.
The following steps can be performed to host a Windows Forms control on a Visual Basic 6 form:
1. Register the assembly containing the Windows Forms control as follows:
regasm LabeledTextBox.dll /tlb /codebase
3. Using the /codebase
option is the easiest way to get your assembly found from within the Visual Basic 6 IDE, but you could alternatively do something like giving the assembly a strong name and installing it into the Global Assembly Cache. During this step, the ComRegisterFunction
method defined in Lines 13–34 of Listing 10.5 is invoked by REGASM.EXE
to add the registry entries specific to ActiveX controls.
4. Open a new Visual Basic 6 project such as a Standard EXE
project.
5. Right-click on the Toolbox
window and select Components...
.
6. Select the name of the control, such as LabeledTextBox
, then press OK
. This dialog is shown in Figure 10.2.
Figure 10.2. Selecting a Windows Forms control registered as an ActiveX control in Visual Basic 6.
9. Drag the control onto the form, as shown in Figure 10.3.
Figure 10.3. Dragging a Windows Forms control onto a Visual Basic 6 form.
From this point, you may have mixed results in getting such an application to work correctly, because this kind of interaction is not supported.
Although .NET types and members are sometimes made invisible to COM, there’s ultimately nothing a .NET component author can do to prevent the use of functionality from COM if it’s publicly available to .NET components. Anyone can write a COM-visible .NET component that acts as an intermediate layer between .NET and COM components. This layer can easily expose COM-invisible .NET functionality to COM by routing calls from the COM component to the .NET component.
But exposing COM-invisible functionality to COM is often easier than explicit delegation. As you know, a .NET class interface contains the members of base classes. More precisely, it contains the public COM-visible members of base classes. This behavior doesn’t take into consideration the COM-visibility of classes themselves, so public members of a COM-invisible base class become visible in a derived class’s class interface unless the members are individually marked as being COM-invisible. Listing 10.6 demonstrates this often-unexpected behavior. This listing uses two custom attributes—ComVisibleAttribute
and ClassInterfaceAttribute
—which are explained in Chapter 12.
Listing 10.6. Tricks with COM-Visibility and Inheritance
Although the IAmVisibleToAll
interface that derives from a COM-invisible interface doesn’t contain the base method (as usual), the ClassVisibleToAll
class contains the COM-invisible base class’s SomeMethod
method. Had the SomeMethod
method been directly marked with ComVisible(false)
, it would not appear in class interfaces for derived classes.
This inheritance behavior, which can seem strange at first glance, can come in handy whenever you want to expose classes to COM that derive from COM-invisible classes. For example, consider the following vanilla Windows Form written in Visual Basic .NET:
Imports System.Windows.Forms
Public Class ComVisibleForm
Inherits Form
End Class
Although System.Windows.Forms.Form
is a COM-invisible class, this simple ComVisibleForm
class can be instantiated from COM, and most of the methods of Form
can be invoked via late binding. For example, a Visual Basic 6 client could do something like:
Dim f As Object
Set f = New ComVisibleForm
f.Text = "Late binding doesn't look too bad in VB6!"
f.FormBorderStyle = 2 'FormBorderStyle.Fixed3D
f.Show
The ComVisibleForm
coclass looks like the following in the type library, with the empty _ComVisibleForm
class interface making the base Form
methods available via late binding:
[
uuid(9A0D1EDF-B7B0-353A-8069-5E75EABF8F19),
version(1.0),
custom(0F21F359-AB84-41E8-9A78-36D110E6D2F9, "ComVisibleForm")
]
coclass ComVisibleForm {
[default] interface _ComVisibleForm;
interface _Object;
interface IComponent;
interface IDisposable;
};
If the base methods were COM-invisible by default, the derived class would have to either override each method or provide separate methods that call each base class method. Instead, this only needs to be done for methods that have COM-invisible parameter types. To get a feel for the methods that can be invoked on the ComVisibleForm
class interface, you could mark the class with ClassInterfaceType.AutoDual
and export a type library. This technique is described in Chapter 12. Listing 10.7 shows what the beginning of this extremely long class interface looks like (truncated for brevity). Note that exporting such a type library causes TLBEXP.EXE
to complain about several methods because you’re now exposing methods with COM-invisible value type parameters. These originally COM-invisible methods are defined in the base classes in the System.Windows.Forms
assembly.
Listing 10.7. The Beginning of a Class Interface for the ComVisibleForm
Class That Derives from System.Windows.Forms.Form
For overloaded methods that end up with mangled names (such as Invoke_2
, Invalidate_6
, and so on), a COM-friendly Windows Form should expose methods with unique names in the derived class that call the base methods so COM doesn’t have to. This technique is also useful for members that can’t be called from COM due to COM-invisible value type parameters. For example, the Form
class has an Anchor
property (inherited from the base Control
class) of type AnchorStyles
, a COM-invisible enumeration. By providing a different property, say MyAnchor
, using the underlying type of the enumeration instead, the same functionality could be exposed to COM. For example:
Imports System.Windows.Forms
Public Class ComVisibleForm
Inherits Form
' MyAnchor delegates to the base Anchor property
Public Property MyAnchor As Integer
Get
Return MyBase.Anchor
End Get
Set
Value = MyBase.Anchor
End Set
End Property
End Class
The best choice, as explained in the following chapter, would be to define an interface with exactly the methods you want to expose to COM, say IForm
, and use that as the default interface.
Reflecting over .NET objects in unmanaged code is fairly simple, even in unmanaged C++, because many of the important reflection classes expose auto-dual class interfaces (such as _Type
and _MemberInfo
). Using reflection from COM can be useful for discovering metadata that does not get exported to type libraries. It’s also great for invoking static members, which do not get directly exposed to COM.
The process of invoking static members on a given CCW instance is straightforward. First, you call the GetType
instance method on the class interface for System.Object
(_Object
) to get a System.Type
instance that represents the original instance. Then, you can call InvokeMember
on the _Type
interface with the appropriate binding flags for invoking the static member. Besides the binding flags that specify the member type (method, property, or field) and visibility, the binding flags should include Static
and FlattenHierarchy
. The latter flag is necessary if the static member is defined on a base class of the current type rather than directly on the type.
However, static members often appear on classes for which an instance can never be obtained. Widely used examples of such classes are Marshal
in the System.Runtime.InteropServices
namespace, Activator
in the System
namespace, or Console
in the System
namespace. These classes serve as containers for static members, so they don’t have any public constructors, nor do any members return instances of them.
Fortunately, it’s still possible to invoke static members on such classes by taking advantage of _Type
, the class interface for System.Type
. Because static members can be invoked via reflection without a corresponding instance, all we need to obtain is the type for the class containing a static member rather than an instance of the class. To call any static member M on any type T, you can perform the following steps:
1. Create any .NET object, and query for the _Object
interface from its CCW.
2. Call _Object.GetType
to get a System.Type
instance.
3. Call _Type.GetType
, which is the instance method inherited from System.Object
, to get another System.Type
instance. This time, however, the Type
instance represents the System.Type
type instead of the original type.
4. Call InvokeMember
on the second Type
instance with the appropriate binding flags to call the static System.Type.GetType
method. Pass the name of T, qualified with its assembly name if necessary, as the parameter to GetType
.
5. Extract the _Type
interface pointer from the VARIANT
returned by InvokeMember
. This represents T, so call InvokeMember
on this interface with the appropriate binding flags, passing the name of M to invoke the desired static method.
Listing 10.8 performs these steps in unmanaged C++ and demonstrates using them with two static members in the mscorlib
assembly: System.Console.ReadLine
and System.AppDomain.CurrentDomain
. Unfortunately, it’s not possible to do this in Visual Basic 6 because Type.InvokeMember
uses by-value array parameters, which are not supported in VB6.
Listing 10.8. Using Reflection to Invoke Any Static .NET Members
The path in Line 5 must be replaced with the location of MSCORLIB.TLB
on your computer, which can vary according to your settings. Step 1 is performed in Lines 28–34. System.Object
is chosen as the dummy object to instantiate, so CLSID_Object
(which is defined thanks to using the #import
statement in Line 5) is passed to CoCreateInstance
. Step 2 is performed in Lines 37–38, and step 3 is performed in Lines 43–44.
Lines 48–53 create a single-element SAFEARRAY
containing the passed-in type name. This is passed to the Type.InvokeMember
call in Lines 58–61, which is step 4. This step demonstrates the invocation of a static method (GetType
) on a class for which we’ve got an instance (System.Type
). Notice that the simplest overload of Type.InvokeMember
is exposed to COM as InvokeMember_3
! Also notice that the SAFEARRAY
passed to InvokeMember_3
contains a VARIANT
rather than directly containing a BSTR
. That’s because Type.InvokeMember
expects an array of System.Object
instances, so passing an array of strings would be a type mismatch.
To pass a null object to a .NET parameter exposed as a VARIANT
, pass a VARIANT
with its vt
field set to VT_EMPTY
. Listing 10.8 does this for the target
parameter of _Type.InvokeMember_3
.
Step 5 is performed in Lines 66–75. Lines 66–67 extract the IUnknown
pointer from the returned VARIANT
and query for the _Type
interface. Lines 71–74 call InvokeMember_3
on this interface with the passed-in member name, member type, and parameters. The returned object is stored in the retVal
variable, which is accessible to the caller of InvokeStaticMember
.
This five-step procedure is pretty lengthy in unmanaged C++, but the same code would look as follows in C#:
Object o = new Object();
Type t = o.GetType();
Type typeOfType = t.GetType();
Type desiredType = (Type)typeOfType.InvokeMember("GetType",
BindingFlags.InvokeMethod | BindingFlags.Static | BindingFlags.Public |
BindingFlags.FlattenHierarchy, null, null, new object[]{typeName});
return desiredType.InvokeMember(memberName, memberType | BindingFlags.Static |
BindingFlags.Public | BindingFlags.FlattenHierarchy, null, null, parameters);
Lines 113–126 demonstrate the static invocation functionality with the System.Console.ReadLine
method, and Lines 132–155 demonstrate it with the System.AppDomain.CurrentDomain
property.
Using reflection in unmanaged script has a limitation in version 1.0 of the CLR: .NET members with enum parameters cannot be invoked. COM clients can invoke .NET members with enum parameters via v-table binding, late binding via IDispatch
, and v-table binding to reflection APIs (such as Type.InvokeMember
), but not when late binding via IDispatch
to reflection APIs (a sort of double-late binding). Unmanaged script falls into the last category when explicitly using reflection, so it runs into this limitation.
By default, .NET events cannot be handled (or sinked) by COM components without extra managed code that handles the events and then communicates with COM. Listing 10.9 demonstrates how events are exposed to COM by listing the 14 members that get exported for the seven events of _AppDomain
, the default interface for System.AppDomain
.
Listing 10.9. Exported Methods for the Seven Events Exposed by the _AppDomain
Interface
Because COM interfaces have no notion of events, each event member is exported as a pair of accessor methods—add_
EventName
and remove_
EventName
. These correspond to the event accessors introduced in Chapter 5, “Responding to COM Events.”
Just as .NET clients invoke these accessors (with different syntax, such as +=
or AddHandler
) to hook and unhook event handlers, a COM client could theoretically call add_
EventName
to hook up an event handler and remove_
EventName
to unhook an event handler. The problem is that a COM client has no good way to pass a correct argument to any of these methods. Each accessor method must be passed an instance of a .NET delegate, so the only way to pass it such a type is to write some managed code that can create a .NET delegate and pass it to COM. (Passing a COM object implementing a class interface such as _EventHandler
does not work, as discussed in Chapter 17, “Implementing .NET Interfaces for Type Compatibility.”) COM clients can’t directly instantiate delegates because they don’t have public default constructors. You could get around this limitation by using the CLR hosting technique described in the “Hosting the Common Language Runtime” section, but attempting to set up a delegate from unmanaged code is tricky without writing some managed code.
Chapter 13, “Exposing .NET Events to COM Clients,” describes how .NET components can expose their events to COM using the familiar connection point protocol. If this is done, then COM clients can handle events the same way that they always have—by implementing source interfaces.
Identifiers in type libraries sometimes have different casing than what you might expect. For example, the names of exported methods or properties are sometimes lowercase, whereas their corresponding .NET members have uppercase names. Listing 10.10 demonstrates this phenomenon for _MemberInfo
, an auto-dual class interface exported for System.Reflection.MemberInfo
. Although the .NET MemberInfo
class has a Name
property, the _MemberInfo
interface has a name
property.
Listing 10.10. The Class Interface for System.Reflection.MemberInfo
Has a Property Called name
Rather Than Name
What causes this to happen? Type libraries, like Visual Basic, are case-insensitive. More specifically, identifiers in a type library (class names, method names, parameter names, and so on) are stored in a case-insensitive table. Once one case of an identifier is added to the table (such as name
), any later occurrences of the identifier, regardless of case, are not added to the table (such as Name
). This situation is pictured in Figure 10.4.
Figure 10.4. Type libraries store identifiers in a case-insensitive table, so the first case emitted by the exporter becomes the only case.
Instead of each occurrence of a name
identifier in the type library having its original case, all occurrences point to the same entry in the table. The first casing encountered wins, and all subsequent occurrences match that case in the output type library. For Listing 10.10, countless methods defined in mscorlib.tlb
before the _MemberInfo
interface have a parameter called name
. For example, the IResourceWriter.AddResource
method is exported before MemberInfo
and has such a parameter, so the all-lowercase version wins.
This phenomenon has nothing to do with .NET or with type library exporting. It’s simply a characteristic of type libraries, and can happen even when you create a type library from an IDL file that has the same identifier with conflicting cases. The type library exporter, however, has a mechanism to help manage this case-insensitivity issue. Appendix B, “SDK Tools Reference,” describes how to control the exported case of identifiers using TLBEXP.EXE
’s /names
option.
The #import
statement in Visual C++ can be used with a rename
directive to change any identifier found in a type library to something else. Therefore, the rename
directive can be used to handle unexpected case changes. For example, if you do the following in an unmanaged C++ application, it will appear as if _MemberInfo
has a property called Name
:
#import <mscorlib.tlb> rename("name", "Name")
Of course, it also means that methods like IResourceWriter.AddResource
are given a parameter called Name
, but that casing change won’t affect the use of such methods. The rename
directive replaces every occurrence of the first string with the second string, and can be specified multiple times for multiple string replacements.
The rename
directive can also be used with substrings of identifier names, so it can be useful for renaming the Get
, Put
, and PutRef
prefixes that are automatically added to property accessors to avoid name conflicts with other members. For example:
#import <mylibrary.tlb> rename("Get", "Get_")
MSCOREE.DLL
exports two functions related to process shutdown that can be useful for unmanaged applications that use .NET objects:
• CoEEShutDownCOM
• CorExitProcess
Calling CoEEShutDownCOM
forces the CLR to release all interface pointers it holds onto inside RCWs. This method usually doesn’t need to be called, but can be necessary if you’re running leak-detection code or somehow depend on all interface pointers being released before process termination. It has no parameters and a void
return type. If used, it should be one of the last calls an application makes before shutting down. To use this function in C++, include COR.H
and link with MSCOREE.LIB
. COR.H
can be found in the same directory as MSCOREE.H
, introduced earlier in the chapter.
CorExitProcess
performs an orderly shutdown of the CLR by calling CoEEShutDownCOM
, finalizing any .NET objects that have not yet been finalized, then exiting the process with the passed-in error code. It has the following signature, from its C++ header file:
void STDMETHODCALLTYPE CorExitProcess(int exitCode);
In managed applications, the finalizers for any objects used almost always get invoked at some time before the process shuts down. An exceptional situation must occur to prevent the finalizers of all objects from running. For example, if an object’s finalizer doesn’t appear to be making progress, the CLR is free to terminate it early. Also, if managed code forces process shutdown with a PInvoke call to ExitProcess
, the CLR doesn’t have a chance to run any cleanup code such as invoking objects’ finalizers.
When an unmanaged application uses .NET objects, however, the chance of finalizers getting run before the process shuts down is much less. Unmanaged process termination does not force finalizers to be run, unless the application uses a .NET-aware runtime such as version 7 of the Visual C++ runtime. The simple action of normally exiting an unmanaged Visual C++ 7 program enforces graceful shutdown of the CLR, but exiting an unmanaged Visual C++ 6 program (or Visual Basic 6 program) terminates the process abruptly. Graceful shutdown doesn’t occur automatically because, without a call to CorExitProcess
, the CLR is only notified that the process is shutting down from DllMain
’s process detach notification. The loader lock is held at this point, so there is no way that finalization code can safely run.
Therefore, sophisticated COM clients need to call CorExitProcess
in order for finalization to occur at shutdown as expected. To use this function in C++, include MSCOREE.H
and link with MSCOREE.LIB
.
If you’re using .NET objects that hold onto non-memory resources, you’ll want to dispose of the objects as soon as you’re finished using them rather than waiting for garbage collection or process shutdown by calling their IDisposable.Dispose
method (assuming the objects follow the convention of implementing IDisposable
). A Dispose
method typically does the same work as an object’s finalizer, but suppresses finalization since the cleanup work is already performed when a client remembers to call it.
This chapter completes this book’s coverage of using .NET components in COM applications. If you’re writing a new client application or investing a lot of development effort in an existing COM client, you should seriously consider writing the client with a .NET language instead. A .NET client doesn’t require registration of any .NET components being used, can naturally make use of static members or events, doesn’t have the same problems with case-sensitivity or overloaded methods (in most .NET languages), doesn’t require special treatment when shutting down a process, and so on. And, as demonstrated in Part II, “Using COM Components in .NET Applications,” a .NET application can work naturally with COM components as well as .NET components!
The next chapter begins Part IV, “Designing Great .NET Components for COM Clients,” which focuses on the .NET side of the same COM client/.NET server direction of COM Interoperability. By designing .NET components that follow these guidelines, you’ll prevent COM clients from encountering some of the issues covered in this chapter.