Chapter 10. Advanced Topics for Using .NET Components

In This Chapter

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

Avoiding Registration

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

Hosting the Common Language Runtime

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

Image

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.

Image

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

Image

Image

Image

Image

Image

Image

Image

Image

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.

Tip

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

Tip

By hosting the runtime and calling _AppDomain::CreateInstance_3 in unmanaged C++, a COM client can instantiate .NET objects using a parameterized constructor.

Using the ClrCreateManagedInstance API

MSCOREE.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

Image

Image

Image

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.

Hosting Windows Forms Controls in Any ActiveX Container

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-ORed 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

Image

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

Image

Image

Image

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.

Image

9. Drag the control onto the form, as shown in Figure 10.3.

10.

Figure 10.3. Dragging a Windows Forms control onto a Visual Basic 6 form.

Image

From this point, you may have mixed results in getting such an application to work correctly, because this kind of interaction is not supported.

Working Around COM-Invisibility

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

Image

Image

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

Image

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.

Using Reflection to Invoke Static Members

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

Image

Image

Image

Image

Image

Image

Image

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.

Tip

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.

Caution

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.

Handling .NET Events

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

Image

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.

Unexpected Casing in Type Libraries

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

Image

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.

Image

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.

Tip

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_")

Advanced Shutdown Topics

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.

Conclusion

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.22.248.208