• 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.
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.
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.
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.
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.
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
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.
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 InterfacesIn 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
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#
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.
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
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
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
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 MethodImplAttribute
s 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
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
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#
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.
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
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.
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
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.
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);
};
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
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
Line 9 contains the necessary InterfaceIsIDispatch
marking that all dispinterfaces must have.
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 DispIdAttribute
s 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.
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.
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.)
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
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
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.
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.
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
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
It’s natural to want to define a property like IHTMLStyleSheet.title
as follows inside a Visual Basic .NET interface definition:
' 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; }
[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
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:
// 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.
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#:
// 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:
to this:
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.
Using OptionalAttribute
without specifying a default value can only be done safely on System.Object
types that were VARIANT
s 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#
This listing assumes that the COM interfaces IHTMLElement
, IHTMLStyleSheetsCollection
, IHTMLStyleSheetRulesCollection
, and IHTMLStyleSheetPagesCollection
have also been given a suitable .NET definition.
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.
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.
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.
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
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
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.
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.
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
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.
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
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
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.
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.
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
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
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.
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++.
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.
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
:
and NetMeeting
:
Without a .NET definition of these classes, instances can still be created as follows:
C#:
Visual Basic .NET:
C++:
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 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.
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.
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
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 DispIdAttribute
s 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.
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.
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);
[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
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 slot
n
, 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.
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).
3.141.24.134