Chapter 21. Manually Defining COM Types in Source Code

In This Chapter

Using SDK Tools for Support

Manually Defining COM Interfaces

Manually Defining Coclass Interfaces and Event Types

Manually Defining COM Structures

Manually Defining COM Enums

Manually Defining COM Classes

Avoiding the Balloon Effect

In an ideal world, a .NET definition of every COM interface, class, enum, and so on, would exist somewhere in a Primary Interop Assembly that’s readily available. Furthermore, every definition would be completely usable in every .NET language, no matter how non-standard it may be. Of course, this is not the world we live in, but there are a variety of things we can do when a desired Primary Interop Assembly does not exist.

We could easily create an Interop Assembly from an existing type library containing the desired type definitions using the type library importer (or even create a Primary Interop Assembly if we’re the author of the COM component). Sometimes the produced assembly still needs some tweaks to be fully usable from managed code (such as returning success HRESULT values or using C-style array parameters), so the IL Disassembler and IL Assembler can be used to alter the Interop Assembly’s metadata. This is the subject of Chapter 7, “Modifying Interop Assemblies.”

If a type library doesn’t exist for a COM component, perhaps because it was only designed for C++ clients, one could be created from an IDL file using existing tools like MIDL.EXE or MKTYPLIB.EXE, then this type library could be imported to an assembly. If only a C++ header describes an interface, then the task of creating .NET type information using this process is even tougher because there’s no automatic way to create an IDL file from a C++ header file.

Another approach for creating .NET type information that describes COM types, the subject of this chapter, is to manually write definitions of COM types in your favorite high-level .NET language. After all, .NET compilers can produce metadata and IL, which is what the type library importer produces. There’s nothing magical about the metadata inside Interop Assemblies—they’re just marked with the appropriate custom attributes to make the CLR treat them as COM types. Because the type library importer makes an effort to generate metadata that complies with the Common Language Specification (whenever it doesn’t restrict functionality), most COM type definitions can be written in any compliant .NET language, although with some huge caveats discussed in this chapter.

If you need to use COM types that aren’t defined in a type library or IDL file, the usefulness of this technique should be obvious. But why would you want to write COM type definitions manually, if using the support of the type library importer is also an option? Here are pros and cons to manually defining COM types in source code:

PROS:

• Writing your own COM type definitions can be much more lightweight than referencing a large Interop Assembly. You can define only the types (and sometimes only the methods) that you need. For example, instead of using the Primary Interop Assembly for the Microsoft HTML Object Library (MSHTML.TLB) that’s over seven megabytes in size, perhaps you might define only a handful of interfaces that you want to use or implement. In addition, you no longer need a separate DLL containing the COM types. You could simply compile your source definitions into your own assembly.

It’s pretty easy to make modifications to the signatures to make them usable or simply more user-friendly. Because most people are more comfortable with a higher-level language like C#, Visual Basic .NET, or C++ rather than IL Assembler, it’s a lot easier to make the same kind of necessary tweaks explained in Chapter 7.

• COM types that you don’t wish to expose can remain private or internal to your assembly. Because the type library importer only generates assemblies, all COM types inside must be public to be usable by other assemblies. But if you ship an Interop Assembly with your product, you’re now exposing public COM types that you might rather not expose to your users. By placing the types in your own assembly, you can restrict their .NET visibility (besides only shipping one DLL). Using this technique, you can publicly expose a new .NET object model that is a thin wrapper over private COM types.

CONS:

• Writing your own COM type definitions from scratch is hard to do. It’s a process much like writing PInvoke signatures. You have to get every detail just right, including data types and custom attributes, (plus more requirements specific to COM Interoperability like the order you define the members) or your code may fail in completely unpredictable ways. In general, there is very little (but some) diagnostic information provided by the CLR if you don’t define the types correctly.

• Writing your own COM type definitions is undesirable for the same reasons that using Primary Interop Assemblies is encouraged, as described in Chapter 3, “The Essentials for Using COM in Managed Code.” If you expose a public COM type in your own assembly, such as the ISmartTagAction interface (used in Chapter 14, “Implementing COM Interfaces for Binary Compatibility”), its .NET identity becomes tied to your assembly. Therefore, source code COM definitions should be kept non-public as much as possible.

This chapter first looks at some .NET Framework SDK utilities that can be useful when writing your own type information. Then, the subject of manually defining COM type information is broken down into the following tasks:

• Manually defining COM interfaces (the most important task), including powerful customizations that are easy to do in high-level source code

• Manually defining coclass interfaces and event-related types, just like what the type library importer would produce

• Manually defining COM structures and enums

• Manually defining COM classes (the most difficult topic, and one with plenty of limitations)

Finally, we end the chapter with some tricks that helps to avoid defining the entire transitive closure of the types you wish to use.

Tip

When defining a COM type in managed code, do not base it solely on the original type’s definition found in documentation. Documentation often has errors, or the format isn’t appropriate for discovering the order of members. Instead, find a “real” definition somewhere in a C++ header file, IDL file, or type library, and base your definition on that instead.

Using SDK Tools for Support

If you’re manually defining COM types that are already defined in a type library or IDL file, using the type library importer and then comparing its generated definitions to your manual type definitions is a great sanity check, because you should trust that the importer generates “correct” metadata (excluding limitations discussed in Chapter 7). It’s important to realize, however, that producing identical metadata is not necessary because there can be minor differences that don’t affect the CLR’s interoperability behavior, not to mention all sorts of allowable customizations described in the upcoming “Handy Customizations” section. Depending on the types being defined, creating types whose definitions exactly match those generated by the importer can be difficult or impossible without using the IL Assembler. The process of comparing type definitions is summarized in Figure 21.1.

Figure 21.1. Comparing metadata produced by the type library importer with metadata produced by compiling high-level source code.

Image

If you don’t like the idea of scrounging through IL Assembler syntax, the Windows Forms Class Viewer (WINCV.EXE) that ships with the .NET Framework SDK might come in handy. This graphical tool displays searchable C#-ish syntax for types in any assembly. It only displays metadata—no source code. Therefore, you could take an Interop Assembly produced by the type library importer and run WINCV.EXE on it as follows:

wincv /r:InteropAssembly.dll /nostdlib

Once the window appears, you can type in names of types (or partial names of types) to search the list. This is shown in Figure 21.2.

Figure 21.2. Using the Windows Forms Class Viewer (WINCV.EXE) to browse metadata in a C#-like source representation.

Image

However, the definitions produced by this tool can be misleading. Its two greatest limitations are:

• Because the tool uses reflection to browse metadata, methods are not guaranteed to be displayed in their original order (which is necessary for interfaces).

• No custom attributes are displayed.

Other than these limitations, non-C#isms are easy to spot, like listing System.Object as an explicit base class, showing all base class methods in a derived class, or showing event accessors as separate methods.

Manually Defining COM Interfaces

As interfaces are the most important types in COM, often interface definitions are all one needs to worry about defining manually. For example, a .NET definition of a COM interface is often unavailable (yet needed) when one wants to implement it in a .NET class in order to plug into an existing architecture (the topic of Chapter 14). Another common case in which such definitions are needed but not available is when a COM interface returns a generic IUnknown or IDispatch interface and one wants to cast the resultant System.Object to a specific COM interface so members can be invoked in an early-bound fashion.

In this section, we examine the following:

• Custom attributes that turn an otherwise purely-managed interface definition to one that’s associated with a COM interface

• Defining all three types of COM interfaces

• Interface inheritance details that are easy to get wrong

• Limitations of the high-level language you might be using to define COM interfaces

• Interesting customizations that can make COM interfaces more .NET-like

Important Custom Attributes

A COM interface must always be marked with two custom attributes defined in System.Runtime.InteropServices:

ComImportAttribute. This is the single attribute that means “this is a COM type.” The name is a little confusing because it sounds like it’s only meant for types imported by the type library importer, but it must be placed on every COM type regardless of whether it was imported or manually defined. ComImport-marked types (also referred to as ComImport types or simply COM types) are never exported by the type library exporter and are only partially registered by the assembly registration process so critical registry entries aren’t overwritten. At run time, the attribute on a class instructs the CLR to call CoCreateInstance for instantiation and to call QueryInterface when casting to an interface.

GuidAttribute. This is needed to give the interface its identity from COM’s perspective. The GUID contained in the attribute is used by the CLR in QueryInterface calls.

Caution

It is critical that every COM interface and class definition is marked with ComImportAttribute. Registering an assembly with COM types that aren’t marked with ComImportAttribute or exporting its type library and registering the type library (or even viewing the type library in OLEVIEW.EXE, which registers it) can overwrite important registry entries and wreak havoc on your computer. For example, a coclass’s InprocServer32 key’s default value could be changed from the file containing the correct class factory to MSCOREE.DLL instead!

Besides these two custom attributes, a third custom attribute is often needed (and should probably always be used for clarity):

InterfaceTypeAttribute. This attribute describes which type of COM interface you’re defining—a dual interface, a dispinterface, or an IUnknown-only interface (one that does not derive from IDispatch). Although this attribute does not effect how the interface is used in managed code, it is essential for proper operation when the interface is passed to unmanaged code. The CLR uses this attribute to determine how to present a v-table to COM, so not setting the attribute’s value correctly can cause clients to call different members than they thought they were calling, with the end result being subtle incorrect behavior or hopefully a crash to alert you that something is wrong.

As mentioned in Chapter 12, “Customizing COM’s View of .NET Components,” InterfaceTypeAttribute is used with a ComInterfaceType enumeration value that can be set to one (and only one) of its three values—InterfaceIsDual, InterfaceIsIDispatch, or InterfaceIsIUnknown. InterfaceIsDual is the default that’s assumed if no InterfaceTypeAttribute exists, because most COM interfaces are dual. Remember to never use InterfaceIsIDispatch unless the interface is a pure dispinterface.

IUnknown-Only Interfaces

In addition to the three custom attributes just introduced, the COM and .NET data type conversions are just about all you need to know when defining simple interfaces that derive directly from IUnknown. Refer to Chapter 4, “An In-Depth Look at Imported Assemblies,” for more details about these conversions.

So, now it’s time for an example. Listing 21.1 contains an IDL definition of the IWMPEffects interface defined by the Windows Media Player SDK. This file gets generated when you use the Windows Media Visualization Wizard in a Visual C++ ATL COM project (available after downloading and installing the SDK from MSDN Online). The IDL file is also available on this book’s Web site. We’ll be discussing and using this interface in Chapter 24, “Writing .NET Visualizations for Windows Media Player,” so, for now, we’ll just focus on creating an appropriate .NET definition.

Listing 21.1. The IWMPEffects Interface in IDL

Image

Image

Listing 21.2 contains a translation of IWMPEffects to C# based on the rules we’ve seen so far in this chapter plus knowledge of data type conversions discussed in Chapter 4.

Listing 21.2. A Straightforward Definition of the IWMPEffects Interface in C#

Image

Tip

Make sure that you define the interface’s methods in the exact same order that they appear in the COM definition. Although the order of methods isn’t important in the pure .NET world, order is critical to COM due to dependence on v-table layout.

This listing assumes that the TimedLevel, RECT, and VisualizationCapabilities types also have an available .NET definition. TimedLevel and RECT are defined later in the “Manually Defining COM Structures” section. Because they are blittable, the marshaler pins them when transitioning to unmanaged code so the InAttribute markings in Lines 11 and 21 don’t have any effect at run time. Still, from a documentation perspective they faithfully represent the intention that the values of these parameters should not change after the method calls.

The definition of VisualizationCapabilities, an enum representing the original DWORD parameter, is deferred to Chapter 24 because it’s not relevant at this time. Notice that by not being marked with the public keyword, the interface is private, so it can be used and implemented by .NET types without being exposed to .NET types in other assemblies.

Tip

Non-public interfaces marked with ComImportAttribute, such as the IWMPEffects interface in Listing 21.2, are still “public” to COM, in that a COM client can successfully call QueryInterface on a COM-Callable Wrapper for a .NET class implementing the interface and obtain an interface pointer to such an interface. This is why implementing a non-public COM interface works when interacting with COM clients that use the interface. Marking a non-public type with ComImport works like the inverse of marking a public type ComVisible(false). Because a definition of a ComImport interface is not exported, it’s assumed that COM clients obtain a definition of the interface elsewhere. If it truly is a COM interface, it should already be defined in a type library, IDL file, or header file.

Although this should seem like a straightforward transformation of the IDL signatures to C# code, the definition in Listing 21.2 actually has three customizations that make it easier to use than the definition of IWMPEffects that would have been generated by the type library importer:

Listing 21.2 defines Render’s hdc parameter (originally an HDC type) and DisplayPropertyPage’s hwndOwner parameter (originally an HWND type) as System.IntPtr types, because we know they’ll be platform-sized integers. However, due to aliasing, the importer generates something different, as you will see in Listing 21.3.

• Although the GetCapabilities method is defined in IDL with a DWORD parameter (which would look like a long type in a type library), the interface’s documentation states that it’s really an enumeration. Therefore, we can define a VisualizationCapabilities enumeration with the documented values and make the GetCapabilities method more strongly typed by using this parameter type instead.

Listing 21.2 defines the BOOL parameter used in GoFullScreen as a System.Boolean (bool) type. And because boolean parameters map to VARIANT_BOOL COM types by default, we had to mark it with UnmanagedType.Bool. However, recall that a BOOL type in IDL becomes just a 32-bit integer in a type library, so the importer could only generate a System.Int32 type for this parameter which is definitely not as user-friendly.

Speaking of the IWMPEffects definition that would have been generated by the type library importer, let’s see what that would look like. Listing 21.3 shows this metadata in IL Assembler syntax by following these steps:

1. Run MIDL.EXE on an IDL file containing the original IWMPEffects definition inside a library statement in order to create a type library. You must use MIDL’s /tlb option, for example:

midl IWMPEffects.idl /tlb IWMPEffects.tlb

3. Run TLBIMP.EXE on the type library created in step 1 to create a temporary assembly:

tlbimp IWMPEffects.tlb /out=TempAssembly.dll

5. Run ILDASM.EXE using its /out option on the assembly created in step 2 and view the output text file in your favorite editor:

ildasm TempAssembly.dll /out=TempAssembly.il
notepad TempAssembly.il

Caution

If using the IL Disassembler to view metadata produced by the type library importer, producing an IL text file with the /out option is the most accurate way to get the information. If you’re working with the graphical mode instead, be sure to turn off the alphabetical sorting of names, otherwise you might attempt to define interface members in that order instead of the order required. This can be turned off by opening the View menu and unchecking Sort by name.

Listing 21.3. Metadata for the IWMPEffects Interface Produced by the Type Library Importer in IL Assembler Syntax

Image

Image

Image

For the specifics about IL Assembler syntax, consult Chapter 7. As you can see in Lines 3–11, the type library importer has marked the interface with InterfaceTypeAttribute and GuidAttribute. Where’s ComImportAttribute? It’s a pseudo-custom attribute, so it appears as import before the interface name in Line 1. In Lines 15 and 71, you can see that the HDC and HWND parameters are imported as by-reference _RemotableHandle types. Furthermore, Lines 18–22 mark the HDC type with ComAliasAttribute ("TempAssembly.wireHDC") and 73–77 mark the HWND type with ComAliasAttribute ("TempAssembly.wireHWND"). They are imported like this because although the IDL definition used HDC and HWND types, the types emitted into the type library are wireHDC and wireHWND due to directives in the included wtypes.idl. Furthermore, these types are both typedefs for the same _RemotableHandle type, so _RemotableHandle is the type seen in the typedef-less world of .NET. HDC and HWND are system handles that are partially remoted, meaning that they are remoted only as an integral type for both local and remote cases. Therefore, it’s safe (and much more user-friendly) to change these types to a simple IntPtr.

As previously shown in Figure 21.1, a good way to double-check your manual type definition is to compile your assembly containing the definition, view the definition in IL Assembler syntax (using ILDASM.EXE just like you did with the Interop Assembly) and compare it to the IL Assembler output from the Interop Assembly.

How does Listing 21.2 compare to Listing 21.3? Besides three parameters with different types and the interface being private instead of public, there are four major differences that can be seen by viewing the raw metadata for our C# definition:

• All of the methods defined in Listing 21.2 are marked with cil managed whereas the imported methods in Listing 21.3 are marked with runtime managed internalcall.

• The imported parameters all have explicit in/out markings with the InAttribute and OutAttribute pseudo-custom attributes, but the parameters in Listing 21.2 only have OutAttribute markings where C#’s out keyword is used and InAttribute markings where the by-reference parameters were explicitly marked in the C# source code.

• The imported string parameters are all marked with MarshalAs(UnmanagedType.BStr) but the string parameters in Listing 21.2 omit this marking.

• The methods in Listing 21.3 are all marked hidebysig, but the methods in Listing 21.2 do not get this marking.

The only other difference is that Listing 21.2 uses InterfaceTypeAttribute with an enumeration parameter whereas the type library importer uses the overload that takes a 16-bit integer. However, using the enumeration is preferred for its clarity.

Despite these differences that appear when viewing both interface definitions in IL Assembler syntax, there is no functional difference between the definitions in Listing 21.2 and Listing 21.3. The cil managed versus runtime managed internalcall markings have no effect on COM interfaces; the import designation ensures that the CLR handles these members correctly. The additional InAttribute, OutAttribute, and MarshalAs(UnmanagedType.BStr) markings all simply indicate the default behavior explicitly. The hidebysig designations, which indicate that the methods can be overridden by derived class methods with the same name and the same signature, make no difference for COM interface methods.

It is possible, however, to generate metadata that more closely resembles what is produced by the type library importer. Listing 21.4 updates Listing 21.2 with custom attributes that eliminate the first three of these four major differences. The hidebysig difference could be eliminated by adding the new keyword to each method (or Shadows in Visual Basic .NET), but then the C# compiler gives a bunch of warnings about the keyword not being required because the members aren’t hiding inherited members. Therefore, eliminating this difference is not worth the annoyance.

Listing 21.4. A Definition of the IWMPEffects Interface in C# That, from a Metadata Perspective, Closely Represents What the Type Library Importer Generates

Image

Image

The way to get the methods marked as runtime managed internalcall instead of cil managed is to use the MethodImplAttribute pseudo-custom attribute defined in the System.Runtime.CompilerServices namespace with a value set to InternalCall and its MethodCodeType named parameter set to Runtime, as shown in the listing.

Although this listing is much more cluttered with custom attributes, this definition is no better than the one in Listing 21.2. The moral of the story is to not bother with MethodImplAttributes on interface members, but some use of InAttribute and OutAttribute custom attributes might be a good idea for clarity (or to gain optimizations for non-blittable types) by more faithfully representing the data flow of parameters.

Listing 21.5 shows how to produce the same metadata as running the C# compiler on Listing 21.4, but using the Visual Basic .NET compiler. Notice that the combination of OutAttribute and ByRef is needed to produce the same effect as C#’s out keyword.

Listing 21.5. A Definition of the IWMPEffects Interface in VB .NET That, from a Metadata Perspective, Closely Represents What the Type Library Importer Generates

Image

Image

Dual Interfaces

The only difference about defining a dual interface instead of an IUnknown-only interface, besides changing the value of InterfaceTypeAttribute, is that methods on a dual interface have DISPIDs. These DISPIDs must be marked on the members you define using the DispIdAttribute, so a .NET class that implements the interface can respond properly to IDispatch.Invoke calls from COM clients that don’t obtain DISPIDs from IDispatch. GetIDsFromNames. The DISPIDs are never used when a .NET component invokes dual interface methods on a COM object because the CLR always calls through the v-table.

Listing 21.6 contains an IDL definition of IRunningAppCollection, a dual COM interface found in ComAdmin.idl, which ships with the Windows Platform SDK.

Listing 21.6. The Dual IRunningAppCollection Interface in IDL

Image

The three methods—_NewEnum, Count, and Item in Lines 11–18—have the DISPIDs DISPID_NEWENUM (-4), 1, and 2, respectively. Listing 21.7 contains a relatively simple translation to C#, making use of DispIdAttribute.

Listing 21.7. A Straightforward Definition of the IRunningAppCollection Interface in C#

Image

In Listing 21.7, Line 6 marks the interface as dual, and Lines 10, 16, and 19 apply the appropriate DISPIDs. The first two property definitions are straightforward, once you recall that marshaling directives for a property’s type must be applied to accessors, as in Line 13. Item is a non-default parameterized property, which C# doesn’t support, so Line 21 defines a get_Item accessor method instead.

Caution

COM interfaces with parameterized properties or properties with by-reference parameters cannot be appropriately defined in C# due to language restrictions. Although such a property’s accessor methods can be defined directly, clients become forced to use the accessor methods rather than a property regardless of their language.

As we’ve seen in previous chapters, a DISPID equal to –4 (DISPID_NEW_ENUM) is not any ordinary DISPID. It represents a member that returns an enumerator interface such as IEnumVARIANT in COM. The type library exporter does some special transformations and uses custom marshaling to convert an appropriate member with DISPID -4 to IEnumerable’s GetEnumerator method.

Using metadata produced by the type library importer as a guide, Listing 21.8 contains an updated definition of IRunningAppCollection that matches what the importer would produce much more closely.

Listing 21.8. A Definition of the IRunningAppCollection Interface in C# That Produces the Almost Identical Metadata as the Type Library Importer

Image

In addition to System.Runtime.InteropServices, this listing uses the System.Collections namespace for IEnumerable and IEnumerator, the System.Runtime.CompilerServices namespace for MethodImplAttribute, and the System.Runtime.InteropServices.CustomMarshalers namespace for EnumeratorToEnumVariantMarshaler. In addition, compiling the listing requires referencing CustomMarshalers.dll for the definition of EnumeratorToEnumVariantMarshaler.

Although the type library importer tends to be slightly more explicit with pseudo-custom attributes than it needs to be, it omits regular custom attributes that specify default behavior because emitting them can significantly add to the size of the Interop Assembly. This is why IRunningAppCollection is not marked with InterfaceType(ComInterfaceType.InterfaceIsDual). Instead, the TypeLibTypeAttribute is placed on the interface with the flags FDual and FDispatchable. This custom attribute is completely ignored by the CLR; it simply preserves miscellaneous type library flags found in IDL attributes for informational purposes only and therefore can safely be omitted.

To match the importer’s transformation for DISPID –4, IRunningAppCollection now derives from IEnumerable and its first member in Lines 13–21 is now GetEnumerator marked with the appropriate custom attributes. TypeLibFuncAttribute captures the fact that the original _NewEnum property was marked restricted. The new keyword not only makes the resultant metadata contain the hidebysig attribute, but eliminates a compiler warning because IRunningAppcollection.GetEnumerator hides the inherited IEnumerable.GetEnumerator.

The Count property in Lines 23–29 hasn’t changed from the previous listing except for the addition of MethodImplAttribute. Notice that the attribute belongs on accessor methods, not on properties.

The last member in Lines 31–37 uses a bizarre trick to work around the C# parameterized property limitation. C# types can have one parameterized property—the indexer. Because IRunningAppCollection has only one parameterized property and because its semantics are appropriate for an indexer, we simply make it an indexer. This has the advantage of making it a full-fledged property with an associated accessor method rather than simply a method. The remaining issue is that default properties are emitted with DISPID 0 because that DISPID represents a default property to COM, but the Item property must have a DISPID equal to 2. Fortunately, the DISPID can be overridden with the DispIdAttribute, which is done in Line 31. Because C# generates a property called Item for an indexer, its metadata ends up matching that produced by the type library importer. The result is a non-default property (from COM’s perspective) that happens to be treated like a default property in .NET.

Tip

Most of the time, an Item property on a COM interface is naturally suited to be a default property (indexer). If it’s not marked with DISPID 0, it could simply be an oversight by the interface’s designer.

The only differences between Listing 21.8 and what the importer would generate for IRunningAppCollection are as follows. In Listing 21.8:

• The interface is private

• Custom attributes that accept either enums or plain integers are used with the enum values

• The second and third properties are not marked with hidebysig

• The interface has a DefaultMemberAttribute custom attribute listing the Item property (emitted automatically by the C# compiler).

All occurrences of MethodImplAttribute, TypeLibTypeAttribute, and TypeLibFuncAttribute could be removed for a much more readable yet functionally equivalent definition.

Listing 21.9 shows a VB .NET definition of IRunningAppCollection equivalent to the definition in Listing 21.8, but omitting the unnecessary MethodImplAttribute for readability and leaving the Item property as non-default because VB .NET can handle this kind of property. Of course, the Item property could still be made into a default property if desirable, but here it’s left alone to more accurately represent the original interface.

Listing 21.9. A Definition of the IRunningAppCollection Interface in Visual Basic .NET

Image

Caution

Version 7.0 of the Visual Basic .NET compiler does not provide a way to mark a property setter’s parameter with MarshalAsAttribute when that property is defined on an interface. Notice how Line 26 in Listing 21.9 places MarshalAsAttribute directly on the property’s return type since Visual Basic .NET doesn’t allow explicitly declaring get and set accessors on an interface’s property. In this example, the compiler does the right thing and places the attribute on the get accessor method’s return type. Had the property not been read-only, there would be no VB .NET syntax for defining it correctly.

Just as with a DISPID value of -4, DISPIDs equal to 0 (DISPID_VALUE) should be treated specially to ensure that an interface’s semantics are preserved in .NET. Besides preserving its DISPID of 0, this means marking such a member as a .NET default member. In C#, this can be accomplished by creating an indexer, as we did for the IRunningAppCollection.Item property. Marking the indexer with DispId(0) is not strictly necessary because the C# compiler gives an indexer this DISPID by default.

Tip

If you want to define a C# indexer that gets emitted with a name other than Item, you can use IndexerNameAttribute defined in the System.Runtime.CompilerServices.CSharp namespace to choose a different name.

In Visual Basic .NET, you can define a default property with the Default keyword as follows:

Public Interface IHaveADefaultProperty
  Default Property Item(ByVal i As Integer) As Integer
End Interface

C++ doesn’t have built-in syntax for creating a default member, so the only way to define a default property is the “raw” approach of marking the class with DefaultMemberAttribute. This might look like the following:

#using <mscorlib.dll>
using namespace System::Reflection;
using namespace System::Runtime::InteropServices;

[DefaultMember("Item")]
public __gc __interface IHaveADefaultProperty
{
  [DispId(0)] __property int get_Item(int i);
};

Dispinterfaces

As with dual interfaces, the definition of a dispinterface (a.k.a. dispatch-only interfaces) must contain the DispIdAttribute on every member. When managed code calls members on a dispinterface implemented by a COM object, the CLR can avoid a call to GetIDsOfNames by obtaining the necessary DISPIDs directly from metadata.

Unlike other interfaces, the ordering of dispinterface members doesn’t matter because all method invocations are done via IDispatch. Listing 21.10 contains an IDL representation of the AddressLists dispinterface defined by the Microsoft CDO 1.21 Library (CDO.DLL).

Listing 21.10. The AddressLists Dispinterface in IDL

Image

Listing 21.11 contains a suitable definition of this interface in C++ with Managed Extensions.

Listing 21.11. A .NET Definition of AddressLists in Visual C++ .NET

Image

Line 9 contains the necessary InterfaceIsIDispatch marking that all dispinterfaces must have.

Caution

Never mark an interface with InterfaceIsIDispatch unless it’s a pure dispinterface. Although using InterfaceIsIDispatch on a dual interface is the only misuse of InterfaceTypeAttribute that works without errors, making this mistake always forces late binding. Because the CLR enables you to call methods of a dispinterface just like any other interface (hiding the late binding as in Visual Basic .NET), there are no warning indicators for making this mistake except for poor performance.

At first it might catch you by surprise that the listing defines a get_Item property accessor in Line 20 rather than an Item method. However, although the definition of Item in Listing 21.10 is listed under the methods section, notice the propget attribute on the method that makes it a property.

Each DISPID in Listing 21.11 is only marked on one of each property’s accessor methods. This is done because the C++ compiler knows to place the attribute on the property instead of the accessor for custom attributes that can be placed on a property. If the listing placed a DispIdAttribute on both of a property’s accessors, the property would end up with two DispIdAttributes in metadata. This is harmless, but it’s better to not cause the duplicate definitions.

As in C# and Visual Basic .NET, the C++ method definitions don’t have hidebysig set by default. Besides slightly different property metadata produced by the C++ compiler that doesn’t affect the use of the COM interface, there are no surprises in the metadata generated for this definition.

Caution

The Visual C++ .NET compiler doesn’t emit metadata for a private interface such as the one in Listing 21.11 unless it is used somewhere, such as implemented by a public class. Therefore, to check the metadata of Listing 21.11, you should make it public or add a type that uses the interface.

Interface Inheritance

The COM interfaces we’ve examined so far inherit directly from IUnknown or IDispatch. Special care is needed when defining a COM interface that directly inherits from an interface other than these famous two.

When constructing a v-table to expose to COM, the CLR uses only the methods of IUnknown, the methods of IDispatch (unless InterfaceTypeAttribute marks it as an IUnknown-only interface), plus the methods defined directly on the interface type. No methods of base interfaces with a .NET definition are considered. Because of this, all base interface methods must be duplicated on the definition of a derived interface. As mentioned in Chapter 4, the type library importer does exactly that when defining interfaces. (One could imagine a ComInterfaceType.Derived value that could be used on interfaces to direct the CLR to include base interface methods in the v-table rather than having to redefine the methods. Unfortunately, no such value exists.)

Caution

It’s easy to forget to redefine base interface members on a derived interface because simply expressing the inheritance relationship, such as the following in C#, exhibits the expected compile-time behavior:

interface IProvideClassInfo2 : IProvideClassInfo

At run time, however, such an omission can cause catastrophic failures for such COM interfaces.

Listing 21.12 contains the definitions of two well-known COM persistence interfaces defined in objidl.idl that are related via inheritance—IPersist and IPersistStream.

Listing 21.12. The IPersist and IPersistStream Interfaces in IDL

Image

Listing 21.13 demonstrates how to properly provide .NET definitions of these two interfaces in C#.

Listing 21.13. C# Definitions of IPersist and the Derived IPersistStream Interfaces

Image

This listing contains four important customizations that the type library importer could not have done. One is in Line 29—preserving the boolean-ness of the fClearDirty parameter just like what was done with the IWMPEffects interface in Listing 21.2. A second customization is done in Line 30, making the parameter type of pcbSize a simple long rather than ULARGE_INTEGER. The ULARGE_INTEGER type is also a 64-bit type, but is a union of a 64-bit integer and two 32-bit integers (to accommodate compilers that can’t handle 64-bit types):

typedef union _ULARGE_INTEGER {
  struct {
      DWORD LowPart;
      DWORD HighPart;

  };
  ULONGLONG QuadPart;
} ULARGE_INTEGER, *PULARGE_INTEGER;

Unions don’t natively exist in .NET (and would have to be treated like a regular structure with distinct fields), so replacing the type with a simple 64-bit integer is not only much more convenient, but safe for all .NET languages because 64-bit signed integers are included in the Common Language Specification (CLS).

Another customization is the use of the UCOMIStream type in Lines 27 and 28. This is used for convenience because the .NET Framework already provides a .NET definition of the IStream COM interface, but it’s also important because it’s the “Primary Interop Definition” of IStream so it’s the type that people should use to represent IStream. The type library importer would have used a different definition of IStream originating from wherever the input type library obtained its definition.

The most vital customization is the use of PreserveSigAttribute in Line 25. IPersistStream.IsDirty only returns either S_OK or S_FALSE HRESULT values, so making this change is the only way the method can be useful in managed code. Technically, the integer return value should be marked with MarshalAs(UnmanagedType.Error), but omitting it is safe.

Line 22 contains the base GetClassID method redefined in the derived interface. Notice that the new keyword is required (or Shadows in Visual Basic .NET) to prevent a compilation warning about hiding an inherited member. If you define a COM interface that derives from IPersistStream, the members of both IPersistStream and IPersist would have to be redefined on that interface, and so on.

Tip

You might be inclined to omit the inheritance relationship when defining a COM interface (for example, having IPersistStream derive directly from IUnknown) because the base methods are copied anyway and you don’t have to deal with the mess of multiply defined members. However, don’t omit the relationship because it’s still important for proper operation on both .NET and COM sides. Not only does it provide the expected behavior in .NET clients using such interfaces (such as implicitly converting an IPersistStream type to an IPersist type, but for COM as well because a CCW makes QueryInterface calls on the derived interface succeed for any of its base interfaces. If the definition of IPersistStream didn’t show it deriving from IPersist, the only way for a .NET class to respond successfully to COM QueryInterface calls for both interfaces would be to implement both interfaces, and the class would still have to deal with duplicate members.

Working With Language Limitations

Because most COM interfaces were designed without regard to the yet-to-be invented Common Language Specification for the CLR, not every interface can be accurately represented in your language of choice. Many were designed to be OLE Automation-compatible, a similar notion, but these two subsets of functionality have some areas where they don’t overlap. Most notably, optional parameters and parameterized properties are OLE Automation-compatible yet the CLS doesn’t require languages like C# to support defining them.

For an example of a troubling interface, look at the IHTMLStyleSheet interface defined in the Microsoft HTML Object Library (MSHTML.TLB). This interface is shown in Listing 21.14 as it’s defined in the IDL file MSHTML.IDL.

Listing 21.14. The IHTMLStyleSheet Interface Defined in IDL

Image

Image

Because Listing 21.14 shows the contents directly from an IDL file, there are some non-standard markings that would show up differently when viewed inside a type library. For example, this interface has two optional parameters—lIndex in the addImport and addRule methods. But rather than being marked [optional, defaultvalue(-1), in] they are only marked [defaultvalue(-1), in]. Both are treated as equivalent by MIDL, which can be seen by viewing this interface’s definition by running OLEVIEW.EXE on the MSHTML.TLB type library.

Every DISPID is marked with a constant value defined in an included header file, such as DISPID_IHTMLSTYLESHEET_RULES. (If you’re defining a .NET interface based on an IDL definition, you’ll sometimes need to search through header files to get all the necessary information.) The non-standard practice of listing the IDL in and out attributes last rather than first (such as retval, out) does not make a difference. However, the non-standard practice of listing a property’s set accessor (propput) before its get accessor (propget) does give us a problem, which is discussed in the following section.

Most language limitations encountered when defining COM interfaces revolve around properties. For example, although Visual Basic .NET permits calling properties with by-reference parameters (as sometimes used in COM interfaces), it does not permit defining such a property. Also, when placing a custom attribute such as MarshalAsAttribute on a VB .NET interface property, the compiler only applies it to the get accessor and not the set accessor. This is why the previous chapter manually defined the IFont COM interface in C# for the font custom marshaler (whose source is available on this book’s Web site); some boolean properties required MarshalAs(UnmanagedType.Bool) on their set accessors. (An alternative definition could have been written in Visual Basic .NET using plain integers instead, but it would not be as user-friendly.)

The next two sections examine the following frequently-encountered limitations that are both demonstrated by IHTMLStyleSheet in Listing 21.14:

• Location and ordering of property accessors

• Optional parameters and default values

Location and Ordering of Property Accessors

It’s natural to want to define a property like IHTMLStyleSheet.title as follows inside a Visual Basic .NET interface definition:

Image

' Incorrect property definition
<DispId(DispIds.IHTMLSTYLESHEET_TITLE)> _
Property title As String

(This assumes the existence of a DispIds class defining the appropriate constants.) This code is not correct, however, because properties defined in VB .NET interfaces always get emitted into metadata with the get accessor method before the set accessor method, as can be seen using the IL Disassembler. The metadata for the preceding property looks like the following after being compiled then viewed in ILDASM.EXE:

  .method public newslot specialname virtual abstract
          instance string  get_title() cil managed
  {
  } // end of method IHTMLStyleSheet::get_title

  .method public newslot specialname virtual abstract
          instance void  set_title(string Value) cil managed
  {
  } // end of method IHTMLStyleSheet::set_title

  .property string title()
  {
    .custom instance void [mscorlib]
      System.Runtime.InteropServices.DispIdAttribute::.ctor(int32) =
      ( 01 00 E9 03 00 00 00 00 )
    .get instance string IHTMLStyleSheet::get_title()
    .set instance void IHTMLStyleSheet::set_title(string)
  } // end of property IHTMLStyleSheet::title

The definition of this property contains three parts—a get accessor, followed by set accessor, followed by the property itself. The location of the property is not important for the layout of the COM interface because COM only sees the accessors. But the location and ordering of the get and set accessors on an interface are critical, unless it’s a dispinterface. Therefore, the two options for defining the IHTMLStyleSheet interface in Visual Basic .NET are:

• After compilation, use the IL Disassembler to generate an IL file, switch the order of get_title and set_title, then reassemble the file into an assembly.

• Make the definition less convenient for .NET clients by changing the title property into two methods, as follows:

<DispId(DispIds.IHTMLSTYLESHEET_TITLE)> _
Sub SetTitle(title As String)

<DispId(DispIds.IHTMLSTYLESHEET_TITLE)> _
Function GetTitle()As String

This way the accessors can be defined in the correct order for the interface’s v-table—set before get. This is only compatible with the original COM interface when v-table binding, however, because IDispatch and Type.InvokeMember require different flags to be passed when invoking a method versus invoking a property accessor.

C# and C++, on the other hand, enable you to control the ordering of the accessors for an interface’s property as follows:

C#:

[DispId(DispIds.IHTMLSTYLESHEET_TITLE)]
string title { set; get; }

C++:

[DispId(DispIds.IHTMLSTYLESHEET_TITLE)]
__property void set_title(String* title);
__property String* get_title();

Both of these properties have their set accessors emitted to metadata before their set accessors. The implementer of an interface containing properties doesn’t need to worry about the order of the get and set accessors in the class definition. As long as the interface through which COM communication is done exactly matches the COM definition, it works.

If a COM interface defines property accessors that are not listed consecutively (in other words, there’s an unrelated method between two accessors) then you’d have to resort to the two workarounds listed earlier or use C++ with Managed Extensions because no .NET language besides C++ lets you define a property with accessors in arbitrary locations.

Non-contiguous property accessors occur more often than you might imagine due to the transformation done when three property accessors exist (Get, Let, and Set, also known as propget, propput, and propputref). In the ActiveX Data Objects (ADO) type library, the _Recordset interface has the following property:

[id(0x000003e9), propputref]
HRESULT ActiveConnection([in] IDispatch* pvar);
[id(0x000003e9), propput]
HRESULT ActiveConnection([in] VARIANT pvar);
[id(0x000003e9), propget]
HRESULT ActiveConnection([out, retval] VARIANT* pvar);

In IL Assembler syntax, the type library importer generates the metadata shown in Listing 21.15.

Listing 21.15. Metadata Generated by the Type Library Importer for the _Recordset.ActiveConnection Property, Shown in IL Assembler Syntax

Image

The importer necessarily generates the three property accessors in the same order as defined in the type library. (It generates the property itself at the end of the interface because its location doesn’t matter.) No .NET language, however, enables you to specify an other accessor as the type library importer does. Had the Let accessor (propput) been defined as the last of the three accessors, you could simply define a let_ActiveConnection method immediately after the property with the set and get accessors (but again, only if clients of the interface use v-table binding, and not if the interface is defined in VB .NET because the set accessor must come first). In C#, this would look like:

Image

// This would have been correct if the unmanaged Let accessor were defined last
[DispId(1001)]
object ActiveConnection
{
  [MarshalAs(UnmanagedType.IDispatch)] set;
  get;
}
[DispId(1001)]
void let_ActiveConnection(object pvar);

But this does not work for the ActiveConnection property because the Let accessor is in the middle. The previous C# code could be compiled and then corrected using the IL Disassembler and IL Assembler and switching the order around as a post-processing step. Or, the COM property could be defined as three methods in your language of choice (here again in C#):

[DispId(1001)]
void set_ActiveConnection([MarshalAs(UnmanagedType.IDispatch)] object pvar);
[DispId(1001)]
void let_ActiveConnection(object pvar);
[DispId(1001)]
object get_ActiveConnection();

This .NET version of ActiveConnection is easily definable in any .NET language, but only appropriate for v-table binding (so the fact that the methods are marked with DISPIDs is misleading). In addition, the user experience suffers by having to call all three accessors explicitly.

Optional Parameters and Default Values

Going back to the IHTMLStyleSheet interface from Listing 21.14, it contains two methods with optional parameters—addImport and addRule. These could be defined in Visual Basic .NET as follows:

<DispId(DispIds.IHTMLSTYLESHEET_ADDIMPORT)> _
Function addImport(bstrURL As String, Optional lIndex As Integer = -1) _
  As Integer

<DispId(DispIds.IHTMLSTYLESHEET_ADDRULE)> _
Function addRule(bstrSelector As String, bstrStyle As String, _
  Optional lIndex As Integer = -1) As Integer

There’s no problem with this, but due to the previously discussed problems with defining IHTMLStyleSheet’s properties in VB .NET, you might opt to define the interface in C# instead.

Ah, but C# doesn’t support optional parameters. No problem—System.Runtime.InteropServices defines the OptionalAttribute pseudo-custom attribute that can be used in languages like C#. This attribute sets the exact same metadata bit as VB .NET does with its Optional keyword. C# clients still couldn’t call the method while omitting the optional parameter, but VB .NET clients could. Using OptionalAttribute gives the following two definitions in C#:

Image

// Incorrect definitions because they don't account for default values
[DispId(DispIds.IHTMLSTYLESHEET_ADDIMPORT)]
int addImport(string bstrURL, [Optional] int lIndex);

[DispId(DispIds.IHTMLSTYLESHEET_ADDRULE)]
int addRule(string bstrSelector, string bstrStyle, [Optional] int lIndex);

But there’s still a problem—there’s no way to place the default value of –1 in the signature in C#. The same goes for signatures defined in C++. This could, again, be “fixed up” by using the IL Disassembler to produce an IL file from the C#-generated assembly, by changing this:

Image

to this:

Image

and then using the IL Assembler to reassemble the DLL. This process adds the same default value information that the type library importer would add.

Tip

Using OptionalAttribute without specifying a default value can only be done safely on System.Object types that were VARIANTs in the COM definition. In this case, compilers like VB .NET fill in a Type.Missing type for the parameters omitted by the client code and COM Interoperability maps the value appropriately. For other types, the value passed depends on the implementation of the client’s compiler, so you can’t count on it being the desired default value.

Finally, Listing 21.16 defines the complete IHTMLStyleSheet in C#. Because the optional parameters’ default values can’t be given directly in the C# definition, OptionalAttribute is omitted altogether. Using it alone can cause subtle incorrect behavior, so either both the optional marking and default value could later be added in a custom disassemble/assemble step or both can be safely omitted (although less convenient for VB .NET clients).

Listing 21.16. The IHTMLStyleSheet Interface Defined in C#

Image

Image

This listing assumes that the COM interfaces IHTMLElement, IHTMLStyleSheetsCollection, IHTMLStyleSheetRulesCollection, and IHTMLStyleSheetPagesCollection have also been given a suitable .NET definition.

Handy Customizations

The .NET definitions created so far have some useful customizations regarding parameter types, exposing HRESULT values, and omitting custom attributes that aren’t strictly necessary. However, many more customizations can be made to give COM interfaces more of a “.NET feel” by morphing them to follow .NET design guidelines, all the while staying within the rules that produce correct run time behavior.

Tip

Any customizations made in this chapter can also be made when customizing an Interop Assembly using the techniques in Chapter 7 and vice-versa. Some different ones are mentioned here simply because making changes to higher-level source code is much easier and less error-prone for most people. Also, the customizations in Chapter 7 focused on necessary changes, whereas the customizations made in this section border on being frivolous.

One interesting customization is that names of types, members, and parameters can be changed. Because the real “name” of a COM class or interface is its GUID, the corresponding .NET type name can be changed without consequence. A great example of this is all the UCOM... types in the System.Runtime.InteropServices namespace. The price to pay of changing COM type names is potential confusion by making the connection to the original COM type less obvious. (Many people don’t realize at first that the UCOMIStream interface and the familiar IStream interface are one and the same.)

As for changing member and parameter names, this can only be done safely for IUnknown-only interfaces because late binding clients rely on invoking by member names and sometimes even specifying parameter names (when users invoke members using named parameters). Furthermore, because type information is never exposed for a managed type marked with ComImport-Attribute, any new COM clients written will never use your new definition of a COM interface, so they won’t see your changed names.

Caution

Even changing member and parameter names on IUnknown-only interfaces can produce undesirable results. For example, suppose that a COM client late binds to a .NET class that implements a .NET IPersistStream interface with renamed methods. Although the IPersistStream interface doesn’t support late binding, the same methods appear on the class interface (if they weren’t privately implemented). If a COM client expects to dynamically invoke the members of IPersistStream through the class interface, this would fail if, for example, the GetSizeMax method were renamed to GetMaxSize. Of course, because the class interface has no contractual relationship with IPersistStream, this behavior might be acceptable to you. If not, then only do such renaming on non-public IUnknown-only COM interfaces that are always privately implemented. This ensures that no COM clients would be able to late bind to such members.

So let’s take a look at how we can legally “.NET-ize” the members of IPersistStream. By staying faithful to their original definitions, the methods look like the following in C#:

void GetClassID(out Guid pClassID);

[PreserveSig] int IsDirty();

void Load([In] UCOMIStream pStm);

void Save([In] UCOMIStream pStm,
  [In, MarshalAs(UnmanagedType.Bool)] bool fClearDirty);

void GetSizeMax(out long pcbSize);

The parameter names certainly don’t follow .NET design guidelines, so we can easily change those. In addition, there’s a neat trick we can do to make GetClassID and GetSizeMax more user-friendly. The IDL definitions of these methods were:

HRESULT GetClassID([out] CLSID *pClassID);

HRESULT GetSizeMax([out] ULARGE_INTEGER *pcbSize);

Had they been defined as:

HRESULT GetClassID([out, retval] CLSID *pClassID);

HRESULT GetSizeMax([out, retval] ULARGE_INTEGER *pcbSize);

then we would have initially written their .NET definitions as:

Guid GetClassID();

long GetSizeMax();

which is much cleaner to use in managed code. Fortunately, there’s no functional difference between the two sets of IDL signatures just shown (when v-table binding). In IDL, marking the final parameter with retval (assuming it’s already marked out and is a pointer) is simply a convention to convey the intention that the parameter represents a return value. The type library importer follows the rules and only changes the last parameter to a return value when it is marked with retval. For cases in which the retval marking is omitted but could be validly used, however, you can decide for yourself whether or not to treat the parameter as a return value. If you look back at the manual COM interface definitions shown in Chapter 14, this trick is used a few times.

Also, consider what would happen if the GetClassID method were originally defined as follows in IDL:

[propget]
HRESULT GetClassID([out, retval] CLSID *pClassID);

Although this definition changes the method to a property get accessor, which looks much different (and perhaps more appealing) in managed code, there is no difference between this definition and the original one from COM’s perspective when only calling it via v-table binding.

Tip

Out parameters can be transformed into return values if they are the method’s last parameter, making the method easier to use in .NET. Methods can even be turned into properties so long as the accessor methods have the same signatures and occupy the same v-table slots as the original COM methods.

Just as with changing the names of members or parameters, any of these changes should not be done when late binding is possible. These changes break the interface’s contract via late binding because IDispatch treats return values and member types (method versus property) specially.

Using all these transformations, Listing 21.17 contains an update to the IPersist and IPersistStream interfaces defined in Listing 21.13.

Listing 21.17. .NET-Friendly C# Definitions of the IPersist and IPersistStream Interfaces

Image

Lines 11 and 22 change the GetClassID method to a Clsid property using both the retval and method-to-property trick. Line 25 changes the IsDirty method to an IsDirty property, and Line 29 changes the GetSizeMax method to a MaxSize property using both tricks used for GetClassID. The parameters for the Load and Save methods were also updated to better match .NET design guidelines.

The one member that is still not ideal is the IsDirty property because it returns a 32-bit integer (0 for S_OK or 1 for S_FALSE) instead of a boolean type that a .NET client might expect. We could change the type to [MarshalAs(UnmanagedType.Bool)] bool, but the values returned would be represented incorrectly. Non-zero (S_FALSE) would mean true whereas zero (S_OK) would mean false, but the IsDirty method returns S_FALSE if the object is not dirty and S_OK if the object is dirty—the exact opposite meaning. To get around this, we could change the property name to IsClean or IsNotDirty and have it successfully return a boolean type! Of course, such a change may be considered straying too far from the original COM interface.

Listing 21.18 updates the IWMPEffects interface defined at the beginning of the chapter. Compared to Listing 21.2, this new definition contains several customizations that make it more .NET–friendly.

Listing 21.18. A .NET-Friendly Update to the IWMPEffects Interface Defined in Listing 21.2

Image

Just about every parameter is renamed from the original names seen in Listing 21.2. Besides this, the GetTitle, GetPresetCount, SetCurrentPreset, and GetCurrentPreset methods are all transformed into properties without the Get/Set prefix. The unique aspect to these transformations is that a pair of methods—SetCurrentPreset and GetCurrentPreset—is transformed into a single property with two accessor methods. The original definition of IWMPEffects has the SetCurrentPreset method occurring first in the interface, so it’s crucial that the order of the .NET property accessors match in metadata. Fortunately, by listing set before the get (Line 17), the C# compiler emits the accessors in that order.

The only other difference from Listing 21.2 is that GoFullscreen has been renamed to GoFullScreen and GetPresetTitle method’s second parameter is now a return value. GetPresetTitle could have been made into a parameterized property but because C# only supports this for a single indexer and GetPresetTitle is not appropriate for an indexer, it’s left as a method.

Tip

If you want to manually create a Primary Interop Assembly in source code, simply add GuidAttribute and PrimaryInteropAssemblyAttribute to your assembly with the appropriate values. For example (in C#):

// Specify the LIBID of the corresponding type library [assembly:Guid("29527e1e-689a-45f8-a035-7878b75d98cd")]// Specify which version of the type library this applies to// (Major, Minor)[assembly:PrimaryInteropAssembly(1, 0)]

When registering an assembly with these custom attributes, the extra PIA-specific values are added to the registry. Although REGASM.EXE can recognize multiple PrimaryInteropAssemblyAttribute custom attributes on the same assembly in order to register a PIA for multiple versions of a type library, you cannot use multiple PrimaryInteropAssemblyAttribute attributes in a high-level language because the attribute is not marked with AllowMultiple set to true. You can only do this in a low-level language like IL Assembler.

Manually Defining Coclass Interfaces and Event Types

If your goal is to write source code that produces metadata as close as possible to what the type library importer would produce, you might want to create a coclass interface. For simple coclasses, a coclass interface is essentially nothing more than a renamed default interface. But for a coclass that lists source interfaces, the coclass interface is the most intuitive means through which a client can hook and unhook event handlers, demonstrated in Chapter 5, “Responding to COM Events.” In this section, we’ll first look at how to define a coclass interface for a coclass without a source interface, then we’ll look at defining a coclass interface that inherits all the importer-style event-related types for a coclass with a source interface.

For the first example, we’ll use the Microsoft HTML Object Library, which defines the following

HTMLStyleSheet class:

[
  uuid(3050f2e4-98b5-11cf-bb82-00aa00bdce0b)
]
coclass HTMLStyleSheet
{

  [default]  dispinterface DispHTMLStyleSheet;
                 interface IHTMLStyleSheet;
                 interface IHTMLStyleSheet2;
};

For classes like this without source interfaces, defining a coclass interface is straightforward. After defining the default interface, all that is required is defining an interface with the coclass name that derives from the default interface. This interface must be marked with three custom attributes—ComImportAttribute, GuidAttribute, and CoClassAttribute. GuidAttribute, in this case, contains the same IID as the default interface (if no interface is marked default, it’s the first one listed). CoClassAttribute, recognized by C# and VB .NET, links the coclass interface to the .NET class definition. Unlike other interfaces, marking a coclass interface with InterfaceTypeAttribute is unnecessary because it has no members.

Listing 21.19 defines the default DispHTMLStyleSheet interface and the HTMLStyleSheet coclass interface in Visual Basic .NET. DispHTMLStyleSheet is a dispinterface that contains all the same members as IHTMLStyleSheet plus two more (that are also defined on IHTMLStyleSheet2). Because the troubling property layout (setter before getter) doesn’t matter for dispinterfaces, this interface can be defined correctly in VB .NET. It can’t be correctly defined in C# or C++, however, because some members have optional parameters with default values.

Listing 21.19. The HTMLStyleSheet Coclass Interface and the Default Interface It Derives from, Both Defined in Visual Basic .NET

Image

Image

Image

This listing, like Listing 21.16, requires .NET definitions of IHTMLElement, IHTMLStyleSheetsCollection, IHTMLStyleSheetRulesCollection, and IHTMLStyleSheetPagesCollection. It also requires the definitions of IHTMLStyleSheet and DispIds from Listing 21.16, plus the definition of HTMLStyleSheetClass—the .NET class as generated by the type library importer. Creating such a class is discussed in the “Manually Defining COM Classes” section.

Caution

The Visual C# .NET compiler (version 7.0) has a limitation regarding coclass interfaces defined in source code. If you instantiate a class using its coclass interface, and if the coclass interface is defined in a different file of the same project, you must make sure that the file defining the coclass interface is compiled before the file using the coclass interface. From a command prompt, this means doing the following:

csc DefinesCoclassInterface.cs UsesCoclassInterface.cs

rather than:

csc UsesCoclassInterface.cs DefinesCoclassInterface.cs

The latter would cause an error as if the coclass interface is just a regular interface, with the message:

Cannot create an instance of the abstract class or interface 'InterfaceName'.

For an example of a coclass that lists a source interface, let’s look at the NetMeeting coclass defined by the Microsoft Windows NetMeeting 3 SDK in netmeeting.idl:

[
  uuid(3E9BAF2D-7A79-11D2-9334-0000F875AE17),
  helpstring("NetMeeting Application")
]
coclass NetMeeting
{
  [default] interface INetMeeting;
  [default, source] dispinterface _INetMeetingEvents;
};

Listing 21.20 contains the IDL definitions of the two short interfaces listed by the NetMeeting coclass: the INetMeeting default interface and the INetMeetingEvents source interface (a dispinterface).

Listing 21.20. The _INetMeetingEvents Dispinterface and INetMeeting Interface

Image

Image

Because these types are meant for scripting, they are also defined in the NetMeeting 1.1 type library, embedded in CONF.EXE in your Program FilesNetMeeting directory.

Besides defining the _INetMeetingEvents and INetMeeting interfaces, the NetMeeting coclass interface, and the NetMeetingClass class, the type library importer would also define the following .NET types if importing the NetMeeting coclass (described in Chapter 5).

__INetMeetingEvents_Event—An interface just like _INetMeetingEvents but with event members rather than plain methods.

__INetMeetingEvents_ConferenceStartedEventHandler and __INetMeetingEvents_ ConferenceEndedEventHandler—The delegate types, one for each member originally defined in _NetMeetingEvents.

__INetMeetingEvents_EventProvider—A private class that implements each event’s add and remove accessors and handles the interaction with COM connection point interfaces.

__INetMeetingEvents_SinkHelper—A private sink class that implements the source interface and raises the events when its methods are called.

The interesting detail about the last two classes is that these are the only kind of types generated by the type library importer that contain IL instructions. So when re-creating these types in source code, we need to write some implementation besides just type definitions. Because Chapter 5 discusses the details of what these types do, how they work, and how to use them, we’ll focus here mainly on defining these types in source code so the coclass interface can be fully defined. Listing 21.21 defines 8 of the 9 types that would result from importing the NetMeeting coclass and the two interfaces from Listing 21.20. The only type omitted is the NetMeetingClass class.

The type and member names used in Listing 21.21 match those that would be produced by the type library importer. The importer chooses names that can be predictably generated with little chance of name conflicts. However, because these names are fairly lengthy (and at times don’t coincide with .NET design guidelines), it’s certainly appropriate to rename these types (and members inside the sink helper and event provider classes) to be more user-friendly.

Listing 21.21. C# Definitions of the NetMeeting Coclass Interface, Both Interfaces Listed by the NetMeeting Coclass, and Event-Related Types

Image

Image

Image

Image

Image

Image

Image

Image

Image

Listing 21.21 assumes that the NetMeetingClass class type is defined. We’ll be defining this in the upcoming “Manually Defining COM Classes” section. In addition to the System and System.Runtime.InteropServices namespaces, Line 2 lists the System.Collections namespace because the event provider class uses an ArrayList to manage the list of event sinks.

Lines 6–11 define the NetMeeting coclass interface. This is just like the HTMLStyleSheet defined previously, except that it also derives from the _INetMeetingEvents_Event event interface. Lines 13–25 define the raw source interface, which is a straightforward translation. Lines 28–46 define the INetMeeting default interface. This, too, is straightforward but with one customization—the return type of IsInConference is defined as a boolean type in Line 41, preserving the original method’s intent. This is the only place in the listing that is noticeably different from what would be generated by the type library importer, and fortunately it’s an improvement.

Lines 49–59 define the event interface, with an event for each of the two methods of the _INetMeetingEvents source interface. This interface is marked COM-invisible simply because there’s no need to expose it to COM. This is the type that must be marked with ComEventInterfaceAttribute. This attribute must be given the type of the raw source interface followed by the type of the event provider class. The CLR uses this to magically associate the event implementation to these event members because the COM objects to which clients appear to be hooking and unhooking event handlers certainly does not implement them. (Note that this does not refute the statement at the beginning of the chapter that “There’s nothing magical about the metadata inside Interop Assemblies.” The metadata just contains a regular custom attribute; the magic is inside the CLR!)

Lines 62–66 define the two delegate types for the two source interface methods. If a source interface method has parameters or a return type other than void, the corresponding delegate type must have matching parameters and return type.

Lines 69–102 contain the sink helper class—the first (and simpler) of the two private classes containing implementation. The sink helper class implements the raw source interface and converts the method calls into invocations on its delegate fields; in essence, the sink helper raises the events. On Line 79, the class defines a public cookie field. This is used by the event provider class to store the cookie returned by a call to IConnectionPoint.Advise, and used again when calling IConnectionPoint.Unadvise. The constructor simply sets the three fields to initial values, and the implementation of each source interface’s method simply invokes the corresponding delegate if it is a valid instance. All three fields are public, set by the event provider. Inside the sink helper is the other place that would change if any of the source interface methods had parameters. These parameters would simply be passed to the delegate invocation.

The _INetMeetingEvents_EventProvider class begins on Line 105 and occupies the rest of the listing. Whereas the sink helper class implements the raw source interface, the event provider class implements the event interface (plus System.IDisposable). Fortunately, we can use the pre-defined UCOMIConnectionPointContainer and UCOMIConnectionPoint types when defining the fields in Lines 108 and 109.

The constructor, called when instantiated by the CLR, initializes its connection point container field to the passed-in object. The connection point field isn’t initialized until the Init method (Lines 120–126) is called. This occurs the first time a .NET client attempts to hook up to an event member (rather than when the COM class is instantiated).

Lines 129–135 define the implementation of IDisposable.Dispose, and Lines 138–141 define the implementation of the class’s finalizer. Both methods make the same call to Cleanup, but Dispose also calls System.GC.SuppressFinalize in Line 134. This is the standard pattern of disabling finalization if the client remembered to call Dispose to eagerly clean up the object’s state. The code that would be generated by the type library importer doesn’t define a Cleanup method. Instead, it simply calls Finalize directly from Dispose. However, C# doesn’t allow calling the class’s finalizer, hence the use of a separate method containing the common code.

The job of Cleanup (Lines 144–168) is to remove each sink helper from the class’s ArrayList and call Unadvise on each one to “unhook” the event handler. The lock(this) statement is equivalent to:

System.Threading.Monitor.Enter(this);
try { ... }
finally { System.Threading.Monitor.Exit(this); }

All exceptions are silently ignored in this cleanup phase because failure to release everything is not considered fatal.

Lines 171–228 contain the implementation for the ConferenceEnded event defined by the event interface. The implementation of the ConferenceStarted event in Lines 231–288 is almost identical, as is the event implementation corresponding to any kind of source interface’s method.

The code makes use of C#’s advanced event syntax to implement custom actions inside the add and remove accessors, much like implementing get and set property accessors. The add accessor begins by calling Init if the class’s connection point member is still null (Line 181). It then creates a new sink helper object and passes it to the call to UCOMIConnectionPoint.Advise. The call ends up setting the sink helper’s cookie field through the out parameter. After this, there are only two things left to do—set the sink helper’s corresponding delegate field to the passed-in delegate reference (Line 188), and add the sink helper to the ArrayList (Line 191).

The remove accessor is similar to Cleanup, but only removes and unhooks the passed-in delegate rather than all of them. The for loop in Lines 201–225 scans each element in the ArrayList, and when the right one is found, it is removed (Line 211). UCOMIConnectionPoint.Unadvise is called in Line 214 with the current sink helper’s cookie. Finally, if the ArrayList is empty, the connection point and ArrayList members are set to null.

If you compare the metadata generated by the C# compiler for Listing 21.21 to metadata that would be generated by the type library importer, you’ll notice slight differences in delegate definitions. For example, C# delegates automatically define BeginInvoke and EndInvoke for asynchronous communication, but the importer-generated delegates do not.

Tip

If you want to manually define the minimum number of types possible while still maintaining the ability to hook up to events in the .NET style, you can skip defining a coclass interface and the corresponding class type. Because a coclass interface has the same IID as the coclass’s default interface, you can simply add the event interface (the one named SourceInterfaceName_Event) to the list of interfaces that the default interface derives from (if any). Clients would then be able to directly hook and unhook event handlers using the event members inherited by the default interface. This is risky, however, if the default interface is public because nothing prevents other COM components from having the same default interface but not sourcing the same events.

Manually Defining COM Structures

Chapter 19, “Deeper Into PInvoke and Useful Examples,” already described how to define unmanaged structures in .NET source code. Unmanaged structures are dealt with no differently in the context of COM Interoperability as in the context of PInvoke. There is no difference in default marshaling behavior regardless of whether the structure is used in a PInvoke signature or a COM interface’s signature. For example, string fields in a struct are always marshaled as LPSTR types by default. Evidence that there’s no such thing as a COM-specific structure is that the ComImportAttribute can’t be applied to structures—only classes and interfaces.

There is one detail to be concerned about that is important for structures used with COM APIs related to packing alignment (introduced in Chapter 19). The default packing alignment for structures in a type library is 4 bytes. The default packing alignment for .NET structures, however, is 8 bytes. This means that you may need to set an explicit packing alignment using the Pack property of StructLayoutAttribute to create a correct definition of a COM structure, depending on the order and types of its fields. In cases where the packing alignment of 4 bytes versus 8 bytes makes a difference, failure to mark the packing alignment correctly can cause potentially subtle failure (just like making any other kind of improper definition of unmanaged entities). For example, the layout of the following structure (shown in IDL) is different depending on whether it uses a packing alignment of 4 bytes or 8 bytes:

typedef struct UnalignedStruct
{
  long one;
  double two;
  long three;
} UnalignedStruct;

Remember that the IWMPEffects interface from Listing 21.2 requires the definition of two structures—TimedLevel and RECT. Now it’s time to define these in managed code. First, Listing 21.22 displays the structure definitions that would be created by the type library importer in IL Assembler syntax.

Listing 21.22. Definitions of the RECT and TimedLevel Structures Generated by the Type Library Importer, Shown in IL Assembler Syntax

Image

Notice that the types are defined as tagRECT and tagTimedLevel instead of RECT and TimedLevel. This is because, as commonly seen in IDL, the structs are defined in the following manner:

typedef struct tagTimedLevel
{
  ...
} TimedLevel;
instead of:
typedef struct TimedLevel
{
  ...
} TimedLevel;

The important thing to notice is that tagRECT has a packing alignment of 4 bytes (specified in Line 4) and tagTimedLevel has a packing alignment of 8 bytes (specified in Line 15). The type library importer always sets a struct’s packing to whatever value is set in the input type library. Unfortunately, tools such as OLEVIEW.EXE do not display the packing alignment of structures. (IDL can’t express this information.) You can obtain this information programmatically using ITypeLib and its related COM interfaces, or hopefully from the documentation of the structures you’re attempting to define.

The .size 0 in Lines 5 and 16 simply indicate that StructLayoutAttribute’s Size property isn’t set. This is no different than when the .size directive is omitted altogether; the IL Disassembler only omits the .size directive when neither Pack nor Size are explicitly set by the metadata producer.

Listing 21.23 shows how to define these same two structures in Visual Basic .NET.

Listing 21.23. .NET definitions of the RECT and TimedLevel Structures Written in Visual Basic .NET

Image

Both structures are renamed to remove their tag prefix, because this is undesirable in a .NET type’s name. Just like renaming classes and interfaces, renaming a structure is always safe in that it doesn’t affect the proper operation of COM clients using the structure. The fields of both structs are also capitalized to match .NET conventions because they are public.

The RECT structure is marked with a packing alignment of 4 bytes in Line 3, and the TimedLevel structure is left with its default packing alignment of 8 bytes to match the COM definitions. Although the setting the packing alignment to 4 bytes doesn’t affect the layout of the simple RECT structure, it’s good practice to mark it anyway. Ensuring that the TimedLevel structure has a packing alignment of 8 bytes is critical, because its layout would change if it were marked with a packing alignment of 4 bytes instead.

Manually Defining COM Enums

Defining a COM enumeration is straightforward, and is just like defining a .NET enumeration. No special custom attributes are required, although it’s good practice to always explicitly list the value of each member because the definition must exactly match the unmanaged definition and you may not be aware of how the compiler assigns values by default (for example, zero-based or one-based). If the COM enumeration’s values are sequential and start with zero, then this is not strictly necessary because .NET enumeration values begin at zero by default in C#, Visual Basic .NET, and C++. Also, be sure to give the enumeration the correct underlying type. Most enumerations have a 32-bit underlying type, and this is the default underlying type when defining enumerations in C#, Visual Basic .NET, and C++.

Tip

For enumerations with values that can be combined like bit flags, it’s helpful to mark the enumeration with System.FlagsAttribute. This strictly informational custom attribute lets clients know that combining the enumeration values with bitwise operators is a valid thing to do. In the future, development environments could make use of this attribute, perhaps to display the marked enumerations differently.

Manually Defining COM Classes

COM interfaces and their parameter types (such as structures or other COM interfaces) are really all you need to define in order to fully interact with COM. The various event-related classes are nice for exposing COM connection points as .NET events, but these aren’t necessary because you could always use the connection point interfaces directly as you would in unmanaged C++. Enums aren’t necessary because their underlying type could always be used as a replacement. Class types and coclass interfaces aren’t necessary, either. For example, to write a new COM-compatible class in managed code, you only need to implement COM interfaces. If you need to instantiate and use an existing coclass, you can do this with only interface definitions, Activator.CreateInstance, and Type.GetTypeFromCLSID.

For example, let’s instantiate instances of the two coclasses from earlier in the chapter, HTMLStyleSheet:

Image

and NetMeeting:

Image

Without a .NET definition of these classes, instances can still be created as follows:

C#:

Image

Visual Basic .NET:

Image

C++:

Image

After the object is created, members can be called on the DispHTMLStyleSheet and INetMeeting interfaces, or the objects could be cast to any other interfaces that the coclasses implement.

Besides lengthier source code, there’s nothing wrong with having to use Activator.CreateInstance; because the returned object is cast to a COM interface, we’re still using the COM object in an early-bound fashion (except when calling through a dispinterface like DispHTMLStyleSheet). If you don’t like using CLSIDs in source code, using GetTypeFromProgID instead of GetTypeFromCLSID would look a little nicer and be less error-prone for coclasses registered with a ProgID. Still, defining a COM class that can be instantiated by simply using the new keyword is sometimes desirable to make coclasses appear more like .NET classes (like what the type library importer does).

Therefore, the following sections describe two ways to create type information for coclasses that enable instantiation just like a .NET class.

Defining Classes the Simple Way

Defining a .NET class type that is able to represent a coclass is surprisingly simple. Furthermore, such a class stands on its own—no additional type definitions are needed to define it. That’s because all that is needed is an empty class marked with ComImportAttribute and GuidAttribute containing the CLSID. This looks like the following for the HTMLStyleSheetClass and NetMeetingClass classes (in C#):

using System.Runtime.InteropServices;

[
  ComImport,
  Guid("3050f2e4-98b5-11cf-bb82-00aa00bdce0b")

]
public class HTMLStyleSheetClass {}

[
  ComImport,
  Guid("3E9BAF2D-7A79-11D2-9334-0000F875AE17")
]

public class NetMeetingClass {}

Because the C# compiler emits a public default constructor by default, each class can be instantiated properly. The CLR knows to call CoCreateInstance with the CLSID contained in the GuidAttribute because each class is marked with ComImportAttribute. Because casting the class to an interface results in a QueryInterface call that succeeds or fails based purely on the COM object’s implementation, it’s not necessary to list the interfaces it implements. In fact, listing them is discouraged because you’d then be forced to implement each interface and that can get you into trouble if it’s not done right (explained in the next section). Therefore, these class types can be used in conjunction with the coclass interfaces defined earlier or just used on their own and provide the proper behavior.

The rules to follow when defining a COM class the simple way are:

• Mark the class with ComImportAttribute and GuidAttribute containing the CLSID.

• Make the class derive directly from System.Object (which is implicit in C#, Visual Basic .NET, and C++).

• Don’t list any interfaces that it implements, and leave the class completely empty.

The first rule is important for obvious reasons. Without ComImportAttribute, you’ve just got an empty class that does nothing. An incorrect CLSID means that the CoCreateInstance call made by the CLR during the object’s instantiation cannot succeed.

The second and third rules, if not followed, result in a TypeLoadException at run time. Excluding one case described in the next section, a ComImport-marked class must not have any members besides those of System.Object and a public default constructor. Furthermore, this constructor must be marked with runtime managed internal (in IL Assembler syntax), unlike COM interface members for which cil managed is acceptable, and must have a Runtime Virtual Address (RVA) of zero.

Every member emitted into metadata has an RVA, which specifies the location of the member’s body relative to the start of the file in which it is defined. The CLR enforces a rule that all members in a ComImport-marked class must have an RVA equal to zero. Not all compilers, however, enforce this rule.

When defining a ComImport-marked class in C#, the public default constructor automatically generated by the compiler is marked with runtime managed internal and has an RVA of zero. Attempting to manually define a ComImport-marked class (such as the previously defined HTMLStyleSheetClass) in Visual Basic .NET or C++ fails for two reasons:

• Neither compiler marks the automatically-generated constructor with the necessary attributes to make it appear as runtime managed internal.

• Neither compiler gives the automatically-generated constructor an RVA of zero.

The first problem could be solved by explicitly defining a public default constructor and marking it with MethodImplAttribute as we’ve done earlier in the chapter. However, the second problem can’t be solved in a high-level language because there’s no mechanism for customizing a member’s RVA.

For example, the C++ compiler emits a public default constructor with a non-zero RVA for the following definition:

#using <mscorlib.dll>
using namespace System::Runtime::InteropServices;

[
  ComImport,
  Guid("3050f2e4-98b5-11cf-bb82-00aa00bdce0b")
]
public __gc class HTMLStyleSheetClass {};

Running a program that attempted to use this class would throw a TypeLoadException with a non-descriptive message, for example:

Could not load type HTMLStyleSheetClass from assembly Chapter21,
Version=1.0.0.0, Culture=neutral, PublicKeyToken=null.

Running the PEVERIFY.EXE SDK tool on the assembly containing the C++ HTMLStyleSheetClass definition gives a more informative error message:

Error: Method marked Abstract/Runtime/InternalCall/Imported must have zero RVA,
and vice versa.

To see members’ RVA values, you have to dig a little deeper into the IL Disassembler (ILDASM.EXE) and press Crtl+M once you have an assembly open in graphical mode. You can then see RVA values as follows:

TypeDef #1
-------------------------------------------------------
  TypDefName: HTMLStyleSheetClass  (02000002)
    Flags     : [Public] [AutoLayout] [Class] [Import] [AnsiClass]  (00001001)

    Extends   : 01000004 [TypeRef] System.Object
  Method #1
  -------------------------------------------------------
    MethodName: .ctor (06000002)
    Flags     : [Public] [ReuseSlot] [SpecialName] [RTSpecialName] [.ctor]  (00001806)
    RVA       : 0x00001000
    ImplFlags : [IL] [Managed]  (00000000)
    CallCnvntn: [DEFAULT]
    hasThis
    ReturnType: Void
    No arguments.

Note that when producing an IL file with ILDASM.EXE’s /out option, no RVA information is included. The IL Assembler, just like any other compiler, chooses its own RVA values when assembling the IL. Furthermore, the IL Assembler always chooses an RVA equal to zero on any member marked runtime managed internal! Therefore, the following steps can be a suitable workaround for defining COM classes in languages other than C#:

1. Explicitly define a public default constructor, or a private default constructor if the class is not creatable, and mark it with [MethodImpl(MethodImplOptions::InternalCall, MethodCodeType=MethodCodeType::Runtime)] in C++ or <MethodImpl(MethodImplOptions.InternalCall, MethodCodeType:=MethodCodeType.Runtime)> in Visual Basic .NET (the syntax varies with the language).

2. Use the IL Disassembler to produce an IL file from your compiled assembly, for example:

ildasm MyAssembly.dll /out:MyAssembly.il

4. Use the IL Assembler to reassemble the assembly from the exact same IL file, for example:

ilasm /dll MyAssembly.il

The simple process of disassembling then assembling changes the necessary RVA values to zero without modifying the IL file. See Chapter 7 for more details about the process of disassembling and assembling, because the commands used differ if the assembly has strong name or embedded resources.

This process works for Visual Basic .NET assemblies, but may fail for C++ assemblies, however, if they contain embedded native code. The IL Disassembler cannot properly disassemble such code, so the re-assembled file would not be correct. Fortunately, ILDASM.EXE warns you when attempting to disassemble embedded native code.

Tip

As in Chapter 7, running PEVERIFY.EXE on assemblies with manually-defined types is a good idea to make sure you aren’t breaking any CLR rules that aren’t enforced by the compiler. In addition, any errors caught are likely to give you more information than the exception thrown at run time. Fortunately, the compilers for high-level languages like C# and Visual Basic .NET catch most problems that would cause bogus type definitions to be emitted (unlike the much less strict IL Assembler).

With correct .NET HTMLStyleSheetClass and NetMeetingClass class definitions (such as the C# definitions given earlier), the classes can now be instantiated as follows:

C#:

DispHTMLStyleSheet sheet = (DispHTMLStyleSheet)new HTMLStyleSheetClass();
INetMeeting meeting = (INetMeeting)new NetMeetingClass();

Visual Basic .NET:

Dim sheet As DispHTMLStyleSheet = CType(new HTMLStyleSheetClass(), DispHTMLStyleSheet)
Dim meeting As INetMeeting = CType(new NetMeetingClass(), INetMeeting)

C++:

DispHTMLStyleSheet* sheet = (DispHTMLStyleSheet*)new HTMLStyleSheetClass();
INetMeeting* meeting = (INetMeeting*)new NetMeetingClass();

After the objects are created, members still must be called by casting sheet and meeting to the appropriate interfaces first. If you defined a coclass interface for HTMLStyleSheet and NetMeeting, as we did in the “Manually Defining Coclass Interfaces and Event Types” section, then the C# and Visual Basic .NET code can use them instead of the Class-suffixed class names. If you don’t want to bother with defining a coclass interface, you can omit the Class suffix from the .NET class to make its use more natural to clients.

Defining Classes the Hard Way

Besides being simple to do, defining .NET classes representing coclasses as done in the previous section is probably sufficient. Still, you might wish to create a full “importer-style” .NET class that not only lists the interfaces it implements, but contains the methods (as you’d expect for a .NET class) so casting to interfaces is unnecessary. This is the hard way of defining a .NET class (although the result is easier for clients).

The bad news is that producing .NET classes that match those produced by the type library importer cannot be done directly by any other compiler but the IL Assembler. (The same metadata can be emitted, however, in any language by using the reflection emit APIs to generate and persist a dynamic assembly.) The metadata required is strange enough that no high-level language has syntax to support it, although C# and Visual Basic .NET come close.

C# supports defining non-static extern members in a class specifically for COM Interoper-ability. For this to work, the method (or property/event accessors) must be marked with MethodImplAttribute as done in previous listings, plus explicit interface implementation must be used in order to tell the CLR which COM interface method should be called if somebody were to call the class’s method. This technique is demonstrated in Listing 21.24 with the NetMeetingClass class, which uses the types defined in Listing 21.21.

Listing 21.24. The NetMeetingClass Type Uses the C# Feature of Non-Static extern Members to Produce a Class Definition Closer to What the Type Library Importer Would Generate

Image

Image

Lines 5–9 mark the class with the same custom attributes that the type library importer would. The ComSourceInterfacesAttribute captures the fact that this class is an event source, and can be useful if a .NET class derives from NetMeetingClass.

NetMeetingClass implements every interface that the type library importer would make it implement—its coclass interface, all the interfaces it listed as implementing (in this case, only INetMeeting), and an event interface corresponding to each source interface listed in the original coclass statement (here, only the event interface corresponding to _INetMeetingEvents).

To compile without errors, the class must explicitly implement every member from each of its implemented interfaces. As required, each member is marked extern, and each method and accessor must is marked with MethodImplAttribute to give it the runtime managed internal marking in IL Assembler syntax. Because the NetMeeting coclass interface has no members, the listing only needs to implement the five methods of INetMeeting and the two events of _INetMeetingEvents_Event. The DispIdAttributes on the class’s members match what the type library importer does, but is not strictly required because they only apply to the class interface exposed to COM, and this class interface is disabled in Line 9. It’s still a good idea to use them, however, in case a derived .NET class wants to expose a class interface with these same DISPIDs on the base members. Unlike interfaces, the order in which members are listed doesn’t matter.

This listing encounters two bugs with the Visual C# .NET compiler (version 7.0). First, the event accessors must have {} rather than a semicolon in order to avoid errors that say, “An add or remove accessor must have a body.” In later versions of the C# compiler, this will likely need to change to use semicolons instead. Second, the listing compiles with several warnings from these same event accessors, stating that the accessors are marked external yet have no custom attributes. The compiler produces the correct metadata, however, so these warnings can be safely ignored.

There’s one important limitation to this C# support. Methods using explicit interface implementation must be private. Because they’re private, nobody can call them directly on the class—users have to cast to the interface anyway. So what’s the point of using this feature? There’s really only one reason: It’s useful to be able to mark the class as implementing the various interfaces so clients can implicitly convert the class type to one of the interface types. A cast is no longer required.

If you wanted to start with C# code like the previous listing and tweak it using the usual steps of disassembling, editing, assembling, this can work fairly well for some examples. For Listing 21.24, you could disassemble the compiled assembly to an IL file then change every method (like Version) from private:

.method private hidebysig newslot final virtual
  instance int32  INetMeeting.Version() runtime managed internalcall
{
  .custom instance void [mscorlib]
    System.Runtime.InteropServices.DispIdAttribute::.ctor(int32) =
    ( 01 00 64 00 00 00 00 00 )                         // ..d.....
  .override INetMeeting::Version
} // end of method NetMeetingClass::INetMeeting.Version

to public, and also renaming the member to remove the “IntefaceName.” prefix:

.method public hidebysig newslot final virtual
  instance int32  Version() runtime managed internalcall
{
  .custom instance void [mscorlib]
    System.Runtime.InteropServices.DispIdAttribute::.ctor(int32) =
    ( 01 00 64 00 00 00 00 00 )                         // ..d.....
  .override INetMeeting::Version
} // end of method NetMeetingClass::Version

and then reassembling the new IL file. But unfortunately, that’s not enough for properties or events. When explicitly implementing a property or event, the C# compiler only emits the accessor methods in the class definition and not the property or event itself. Therefore, to make the class’s metadata work like the metadata produced by the importer, adding these missing members is necessary.

There’s often another complication, however, and that’s when the class implements interfaces that share the same method name. A great example is the HTMLSytleSheet coclass. Due to the strong relationship between IHTMLStyleSheet, IHTMLStyleSheet2, and DispHTMLStyleSheet, all three interfaces share 16 members with the same name. The type library importer decorates these as InterfaceName_MemberName when adding them to the class definition. Because explicitly implemented members are named InterfaceName.MemberName, this is a simple change to turn dots into underscores when editing the IL file. This needs to be done because most languages can’t consume a public member name with a dot in it.

Unlike C#, Visual Basic .NET makes it easy to explicitly and publicly implement interface members, and even rename the class’s members. Unfortunately, there’s still one missing piece—the ability to specify an external method with a keyword like C#’s extern. Therefore, any class definition written in VB .NET similar to Listing 21.24 would still require modifications using the IL Disassembler and IL Assembler.

Caution

Attempting to place implementation directly in a ComImport-marked class or failure to mark a method or accessor with the appropriate MethodImplAttribute causes a TypeLoadException, as described in the “Defining Classes the Simple Way” section, because such an assembly fails CLR verification.

Avoiding the Balloon Effect

One of the benefits for manually defining your own COM types listed at the beginning of the chapter is that it can be more lightweight than using an entire Interop Assembly because you only need to define the types you use. Taking a closer look at the IHTMLStyleSheet example, however, you’ll notice that defining this interface alone also requires the definitions of IHTMLElement, IHTMLStyleSheetRulesCollection, IHTMLStyleSheetPagesCollection, and IHTMLStyleSheetsCollection, because these types are used as parameters in IHTMLStyleSheet. If you go ahead and start defining the IHTMLElement interface, you’ll quickly discover that this interface requires the definitions of IHTMLStyle and IHTMLFiltersCollection. Meanwhile, IHTMLStyleSheetRulesCollection requires the definition of IHTMLStyleSheetRule which requires the definition of IHTMLRuleStyle, and so on. Before you know it, you’re redefining an entire type library’s contents! This is the balloon effect.

Fortunately, there are ways to combat this. For example, if you don’t think you’re ever going to need to call IHTMLStyleSheet’s imports property, you could change its definition from (in C#):

[DispId(DispIds.IHTMLSTYLESHEET_IMPORTS)]
IHTMLStyleSheetsCollection imports { get; }

to:

[DispId(DispIds.IHTMLSTYLESHEET_IMPORTS)]
[return: MarshalAs(UnmanagedType.IUnknown)] object imports { get; }

or:

[DispId(DispIds.IHTMLSTYLESHEET_IMPORTS)]
[return: MarshalAs(UnmanagedType.IDispatch)] object imports { get; }

In essence, you’re changing the COM definition from the following (in IDL):

[propget, id(DISPID_IHTMLSTYLESHEET_IMPORTS)]
HRESULT imports([retval, out] IHTMLStyleSheetsCollection** p);

to:

[propget, id(DISPID_IHTMLSTYLESHEET_IMPORTS)]
HRESULT imports([retval, out] IUnknown** p);

or:

[propget, id(DISPID_IHTMLSTYLESHEET_IMPORTS)]
HRESULT imports([retval, out] IDispatch** p);

This is a valid change to make because IHTMLStyleSheetsCollection derives from both IDispatch and IUnknown. Not only is it valid, but it removes the need to define the IHTMLStyleSheetsCollection in managed code. If you end up wanting to use this property in the future but don’t want to change its signature, you could always define the IHTMLStyleSheetsCollection interface at that time then cast the returned object to that interface. (Or you could even late bind to the returned object if you didn’t want to define the interface.)

This transformation is only valid for reference types (COM interfaces). For value types that you don’t want to define, changing a parameter to the System.IntPtr type works instead as long as the value type parameter has at least one level of indirection. Using another example from the Microsoft HTML Object Library, the IElementBehaviorRender interface defines the following method:

HRESULT HitTestPoint([in] tagPOINT* pPoint,
  [in] IUnknown* pReserved, [out, retval] long* pbHit);

This can be correctly defined as follows in Visual Basic .NET:

Function HitTestPoint(ByRef pPoint As tagPOINT, _
  <MarshalAs(UnmanagedType.IUnknown)> pReserved As Object) As Integer

or defined the short-cut way as follows:

Function HitTestPoint(pPoint As IntPtr, _
  <MarshalAs(UnmanagedType.IUnknown)> pReserved As Object) As Integer

This technique was shown in Chapter 6, “Advanced Topics for Using COM Components,” when encountering pointers to structures for which passing Nothing (null) is desired. A caller can pass a value like IntPtr.Zero or pass a valid structure by calling Marshal.StructureToPtr (or Marshal.PtrToStructure if the IntPtr value is being returned).

Using the IntPtr type works for COM interface parameters as well. A caller can get an IntPtr type that represents a COM interface by calling either Marshal.GetIUnknownForObject, Marshal.GetIDispatchForObject, or the general Marhsal.GetComInterfaceForObject. The returned IntPtr value can then be passed to a method that expects an interface pointer in a parameter changed to be the IntPtr type. For an IntPtr value returned, a caller can call Marshal.GetObjectForIUnknown to convert an IntPtr value to an object that can be cast to COM interfaces that it implements. These methods are covered in more detail in Appendix A, “System.Runtime.InteropServices Reference.”

The kinds of modifications discussed so far should be done on private type definitions because they aren’t as convenient to use as “the real thing.” There’s one more major shortcut that should definitely be restricted to non-public types in well-controlled situations. That shortcut is to replace a member that you’re not going to use with a dummy method to fill the slot in the v-table. For example, if you know that your program will be passed an object that implements IHTMLStyleSheet and all you need to do is cast it to the IHTMLStyleSheet type and call its addImport and addRule methods, you could define the interface as shown in Listing 21.25.

Listing 21.25. A Bare-Bones C# Definition of the IHTMLStyleSheet Interface That Defines Only Two Methods, Using Placeholders for the Other Members

Image

This listing omits the TypeLibTypeAttribute because we’re the only client and we don’t care about this information. It also doesn’t bother to mark the members with DISPIDs because we aren’t going to implement this interface. Renaming the interface is good practice because it draws attention to the fact that this isn’t the full COM interface by the same name.

If you have the time, it’s probably better to use the original member names for the placeholders rather than slotn, because it makes it easier to plug in remaining members if desired.

This quick and dirty approach is useful when attempting to write a quick prototype application that requires several COM interface definitions. Because defining signatures correctly and dealing with the balloon effect can be quite time consuming, this placeholder technique enables you to define the most important members first and then fill in the remaining members when (and if) time permits.

Conclusion

This chapter highlighted all the major steps and roadblocks when manually defining type information for COM interfaces, classes, structures, and enumerations. Each .NET language has strengths and weaknesses. Whereas C# has the most comprehensive support for defining COM classes, Visual Basic .NET is usually best for defining COM interfaces because, for example, parameterized properties and optional parameters are more common than properties that define set accessors before get accessors. On the other hand, not being able to place MarshalAsAttribute on a VB .NET interface’s property setter can be a big problem.

Unfortunately, defining types correctly can be a time-consuming and frustrating process much like defining PInvoke signatures. When checking the correctness of your type definitions, an easier-sounding approach than the one pictured in Figure 21.1 would be to temporarily remove ComImportAttribute from interface definitions, export a type library, and view it in OLEVIEW.EXE to compare the exported IDL syntax to the syntax in the original IDL file or original type library. Don’t do this! Because OLEVIEW.EXE registers the input type library, you could overwrite important registry entries. If you have a type library viewer that doesn’t touch the registry, however, then this approach can work well (as long as you export the type library using TLBEXP.EXE instead of REGASM.EXE, which also registers the type library).

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

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