• Transforming Types Without Custom Marshaling
• Custom Marshaling Architecture
• Marshalers, Marshalers, Marshalers!
• Limitations
Custom marshaling is the extensibility mechanism for the Interop Marshaler. The term custom marshaling can mean different things in different situations, but this chapter refers to customizing Interop marshaling across managed/unmanaged boundaries. COM-style custom marshaling across contexts (performed by implementing IMarshal
) is completely independent of Interop custom marshaling, and both can coexist peacefully.
The work done by the Interop Marshaler without custom marshaling is summarized in Figure 20.1. These are the same diagrams that were presented in Chapter 2, “Bridging the Two Worlds—Managed and Unmanaged Code.”
Figure 20.1. COM and .NET interaction when no custom marshaling is involved.
The CLR’s custom marshaling infrastructure enables you to replace the system-supplied COM-Callable Wrappers (CCWs) and Runtime-Callable Wrappers (RCWs) on a call-by-call basis. This capability is illustrated in Figure 20.2.
Figure 20.2. COM and .NET interaction with custom marshaling.
Notice how an object implementing IMyInterface
is exposed via a wrapper implementing IAnyInterface
instead. When you control the wrappers, you can transform objects however you’d like (within some limitations discussed in the “Limitations” section at the end of the chapter).
Transforming one data type to a completely different type may sound like a crazy thing to do, but it’s actually the Number One application of custom marshaling. This occurs because the arrival of .NET has introduced several brand-new data types that, at some logical level, serve the same purpose as existing COM types. For example, although the .NET System.IO.Stream
is unrelated to COM’s IStream
interface (from the perspective of any runtimes or compilers), they are extremely similar abstractions that enable reading, writing, and seeking over a buffer.
Likewise, the .NET System.Drawing.Font
class and the COM IFont
interface are just different ways of exposing the same operating system font objects. The same goes for System.Drawing.Color
and OLE_COLOR
, System.Collections.IEnumerator
and IEnumVARIANT
, and so forth. Such transformations are really no different than the data type transformations done by the standard Interop Marshaler, which bridges the “unrelated” System.String
and BSTR
types, .NET arrays and SAFEARRAY
s, System.Guid
and GUID
types, and so on.
Custom marshaling provides the glue between objects that use different but semantically similar data types. It can also be used to customize standard marshaling in more subtle ways (shown in the last example in the “Marshalers, Marshalers, Marshalers!” section) but this isn’t as common.
In this chapter, we begin by examining how one could convert one data type to another without any custom marshaling support from the CLR. Then we examine the custom marshaling infrastructure that standardizes such conversions. Finally, we end the chapter with lots of examples and a discussion of custom marshaling limitations.
Imagine that you’re using a COM object in managed code that returns a stream to you in the form of an object implementing COM’s IStream
interface. You want to use some .NET APIs that operate on System.IO.Stream
objects, such as constructing a System.IO.StreamReader
with this IStream
instance. The following C# code would not compile because you must pass a Stream
instance to the StreamReader
constructor:
IStream s = comObject.GetXml();
StreamReader sr = new StreamReader(s); // This line doesn't compile because
// IStream isn't a Stream
Console.SetIn(sr);
...
sr.Close();
To make this work, you could define an adapter class that wraps an object implementing IStream
inside a class that derives from Stream
. An instance of such a class is called an adapter object, which could be used as follows:
IStream origStream = comObject.GetXml();
Stream s = new ComStream(origStream); // Convert IStream to Stream
StreamReader sr = new StreamReader(s);
Console.SetIn(sr);
...
sr.Close();
Just like System.IO.FileStream
or System.IO.MemoryStream
, the ComStream
class can be used anywhere Stream
is required thanks to the polymorphism provided by inheritance. The ComStream
class is responsible for wrapping IStream
appropriately so Stream.Read
calls IStream.Read
, Stream.Write
calls IStream.Write
, and so on. All of IStream
’s methods are displayed in Listing 20.1, which contains the definition for System.Runtime.InteropServices.UCOMIStream
—the official .NET definition of the COM IStream
interface.
Listing 20.1. The .NET Definition of UCOMIStream
Shown in C# Syntax
Representing streams in .NET, the System.IO.Stream
abstract class has abstract Read
, Write
, Seek
, SetLength
, and Flush
methods, a handful of abstract properties, and many more public members. Although the mapping of core functionality (read/write/seek) might be straightforward, other areas might require some thought. Listing 20.2 defines a ComStream
adapter class in C# that exposes UCOMIStream
functionality as a .NET Stream
.
Listing 20.2. ComStream.cs
. An Adapter Class That Exposes an IStream
Implementation as a .NET Stream
Lines 1–3 use the System
namespace for IntPtr
and a handful of exception types, the System.IO
namespace for Stream
, and the System.Runtime.InteropServices
namespace for UCOMIStream
and Marshal
. The constructor in Lines 11–21 sets the private UCOMIStream
field to the passed-in instance if it’s not null. The class’s finalizer in Lines 24–27 calls the Close
method, which is Stream
’s equivalent to IDisposable.Dispose
. (The base Stream
class explicitly implements IDisposable
, and its Dispose
implementation simply calls Close
.) Lines 30–33 define an UnderlyingStream
property that publicly exposes the raw COM stream object. This property can be useful in cases where the COM object implements other interfaces for a client to make use of.
Line 37 begins the implementation of Stream
’s members, which forward calls to the UCOMIStream
interface. The first two lines of the Read
method (39 and 40) perform a common task for a class that operates on a disposable instance—checking to see if the object has been disposed already and throwing an ObjectDisposedException
if it has. Notice that this implementation of Read
only accepts an offset
value of zero. This is done for simplicity and performance, because the only way to pass UCOMIStream.Read
an appropriate array when a non-zero offset is given would be to instantiate a new array and copy the source array’s elements beginning at the offset.
The tricky part of the Read
implementation is that it needs to obtain the address of an integer variable, represented as an IntPtr
type, to pass as the third parameter to UCOMIStream.Read
. Line 48 uses C# unsafe code to cleanly accomplish this. Because of this, the method must be marked with the unsafe
keyword and the class must be compiled with the /unsafe
option from the command line. (Or, Allow unsafe code blocks
must be set to True
within Visual Studio .NET.) See Chapter 6, “Advanced Topics for Using COM Components,” for another way to get a value type’s address that works for any .NET language.
ComStream.Write
in Lines 57–67 works just like ComStream.Read
, but because it doesn’t return how many bytes were written, Line 66 can simply pass a “null pointer” (IntPtr.Zero
) as the third parameter to UCOMIStream.Write
. ComStream.Seek
(Lines 70–85) also has a similar implementation, and uses unsafe code just like ComStream.Read
to pass an address of a 64-bit variable. Fortunately, the STREAM_SEEK
enumeration used by UCOMIStream
and the SeekOrigin
enumeration used by Stream
have the exact same values with the exact same meanings, so the input origin
variable can simply be cast to the integer type (which represents a STREAM_SEEK
value) expected by UCOMIStream.Seek
in Line 82.
The remaining methods and properties in Lines 88–157 do what they can to bridge the functionality from UCOMIStream
to the functionality of Stream
, but the mapping isn’t always obvious. For example, ComStream.Length
uses UCOMIStream.Stat
to obtain the stream’s length, and ComStream.Flush
calls UCOMIStream.Commit
with the value 0 to represent STGC_DEFAULT
(an enumeration value used with COM’s IStream
interface).
This ComStream
class doesn’t bother overriding members of Stream
that already have a suitable implementation, such as ReadByte
, WriteByte
, BeginRead
, BeginWrite
, EndRead
, and EndWrite
. The class also lets any exceptions thrown be exposed directly. To be more user-friendly, it could have caught some exceptions and rethrown them with different exception types that a user of Stream
would expect from its methods. This can be especially helpful when the inner object throws lots of COMException
s.
The problem with the adapter class approach used in Listing 20.1 is that it uses System.Runtime.InteropServices.UCOMIStream
for the .NET definition of IStream
, but no Interop Assemblies generated by the type library importer use this type unless they were manually altered as in Chapter 7, “Modifying Interop Assemblies.” Most likely, any APIs that use IStream
exposed by an Interop Assembly end up using a .NET IStream
interface definition contained in the same assembly (due to the #import
effect described in Chapter 15, “Creating and Deploying Useful Primary Interop Assemblies”). This isn’t a huge setback when the object implementing IStream
is a COM object. You can explicitly cast a COM object to any .NET definition of IStream
you’d like and it will always succeed as long as its QueryInterface
implementation responds positively to the IID of IStream
. Therefore, the ComStream
adapter class defined in Listing 20.2 could be used as follows if an object returns a non-standard IStream
interface:
IStream origStream = comObject.GetXml();
Stream s = new ComStream((UCOMIStream)origStream);
StreamReader sr = new StreamReader(s);
Console.SetIn(sr);
...
sr.Close();
An adapter class may be desired in the other direction as well. Suppose you’re using a .NET class that returns a Stream
instance, and you need to pass the stream to a COM object. Again, doing this directly isn’t possible:
Stream s = System.Console.OpenStandardOutput();
comObject.WriteContentsToStream(s); // This line doesn't compile because
// Stream doesn't implement IStream
s.Close();
Instead, you could define an adapter class that wraps an object derived from Stream
inside a class that implements IStream
. This could then be used as follows:
Stream s = System.Console.OpenStandardOutput();
UCOMIStream istream = new ManagedIStream(s); // Convert Stream to IStream
comObject.WriteContentsToStream(istream);
s.Close();
Listing 20.3 contains a definition of the ManagedIStream
adapter class that internally acts upon a Stream
class.
Listing 20.3. ManagedIStream.cs
. An Adapter Class That Exposes a Stream
Object Through an IStream
Implementation
Lines 1–3 use the System
namespace for IntPtr
and a handful of exception types, the System.IO
namespace for Stream
, and the System.Runtime.InteropServices
namespace for UCOMIStream
and Marshal
. The class is marked with ClassInterfaceType.None
because any COM access should be done through the UCOMIStream
interface. This also removes the need to mark the class’s members with the necessary attributes that make them work correctly when called from unmanaged code. These attributes can be seen in Listing 20.1 on the UCOMIStream
interface definition, such as [MarshalAs(UnmanagedType.LPArray, SizeParamIndex=1), Out]
on the first parameter of UCOMIStream.Read
.
The constructor, finalizer, and UnderlyingStream
property in Lines 12–40 work just like they do in Listing 20.2. The ManagedIStream.Read
, ManagedIStream.Write
, and ManagedIStream.Seek
methods (Lines 44–101) all have an IntPtr
third parameter that can either be “null” (IntPtr.Zero
) or have a value if the caller wants to know how many bytes were read, written, or moved past. Therefore, Lines 49, 67, and 91 check the value of the third parameter to determine whether to do the extra work. Writing to the “dereferenced pointer” is done with Marshal.WriteInt32
in Lines 56 and 76–77 and Marshal.WriteInt64
in Lines 98–99. The cast to an integer in Line 77 could fail if more than 0x7FFFFFFF bytes were written, so a robust adapter class may wish to check for this case.
The Revert
and Clone
methods (Lines 154–158 and 189–192, respectively) throw a NotImplementedException
because there’s no corresponding functionality in the Stream
class. For an implementation of ManagedIStream.Clone
, you might be tempted to try casting the originalStream
instance to ICloneable
and using its cloning functionality, if it exists.
However, this does not give the same semantics as UCOMIStream
’s Clone
method, which is supposed to clone just the seek pointer so that the new seek pointer operates on the same underlying bytes. This functionality is not available from System.IO.Stream
.
Rather than throwing a NotImplementedException
, LockRegion
and UnlockRegion
(Lines 161–165 and 168–172, respectively) throw a COMException
with the HRESULT STG_E_INVALIDFUNCTION
because the documentation for these functions states that this is the HRESULT
that should be returned when a stream does not support locking. In the upcoming “Example: Marshaling Between System.IO.Stream
and IStream
” section, we’ll see how to support locking when the .NET stream is an instance of System.IO.FileStream
.
By defining a pair of adapter classes like ComStream
in Listing 20.2 and ManagedIStream
in Listing 20.3, you can transform one type to another without using any custom marshaling support from the CLR. At this point, you might be asking why we spent so much time looking at this approach because this chapter is supposed to be about custom marshaling. The answer is that this same approach is exactly what is done when you use the built-in custom marshaling support—it’s just encapsulated. The next section describes how custom marshaling works and how adapter classes like the ones in the previous two listings fit in.
There are usually three components involved in custom marshaling:
• The custom marshaler
• The consumers
• The adapter objects
The custom marshaler is an object that the CLR calls into when data transformations need to occur. The consumers of a custom marshaler are signatures marked with the MarshalAsAttribute
pseudo-custom attribute and the value UnmanagedType.CustomMarshaler
, specifying that a given parameter, return type, or field should not use the standard marshaling support. When a member marked with a custom marshaler (via MarshalAsAttribute
) is invoked, the CLR instructs the custom marshaler to do the marshaling and return an object representing the marshaled result. These objects are typically adapter objects but, as discussed in the “The Adapter Objects” section later in this chapter, they may not be.
Just as CCWs and RCWs are largely transparent to their users, so are adapter objects when they are used with the custom marshaling infrastructure. This transparency is the only reason for bothering with custom marshaling because everything can be accomplished by directly using adapter classes the same way they were used in the previous section.
Use custom marshaling to save your users the extra step of directly instantiating adapter classes to convert between two different data types.
There are two requirements for a custom marshaler. It must:
• Implement the System.Runtime.InteropServices.ICustomMarshaler
interface
• Implement a static GetInstance
method
The reason there are two requirements rather than just one (such as only implementing the ICustomMarshaler
interface) is that interfaces cannot have static members. (Well, they can in IL Assembler but not with the semantics needed here.)
ICustomMarshaler
InterfaceSystem.Runtime.InteropServices.ICustomMarshaler
is a relatively simple interface, shown as follows in C# syntax:
public interface ICustomMarshaler
{
object MarshalNativeToManaged(IntPtr pNativeData);
IntPtr MarshalManagedToNative(object ManagedObj);
void CleanUpNativeData(IntPtr pNativeData);
void CleanUpManagedData(object ManagedObj);
int GetNativeDataSize();
}
In these methods, native is used as another term for unmanaged. Here are descriptions of the five methods:
• MarshalNativeToManaged
. This method is called by the CLR whenever an object must be marshaled from unmanaged code to managed code. It’s called with a pointer to the unmanaged object, represented as a System.IntPtr
type. The implementer must return an appropriate .NET object representing the “managed view” of the unmanaged object. This can be an instance of an adapter class like ComStream
in the previous section.
• MarshalManagedToNative
. This method is called by the CLR whenever an object must be marshaled from managed code to unmanaged code. It’s called with the managed object instance. The implementer must return a pointer to an appropriate unmanaged object (as a System.IntPtr
type) representing the “unmanaged view” of the .NET object. This object can be an instance of an adapter class like ManagedIStream
in the previous section. If a COM interface pointer is returned, its reference count must be incremented.
• CleanUpNativeData
. This method is called by the CLR when it’s appropriate to clean up the unmanaged object allocated in the MarshalManagedToNative
method. This can include decrementing the reference count for a COM object that was incremented in MarshalManagedToNative
, freeing memory that was allocated, unpinning memory that was pinned, and so on. The passed-in System.IntPtr
parameter is a pointer to whatever needs to be cleaned up.
• CleanUpManagedData
. This method is called by the CLR when it’s appropriate to clean up the managed object allocated in the MarshalNativeToManaged
method. Of course, because managed objects are garbage-collected, typically nothing needs to be done in this method. Even if the object implements IDisposable
, you should not call its Dispose
method unless you’re performing custom marshaling by value. This is discussed in the upcoming “An Alternative to Adapter Objects” section. The passed-in object parameter is the instance that could potentially require clean-up.
• GetNativeDataSize
. This method is supposed to return the size, in bytes, of the unmanaged type being marshaled if it’s a value type, or –1 to indicate that you’re not marshaling a value type. Because custom marshaling of value types is not supported in version 1.0 of the .NET Framework, this method is never called by the CLR. See the “Limitations” section in this chapter for more information. This method’s implementation should always return –1.
GetInstance
MethodBesides implementing ICustomMarshaler
, a custom marshaler must implement a static (Shared
in VB .NET) method named GetInstance
with the following signature (shown in C#):
static ICustomMarshaler GetInstance(string cookie)
Because this method is not part of the ICustomMarshaler
interface definition, you can’t rely on the compiler to remind you to implement this method.
Remember to implement GetInstance
when writing a custom marshaler (and be sure to make it static), because your compiler can’t enforce this requirement. Failure to do so causes an exception at run time when the CLR attempts to use your custom marshaler.
GetInstance
is called by the CLR in order to get an instance of the custom marshaler. The string parameter is a “cookie” value that can be specified by the custom marshaler consumer and used by the custom marshaler however it wishes. The following is a proper implementation of GetInstance
in C# that ignores the cookie value and assumes the presence of a private class member called marshaler
:
public static ICustomMarshaler GetInstance(string cookie)
{
if (marshaler == null)
marshaler = new MyMarshaler();
return marshaler;
}
This instantiation pattern makes the custom marshaler a singleton object, which is appropriate because custom marshalers must not maintain any state. This is the case because the CLR calls GetInstance
only once per application domain per parameter, field, or return type that uses the custom marshaler, regardless of how many times the marshaler is used.
Similar to the custom registration functions presented in Chapter 12, “Customizing COM’s View of .NET Components,” the GetInstance
method doesn’t have to be public because the CLR can invoke it regardless of its visibility, but it must be static.
The consumers of a custom marshaler were described earlier as any member signatures with a parameter, return type, or field marked with MarshalAsAttribute
and the value UnmanagedType.CustomMarshaler
. This works on any parameters and return values, regardless of whether they belong to a PInvoke signature or a member of a COM object, and works on any fields, regardless of whether they are exposed as fields of a structure or properties of a class. Recall that to use MarshalAsAttribute
with a property, you must mark the parameter and return type of its accessor methods.
MarshalAsAttribute
has the following named parameters relevant to custom marshaling:
• MarshalTypeRef
. This can be set to the type of a custom marshaler class, using typeof
in C#, GetType
in VB .NET, or __typeof
in C++. Either this named parameter or MarshalType
must be used with UnmanagedType.CustomMarshaler
, but MarshalTypeRef
is recommended.
• MarshalType
. This can be set to a string specifying the custom marshaler type. This can be simply the namespace-qualified type name if it resides in the same assembly, but it must be qualified with a full assembly name if it’s in a different assembly. This is useful for a “late bound” reference to a custom marshaler type, which does not require its metadata at compile time.
• MarshalCookie
. This can be set to a string that will be passed to the custom marshaler’s GetInstance
method. If MarshalCookie
is not set, an empty string is passed.
Either MarshalTypeRef
or MarshalType
must be used to specify the custom marshaler type, but MarshalCookie
is optional. If both MarshalTypeRef
and MarshalType
are specified (which you should never do), MarshalTypeRef
is ignored. If the custom marshaler type specified by MarshalTypeRef
or MarshalType
cannot be loaded when a marked member is called, a TypeLoadException
is thrown.
The use of MarshalAsAttribute
for custom marshaling is demonstrated in the following C# signatures:
The choice of using MarshalTypeRef
with a Type
object versus MarshalType
with a string is just like the choice encountered with ComSourceInterfacesAttribute
, discussed in Chapter 13, “Exposing .NET Events to COM Clients.” You should use MarshalTypeRef
whenever possible, because the compiler emits a string with the full assembly name, as can be seen using ILDASM.EXE
:
Or, if the custom marshaler type happens to be in the same assembly as the attribute using MarshalTypeRef
, only the type name is emitted.
Although using a partial assembly name with MarshalType
, such as "StreamMarshaler, StreamMarshaler"
works for assemblies not in the Global Assembly Cache, you should not use this if there’s a chance that the custom marshaler will ever be installed in the GAC at a later date.
By-reference or out parameters are automatically supported without any extra work needed by the custom marshaler. For example, both MarshalManagedToNative
and MarshalNativeToManaged
are called when marshaling a by-reference parameter, but only one of them is called when marshaling a by-value parameter. Which one is called depends on the direction of the call. To get both MarshalManagedToNative
and MarshalNativeToManaged
to be invoked when marshaling a by-value reference type, a consumer would need to mark the parameter with both InAttribute
and OutAttribute
, a requirement that matches the behavior of standard marshaling. This is often not necessary, however, because the use of adapter classes means that changes to the reference type’s state (such as the bits in a stream) are seen after the call anyway because the callee is indirectly using the same object instance.
The previously shown C# signatures are exported to a type library as follows:
HRESULT GetStream([out, retval] IUnknown** pRetVal);
HRESULT WriteToStream([in] IUnknown* s);
HRESULT GetStreamViaOutParameter([out] IUnknown** s,
[out, retval] VARIANT_BOOL* pRetVal);
HRESULT GetStreamViaByRefParameter([in, out] IUnknown** s,
[out, retval] VARIANT_BOOL* pRetVal);
HRESULT PrintString([in] long s);
HRESULT SetData([in] long data);
Notice that the unmanaged type to which each managed type is being custom-marshaled does not show up in an exported type library. That’s because the MarshalAsAttribute
contains no information about what unmanaged type a custom marshaler marshals; it’s hidden as an implementation detail. Users of a custom-marshaled object must call QueryInterface
to get a pointer to the desired interface. Visual Basic 6 users don’t have to do anything because the VB6 runtime does the QueryInterface
call, although the type will appear as Unknown
in the object browser and IntelliSense.
Also, notice that custom-marshaled string and array parameters are exported as long
. This really represents a pointer value; in other words, void*
. Such a type library works fine on a 32-bit platform, but is not a portable way to express a pointer.
To work around limitations of what the type library exporter produces for custom-marshaled parameters, you could change the type library (using OLEVIEW.EXE
to get an IDL representation then using MIDL.EXE
to compile a new type library) or create a new one yourself. It’s valid to change a custom-marshaled IUnknown*
to a specific interface like IStream*
if and only if the custom marshaler always produces a pointer to IStream
or a derived interface. This depends on the implementation of the custom marshaler, because it may validly produce a plain IUnknown
pointer instead of a more specific interface. It’s also a good idea to change the long
type exported to void*
for custom-marshaled strings and arrays.
Adapter objects are typically what a custom marshaler returns from ICustomMarshaler.MarshalNativeToManaged
and ICustomMarshaler.MarshalManagedToNative
. To demonstrate, Listing 20.4 (when combined with the previous two listings) contains a complete custom marshaler to transform a .NET Stream
to a COM IStream
and vice-versa. This can be compiled from a command prompt as follows:
csc StreamMarshaler.cs ComStream.cs ManagedIStream.cs /unsafe
Listing 20.4. StreamMarshaler.cs
. A C# Custom Marshaler That Converts Stream
to IStream
and Vice-Versa
MarshalNativeToManaged
returns null on Line 11 if the incoming pointer value represents null, or returns a new ComStream
instance from the object implementing UCOMIStream
on Line 17. This object is the standard RCW obtained from the incoming IntPtr
value using Marshal.GetObjectForIUnknown
on Line 13.
In the reverse direction, MarshalManagedToNative
constructs a new ManagedIStream
instance from the incoming object on Line 30 and uses Marshal.GetComInterfaceForObject
to create a standard CCW for the object. The pointer to the CCW is returned in Lines 31–32. Marshal.GetComInterfaceForObject
increments the reference count for the CCW, so CleanUpNativeData
calls Release
on the same object in Line 37 to avoid a reference count leak.
Marshal.GetIUnknownForObject
could have been called instead of Marshal.GetComInterfaceForObject
, but it’s better to return an IStream
pointer rather than an IUnknown
pointer in case consumers of the custom marshaler are tempted to modify a type library to change an exported IUnknown*
corresponding to a custom marshaled Stream
parameter to IStream*
.
Using members of the Marshal
class in custom marshalers is a great way to leverage bits and pieces of standard marshaling functionality inside your custom transformations. Several of its methods, such as the GetHRForException
and ThrowExceptionForHR
methods used in the upcoming “Marshalers, Marshalers, Marshalers!” section, were designed to be used inside custom marshalers. Appendix A, “System.Runtime.InteropServices
Reference,” covers every method of the Marshal
class.
Because C# code cannot interact with COM objects directly, it can’t truly provide a custom CCW and custom RCW as explained at the beginning of the chapter. Instead, the custom marshaler in Listing 20.4 uses the standard CCW for the adapter object to act as a custom CCW for the original .NET Stream
object when marshaling from .NET to COM. In the reverse direction, the marshaler wraps a standard RCW with the ComStream
wrapper rather than actually replacing the RCW that directly wraps the COM object. These two situations are pictured in Figures 20.3 and 20.4, using the example of Stream
on the .NET side and IStream
on the COM side.
Figure 20.3. Using the standard CCW for an adapter object when marshaling from .NET to COM.
Figure 20.4. Using the standard RCW with an adapter object when marshaling from COM to .NET.
This approach is valid, and is pretty straightforward to implement because everything primarily stays in the managed realm. Using Visual C++ .NET with Managed Extensions, however, you can make one adapter object the RCW and the other one the CCW. This means using the C++ capabilities of mixing managed and unmanaged code to handle most or all of the interactions normally done by the CLR-supplied wrappers. The custom CCW is a full-fledged COM class (even with an explicit implementation of IUnknown
) that happens to interact with managed objects. The custom RCW is still a .NET class, but one that does not need to rely on COM Interoperability to interact with unmanaged objects.
Figures 20.5 and 20.6 illustrate this approach in both directions, using the example of Stream
on the .NET side and IStream
on the COM side. Although these diagrams show no use of standard RCWs, in practice a standard RCW is often used by the custom RCW as an easy way to ensure context-safe calls on the wrapped COM interface. This will be demonstrated in Listing 20.9 later in the chapter. Listings 20.9 and 20.10 will re-implement the two adapter classes from Listings 20.2 and 20.3 in C++ to demonstrate how to implement this custom marshaling technique.
Figure 20.5. Using an unmanaged adapter object that is a custom CCW when marshaling from .NET to COM.
Figure 20.6. Using a managed adapter object that is a custom RCW when marshaling from COM to .NET.
Although the diagrams for this case are simpler, implementing the adapter objects may be more complicated. Here are the pros and cons of using C++ to implement adapter objects that work directly with managed and unmanaged code:
PROS:
• Better performance. Besides the fact that you can eliminate a wrapper in each direction, the other approach sometimes involves extra data transformations. For example, supporting non-zero offsets in ComStream.Read
and ComStream.Write
in Listing 20.2 would’ve involved copying one .NET array to another before exposing it to COM. When the adapter class is rewritten in Listing 20.9, you’ll see that a pointer to the correct element in the array can be exposed directly without performing any copying.
• No limitations. Because the COM adapter object uses its .NET object directly and the .NET adapter object uses its COM object directly, you aren’t limited by the support given by the standard RCW and CCW objects. For example, you could support VARIANT
s with the VT_RECORD
type.
CONS:
• Less built-in support. The price of total freedom is the fact that you have to do more yourself. For example, you’re responsible for all data transformations between COM and .NET—manually converting between .NET strings and BSTR
s, .NET arrays and SAFEARRAY
s, .NET objects and VARIANT
s, and so on. (The System.Runtime.InteropServices.Marshal
class helps a great deal, however, by providing small chunks of standard marshaling functionality that can be used by your custom wrappers.) Also, whereas a standard CCW implements many standard interfaces (like IProvideClassInfo
, ISupportErrorInfo
, IConnectionPointContainer
, and so on), a custom CCW COM object doesn’t magically implement any interfaces that you don’t explicitly implement in your source code.
• Difficulty integrating with .NET code in other languages. If you don’t mind having your custom marshaler in a separate assembly or are doing your development in C++ anyway, this doesn’t apply. People often have the urge to contain all their functionality in a single file, but if your primary development language is C# or VB .NET, you can’t merge your custom marshaling code with your other code without ILDASM.EXE
and ILASM.EXE
trickery. You could also opt to produce a C++ module and a C# module and link them into a multi-file assembly.
Listing 20.5 contains the code for a typical custom marshaler written in C++, assuming the existence of two adapter classes: CustomCCW
—an unmanaged COM class, and CustomRCW
—a .NET class. This custom marshaler will be used with the C++ adapter classes implemented in the “Marshalers, Marshalers, Marshalers!” section later in this chapter, replacing UnmanagedType and ManagedType with the types being custom-marshaled.
Listing 20.5. A Typical Custom Marshaler Written in C++ That Uses Adapter Classes Serving Directly as Custom CCWs and RCWs
In the C#-authored custom marshaler, Marshal.GetObjectForIUnknown
was used to convert an IntPtr
to an object inside MarshalNativeToManaged
. Here, in Line 11, IntPtr
’s ToPointer
method is called, which returns void*
. This void*
can then be cast to the desired .NET type. This type is passed to the .NET CustomRCW
adapter object.
In C++, use IntPtr.ToPointer
as an intermediate step when converting an IntPtr
value to a class or interface type. This is necessary because you cannot cast an IntPtr
value to a reference type directly.
In MarshalManagedToNative
, Line 19 calls QueryInterface
directly on the CustomCCW
object and Line 20 casts the returned interface pointer to return directly to an IntPtr
value. This can only be done when CustomCCW
is an unmanaged COM class. If CustomCCW
is a .NET class instead, it wouldn’t really be a CCW, so Marshal.GetComInterfaceForObject
could be used to obtain an IntPtr
value representing an interface pointer for the CLR-supplied CCW for the CustomCCW
.NET class.
The rest of the listing is essentially the same as the C# version. The “Marshalers, Marshalers, Marshalers!” section later in this chapter shows examples demonstrating how to write CustomCCW
and CustomRCW
classes that can be used by such a custom marshaler.
Both adapter classes in Listings 20.2 and 20.3 had an UnderlyingStream
property that provided users of the adapter object the ability to retrieve the original object. This is helpful if the original object contains other functionality that is not made available by the adapter object that only knows about the stream functionality. Such a mechanism is important for adapter objects used by a custom marshaler because users of the custom marshaler don’t have access to the original objects from the other side of the managed/unmanaged transition.
For an illustration of why this can be important, consider the Visual Basic 6 error object (ErrObject
). The VB6 runtime wraps an incoming object that implements IErrorInfo
, presenting the information in the way expected by Visual Basic 6 programs. However, because there’s no way to access the original object implementing IErrorInfo
from a Visual Basic 6 error object, you can’t get a hold of _Exception
or any other interfaces implemented by error objects created from .NET exceptions. Had ErrObject
been defined with a method like GetErrorInfoObject
, .NET-aware Visual Basic 6 programs could have taken advantage of this extra functionality.
The System.Runtime.InteropServices
namespace defines an interface that formalizes the practice of exposing the wrapped object. This interface is called ICustomAdapter
and has a single parameter-less method—GetUnderlyingObject
. As you might expect, GetUnderlyingObject
returns a System.Object
instance that should be the original object. All adapter objects should implement this interface, so those clients that know to look for it can take advantage of it. Unlike COM aggregation, the outer object can’t dynamically support arbitrary interfaces of the inner object directly, so providing access to the inner object is the next best thing.
Using adapter objects can be considered custom marshaling by reference. The term marshal by reference is used elsewhere in the .NET Framework when discussing objects marshaled across application domain boundaries. When an object is marshaled by reference across an application domain, a proxy handles communication with the original object. Similarly, when an object is custom marshaled by reference across the managed/unmanaged boundary, the adapter object acts like a proxy.
But adapter objects aren’t the only means of implementing custom marshalers, and sometimes aren’t even an option. Adapter objects can always be used when transforming from one interface to another. Consider a class such as System.Drawing.Font
, however, which we want to custom marshal with IFont
, the interface representing a font in COM. The issue here is that Font
is sealed (NotInheritable
in VB .NET), so it’s impossible to create a derived class that acts like a Font
and forwards its implementation to an object implementing IFont
.
Instead, when marshaling IFont
(a “COM font”) to Font
(a “.NET font”), a custom marshaler could simply create an instance of the Font
class, initialize it with the state of an IFont
object, and pass this new “detached” object as the custom-marshaled IFont
. In the reverse direction, an adapter object could still be used to provide an IFont
implementation that forwards calls to a Font
object. Or, the copying approach could be used here as well: instantiate a StdFont
class (a standard OLE Automation class that implements IFont
), initialize it with the state of a Font
object, and pass this new object as the custom-marshaled Font
.
This approach that doesn’t use adapter objects can be considered custom marshaling by value. When an object is marshaled by value across an application domain boundary, it is copied from one domain to another. Similarly, when an object is custom marshaled by value across the managed/unmanaged boundary, the original object’s state is copied to an object of a different but related type.
Figures 20.7 and 20.8 illustrate custom marshaling by value in both directions. They assume that a language like C# or Visual Basic .NET is being used. If custom marshaling by value is performed in Visual C++ .NET, the standard RCW can be eliminated because the COM object (in this case, StdFont
) can be used directly by the custom marshaler.
Figure 20.7. Custom marshaling by value from .NET to COM.
Figure 20.8. Custom marshaling by value from COM to .NET.
Custom marshaling by value works naturally for fonts, as it’s easy to imagine initializing a COM font object with the state of a .NET font object, and vice-versa. As long as two font objects have the same name, size, and settings for bold, italic, and so on, the new object should represent the old one just fine. In addition, the System.Drawing.Font
class already has methods that facilitate such a transformation, so you don’t have to get and set properties one-by-one to perform the copying. This will be demonstrated in Listing 20.6.
Although the sealed nature of Font
requires custom marshaling by value (at least in one direction), it’s sometimes a more attractive approach than custom marshaling by reference anyway. Here’s a list of pros and cons to custom marshaling by value:
PROS:
• Ease in custom marshaler implementation. For a pair of objects that provide a public means for getting/setting their state (or for immutable objects that provide a public means of getting their state), custom marshaling by value can be much easier than writing adapter classes that implement all of an object’s public members and forward the calls appropriately. In addition, existing objects that you instantiate often implement several other interfaces that clients might find handy. For example, the StdFont
class implements IFontDisp
in addition to IFont
.
CONS:
• Poorer performance. The overhead of copying an object’s state at every managed/unmanaged transition can substantially hurt performance. For font objects the copying may not be noticeable, but for streams with large buffers the extra copying can be unacceptable. When marshaling needs to occur in both directions for a single call (as with a by-reference parameter), the copying needs to occur twice.
• Subtlety for consumers. When types are custom marshaled by value, any changes made by the callee to the state of an object do not get reflected to the caller after the call by default. This behavior is often non-intuitive for reference types because they exhibit marshal-by-reference behavior in pure managed code. (The marshal-by-value behavior does match the default behavior for non-blittable reference types when called across the managed/unmanaged boundary, however.) To get changes to an object’s state propagated back to the caller, a signature either must use the custom marshaler on a by-reference parameter or mark a by-value parameter with InAttribute
and OutAttribute
.
• Limiting. Custom marshaling by value means that providing functionality like ICustomAdapter.GetUnderlyingObject
isn’t possible. For example, once an object implementing IFont
has been returned by a custom marshaler, the original Font
object cannot be accessed (just like the IErrorInfo
/ErrObject
example).
Regardless of whether you choose custom marshaling by reference or custom marshaling by value, you should document the behavior for consumers of your custom marshaler so they know what behavior to expect in terms of seeing changes to a parameter’s state after a method call (and whether to use InAttribute
and OutAttribute
).
Now that we’ve covered all the background information about custom marshaling, it’s time for some examples. First we’ll look at three examples of converting between related COM and .NET types:
• System.Drawing.Font
and IFont
• System.IO.Stream
and IStream
• System.String
and SAFEARRAY
The first example uses custom marshaling by value, as pictured in Figures 20.7 and 20.8. The second example is similar to the custom marshaler already covered in Listings 20.2, 20.3, and 20.4, but uses C++ to demonstrate more direct custom marshaling by reference, as pictured in Figures 20.5 and 20.6. Plus, the second example adds a few more features that the C# StreamMarshaler
does not have. The third example isn’t practical like the first two, but demonstrates custom marshaling with something other than an interface on the COM side—a SAFEARRAY
.
In addition to these three examples of transforming one type to another, we’ll cover a fourth example that modifies a type while preserving its interface identity. In this example, we’ll see how to use a custom RCW to provide deterministic release of resources held by any .NET object exposed to COM.
Here’s the first example of a custom marshaler that marshals its objects by value. As you’ll see in these listings, custom marshaling by value often involves much less code than custom marshaling by reference because no adapter objects are involved. This example is broken up into two parts:
• The custom marshaler
• A sample consumer and its client
The remaining examples only show custom marshalers, because the consumers of any custom marshalers should be self-explanatory once you see an example of one.
Listing 20.6 contains FontMarshaler
, a complete custom marshaler class that marshals System.Drawing.Font
to IFont
and vice-versa. This class requires .NET type definitions for the IFont
interface and FONTDESC
and LOGFONT
structures. FONTDESC
and LOGFONT
are defined in the listing, but the definition of IFont
is available on this book’s Web site in the IFont.cs
file. This definition is written in C# due to a Visual Basic .NET limitation described in the next chapter, “Manually Defining COM Types in Source Code.” Therefore, you must perform the following command-line steps to compile Listing 20.6:
2. vbc /t:library FontMarshaler.vb /r:IFont.dll /r:System.Drawing.dll
Listing 20.6. FontMarshaler.vb
—A Custom Marshaler That Marshals Between .NET Fonts and COM Fonts
To perform the custom marshaling, we need a way to convert a .NET font to a COM font, and vice-versa. System.Drawing.Font
doesn’t have a method to construct an instance of itself from an IFont
interface, but it does have a method to construct one from a Windows font handle (an HFONT
)—Font.FromHFont
. Fortunately, IFont
has a property called hFont
that retrieves a Windows handle to the font object. Therefore, the MarshalNativeToManaged
implementation creates a .NET font from the incoming pointer to a COM font by calling Font.FromHFont
on Line 23. An interesting point about Font.FromHFont
is that it throws an exception if the input font isn’t a TrueType font, because GDI+ doesn’t support fonts that aren’t. A more accommodating custom marshaler could use a heuristic to find a best match TrueType font for any incoming non-TrueType font. In this simple example, we let the exception get exposed to the user.
In the reverse direction, MarshalManagedToNative
calls the Windows API OleCreateFontIndirect
in Line 50 to create a StdFont
object that implements IFont
. Creating such an object requires a FONTDESC
structure filled with information explaining what kind of font is desired. Lines 39–45 fill in this information with data obtained from the incoming .NET Font
instance. Font.ToLongFont
is called in Line 34 so we can get information about the font’s weight and character set because this information isn’t available directly from the Font
class itself. A font’s size is stored differently in COM fonts than in .NET fonts, so the .NET font size is multiplied by 10,000 in Line 40 to account for the different representation. OleCreateFontIndirect
returns an HRESULT
, so Line 51 checks for a failure value then, if necessary, throws the appropriate exception using Marshal.ThrowExceptionForHR
. By defining the OleCreateFontIndirect
PInvoke signature with DllImportAttribute
and setting PreserveSig
to false, the HRESULT
to exception transformation could’ve been handled by the CLR instead. But for the simplicity of this example, Declare
is used to define the PInvoke method in Lines 83–86.
The call to OleCreateFontIndirect
increments the StdFont
object’s reference count, and the call to Marshal.GetComInterfaceForObject
increments the object’s reference count again, so CleanUpNativeData
calls Marshal.Release
twice in Lines 60–61 to properly restore its reference count. Because Font
implements IDisposable
, and because we’re marshaling it by value, CleanUpManagedData
calls IDisposable.Dispose
in Line 67.
Lines 89–117 contain the .NET definitions of the FONTDESC
and LOGFONT
structures. Although FONTDESC
is defined as a value type, LOGFONT
is defined as a formatted reference type for an extremely subtle reason. The System.Drawing.Font.ToLogFont
method called in Line 34 has a System.Object
parameter that must be a LONGFONT
instance to be filled in with information. Had LOGFONT
been defined as a value type, it would be boxed for the call to ToLogFont
and any changes to the boxed object’s fields would be lost after the call. If you want the state of a by-value parameter to be changed after the call, it must be a reference type. The use of StructLayoutAttribute
in Line 100 not only formats the LOGFONT
reference type with sequential layout, but it also sets the character set of the lfFaceName
field appropriately.
Listing 20.7 contains a .NET Windows Form that uses the FontMarshaler
custom marshaler from Listing 20.6 on a property that gets/sets the font of its TextBox
control. This form is the consumer of the custom marshaler, and can be built from a command prompt as follows:
vbc /t:library ManagedForm.vb /r:System.Windows.Forms.dll /r:System.Drawing.dll /r:System.dll /r:FontMarshaler.dll
Listing 20.7. ManagedForm.vb—
A Windows Forms Control That Uses the FontMarshaler
Custom Marshaler
This is a standard Windows Form with a TextBox
, a Button
for changing the font, and a FontDialog
. The important part is emphasized in Lines 31–42—a TextBoxFont
property of type Font
, marked with the FontMarshaler
custom marshaler on both accessors. This property gets and sets the font of the form’s TextBox
control.
If .NET clients were to use the ManagedForm
class and get/set its TextBoxFont
property, custom marshaling would not be involved because there would be no managed/unmanaged transition. The MarshalAsAttribute
marking would be ignored and Font
instances could be passed back and forth as usual. If COM clients use the ManagedForm
class, however, they view the property as enabling them to get/set the font using an IFont
instance.
Listing 20.8 contains such a Visual Basic 6 client that instantiates the Windows Forms component and enables a user to invoke the .NET property’s get and set accessors. The combination of these two forms is a testing application that enables you to see the successful custom marshaling of fonts right before your eyes. The application is pictured Figure 20.9.
Figure 20.9. Using FontMarshaler
to achieve font interoperability.
Listing 20.8. VB6Form.frm
—A Visual Basic 6 Form That Uses the Visual Basic .NET Form
This form is a standard Visual Basic 6 form with a TextBox
, three Button
s, and a CommonDialog
control used for selecting a font. Line 5 initializes the TextBox
to a TrueType font because those are the only fonts supported by the .NET Font
type. Line 8 instantiates the Windows Form, and Line 9 makes it visible by calling Show
through its class interface.
Lines 12–27 contain the two methods that invoke the .NET property’s accessors. Line 15 calls the custom-marshaled TextBoxFont
property get accessor when the user clicks Get .NET Font
, and Line 26 calls the custom-marshaled TextBoxFont
property set accessor when the user clicks Set .NET Font
. Notice how the fact that the property is of type IUnknown
doesn’t affect the Visual Basic 6 client code one bit.
The ChangeFontButton_Click
method in Lines 29–57 handles the font dialog displayed to the user. Although the dialog presents the user with a choice of font color, color information is not part of the information stored with IFont
.
To build this application, you should register the ManagedForm
assembly and generate a type library. For example:
regasm ManagedForm.dll /tlb
Then create a new Visual Basic 6 Standard EXE
project, reference the exported type library, and paste the code from Listing 20.8. Add the controls pictured in Figure 20.9 to the form and rename them appropriately. Don’t forget that if you plan to run the application within the Visual Basic 6 IDE, you should either add the ManagedForm
assembly (and its dependent FontMarshaler
and IFont
assemblies) to the GAC, or simply use REGASM.EXE
’s /codebase
option when performing the registration.
System.IO.Stream
and IStream
This example contains two listings—one with the contents of CustomRCW
and one with the contents of CustomCCW
. The CustomRCW
class serves the same purpose as ComStream
in Listing 20.2 except that it uses the power of mixed-mode C++ programming to wrap a COM object directly rather than wrapping its RCW. The CustomCCW
class serves the same purpose as ManagedIStream
in Listing 20.3 but is actually a COM class wrapping a .NET Stream
object.
These two classes can be used with a custom marshaler just like TypicalCustomMarshaler
in Listing 20.5, replacing UnmanagedType
with IStream
and ManagedType
with Stream
. So the two important ICustomMarshaler
methods would look like the following:
Object* MarshalNativeToManaged(IntPtr pNativeData)
{
if (pNativeData == IntPtr::Zero) return NULL;
return new CustomRCW(
static_cast<IStream*>(pNativeData.ToPointer()));
};
IntPtr MarshalManagedToNative(Object* ManagedObj)
{
if (ManagedObj == NULL) return IntPtr::Zero;
CustomCCW *ccw = new CustomCCW(static_cast<Stream*>(ManagedObj));
IStream* intf = NULL;
ccw->QueryInterface(IID_IStream, (void**)&intf);
return (IntPtr)intf;
}
The resultant custom marshaler can be found on this book’s Web site in the file StreamMarshaler.cpp
.
Listing 20.9 contains the definition of CustomRCW
, which wraps an IStream
implementation in a Stream
-derived object.
Listing 20.9. The CustomRCW
Adapter Class That Exposes IStream
as Stream
Line 2 includes the objidl.h
header file, part of the Windows Platform SDK, because it contains an unmanaged definition of the IStream
interface. The class is defined as a managed class with the __gc
in Line 9. Besides deriving from Stream
, it implements ICustomAdapter
, as all adapter classes should.
The CustomRCW
constructor in Lines 13–23 is passed an unmanaged COM IStream
interface pointer. Notice, however, that rather than storing the IStream
pointer as a class member to be used directly by CustomRCW
’s other methods, it calls GetObjectForIUnknown
in Line 17 to construct a standard RCW for the COM object. This RCW is used by the other methods, which call GetComInterfaceForObject
to extract the original IStream
interface pointer wrapped by this RCW. Why does CustomRCW
do this extra wrapping and unwrapping instead of storing and using the IStream
pointer directly? The answer is that storing and using a COM interface pointer directly is not context-safe. For example, if managed code uses CustomRCW
from a different apartment than the one it was created in, calling a cached COM interface pointer directly would violate COM rules. The easiest way to ensure context safety is to let the CLR handle cross-context COM marshaling automatically. When you use a standard RCW, calls to the COM object always occur in the correct context. Of course, you could alternatively use your own mechanism to ensure context-safety that doesn’t involve standard RCWs, such as COM’s global interface table (GIT).
Rather than storing the unmanaged COM interface pointer you’re wrapping as a member of a custom RCW, you should store its standard RCW to ensure that it is always used in a context-safe manner. Although this contradicts Figure 20.6, which illustrates custom RCWs written in C++ as not wrapping standard RCWs, doing so can still perform better than writing a custom marshaler in C# or Visual Basic .NET in some cases because the raw COM object can be extracted from the standard RCW before calling its members.
The Read
method in Lines 39–76 is noticeably different from the C# ComStream.Read
method in Listing 20.2. In Line 56, the incoming .NET array is pinned so the address of the object can be passed directly to IStream.Read
in Line 64. The call to GCHandle.Alloc
is placed inside a try...finally
block so the allocated GC handle always gets freed. Also inside the try...finally
block, Lines 59–60 extract the IStream
COM interface pointer from the standard RCW created in the constructor using GetComInterfaceForObject
(which increments the reference count), and Line 72 releases the pointer. This same pattern is used in every method that needs to call a member of IStream
.
Because a pointer to the pinned array is directly accessible, we can support non-zero offsets without having to perform any copying, unlike in Listing 20.2. (Listing 20.2 could have achieved the same effect as this listing by using a custom .NET definition of IStream
that uses the ArrayWithOffset
value type. See Appendix A, “System.Runtime.InteropServices
Reference,” for more information about this type.) Line 64 simply adds the offset to the pointer value. Lines 66–67 check the HRESULT
returned from the call to IStream.Read
and use the handy Marshal.ThrowExceptionForHR
method to throw an exception just like the standard Interop marshaler would if it were throwing an exception for a failure HRESULT
. If the IStream.Read
implementation sets rich error info via IErrorInfo
, the thrown exception gets automatically populated with this information.
Using Marshal.ThrowExceptionForHR
is needed throughout a custom RCW implementation to properly transform failure HRESULT
s and additional IErrorInfo
information into exceptions, unless you plan to provide a custom mapping from HRESULT
values to exception types. Although Listing 20.9 checks to see if a returned HRESULT
has a failure value before calling Marshal.ThrowExceptionForHR
, this actually isn’t necessary because this method only throws an exception if the HRESULT
has a failure value. Still, leaving the check makes the code easier to follow (and faster in non-failure cases).
The Write
method in Lines 80–116 is structured just like the Read
method. The get_Length
property accessor in Lines 151–176 has an interesting detail that’s worth pointing out. The unmanaged STATSTG
struct is used by its other name of tagSTATSTG
because using STATSTG
would be ambiguous between the unmanaged struct defined in objidl.h
and the .NET definition of the same struct in System.Runtime.InteropServices
. The only other way around this problem would be to omit the using namespace System::Runtime::InteropServices
directive from Line 6, but doing so would make the code practically unreadable due to the abundance of System::Runtime::InteropServices
qualifications that would litter the code. Doing mixed-mode programming in Visual C++ .NET is filled with little nuances such as this to appease both managed and unmanaged worlds simultaneously.
Listing 20.10 contains the definition of CustomCCW
, which wraps a Stream
implementation in a COM object that implements IStream
. It has a few special cases to provide more functionality if the object being wrapped is an instance of FileStream
.
Listing 20.10. The CustomCCW
Adapter Class That Exposes Stream
as IStream
, with Extra Support for FileStream
Line 10 begins the definition of the CustomCCW
class that implements IStream
. It can’t implement IDisposable
or ICustomAdapter
like the CustomRCW
class does in Listing 20.9 because COM classes can’t directly implement .NET interfaces using mixed-mode C++ programming. Still, it implements a custom GetUnderlyingObject
method (Lines 74–77) for the benefit of COM clients. (Alternatively, it could implement the exported ICustomAdapter
interface defined in MSCORLIB.TLB
, which would involve returning the IStream
interface pointer inside a VARIANT
.)
An important aspect of this listing is the use of the gcroot
template on Line 414. Visual C++ .NET does not allow an unmanaged class to contain a managed member (a __gc
pointer type), so defining the originalStream
field of type Stream
would cause a compilation error. Fortunately, the vcclr.h
header file referenced in Line 3, which contains helper functions specifically for C++ managed extensions, defines a gcroot
type-safe wrapper template to use specifically for this purpose. This template provides the illusion of having a managed member of an unmanaged class, but in reality an integer representing a GCHandle
is being stored inside the unmanaged object. The template has a destructor that frees the GCHandle
.
In Visual C++ .NET, use the gcroot
template defined in vcclr.h
whenever you require a managed member of an unmanaged class. Through clever wrapping of a GCHandle
, you can use a gcroot<
Type
*>
variable just as if it were defined as Type
*
. This is sometimes called a virtual __gc
pointer.
The constructor in Lines 14–31 differs from the ManagedIStream
constructor in Listing 20.3 in two ways. First, because CustomCCW
is a COM class, it must maintain a reference count, so Line 19 initializes it to zero. Second, Line 22 checks to see if the Stream
object being wrapped is also a FileStream
. The boolean isFileStream
variable is used later in the listing to provide more functionality specific to file streams. Lines 34–71 contain a standard implementation of IUnknown
’s methods. The InterlockedIncrement
and InterlockedDecrement
Windows APIs are used to provide thread-safe reference counting.
The IStream
methods follow the opposite pattern as in the previous listing—each method implementation is contained inside a try...catch
block and Marshal.GetHRForException
is used to return the failure HRESULT
corresponding to any .NET exception that may be thrown. The CopyTo
implementation in Lines 205–255 differs from the CopyTo
implementation from Listing 20.3 because it calls CustomCCW.Read
and CustomCCW.Write
rather than calling the underlying stream’s Read
and Write
methods directly. This works more naturally in this case because calling Stream.Read
and Stream.Write
would require more lines of code. Marshal.AllocHGlobal
is used in Line 222 to allocate memory for the temporary buffer and Marshal.FreeHGlobal
frees it in Line 249.
The LockRegion
, UnlockRegion
, and Stat
methods are the ones that do extra work if the wrapped Stream
object is a FileStream
instance. Although generic Stream
objects don’t support any locking or unlocking functionality, FileStream
objects do. FileStream
has a Lock
method called by LockRegion
in Lines 285–310, and an Unlock
method called by UnlockRegion
in Lines 313–338. In Stat
(Lines 343–386), more information in the structure can be filled in that applies to file streams only—the stream’s creation time, last access time, last write time, and possibly its name. The Stat
method uses _FILETIME
in Lines 359–361, rather than FILETIME
, for the same reason the previous listing used tagSTATSTG
—to avoid a name collision with System.Runtime.InteropServices.FILETIME
.
So far all the examples have custom marshaled .NET types to COM interfaces. This example demonstrates custom marshaling a .NET string to a SAFEARRAY
instead. This isn’t a practical custom marshaler like the others, but it’s an easy-to-follow example of custom marshaling to something other than an interface pointer on the COM side. The marshaling is accomplished by value, so Listing 20.11 contains the complete custom marshaler in the StringMarshaler
class, written in C++. This listing can be compiled from a command prompt as follows:
cl /CLR /LD StringMarshaler.cpp /link oleaut32.lib
Listing 20.11. The StringMarshaler
Custom Marshaler Marshals .NET Strings to COM SAFEARRAY
s and Vice-Versa
Line 3 includes oleauto.h
, a header file containing the definitions of several SAFEARRAY
-related functions. The System.Text
namespace is used in Line 6 for the UnicodeEncoding
class. The StringMarshaler
constructor initializes its encoding
member on Line 19, used by MarshalManagedToNative
to convert a .NET string to an array of bytes. The first parameter passed to the UnicodeEncoding
constructor chooses big-endian or little-endian ordering based on the current operating system. Passing false for the second parameter turns off the byte order mark (BOM), which we don’t want to include as part of the string data.
Lines 24–62 contain the implementation of MarshalNativeToManaged
. After obtaining a pointer to the SAFEARRAY
by calling ToPointer
on the input IntPtr
value, several SAFEARRAY
APIs defined in oleauto.h
are called using the IntPtr
value. Lines 35–40 ensure that the incoming array has only a single dimension and contains byte elements. The SafeArrayGetVartype
API only works reliably on SAFEARRAY
s created using SafeArrayCreateEx
or SafeArrayCreateVectorEx
, so checking for an element size of 1 byte instead is a sure-fire way to check the type of elements.
Lines 43–49 get the array’s upper and lower bounds, so the contents of the array can be copied regardless of the bounds. Lines 52–53 create a new .NET array with the correct number of elements, then the loop in Lines 56–59 fill the new array with the SAFEARRAY
’s data. This is done simply so it can be passed to the static BitConverter.ToString
method in Line 61.
MarshalManagedToNative
is defined in Lines 64–100. Line 81 obtains a .NET array for the string by calling the UnicodeEncoding.GetBytes
method. Then a new SAFEARRAY
is created using SafeArrayCreateVectorEx
in Lines 86–87, and the data from the .NET array is copied to the SAFEARRAY
in Lines 90–93. Line 99 returns an IntPtr
value representing the pointer to the SAFEARRAY
. The implementation of CleanUpNativeData
in Lines 102–106 frees the SAFEARRAY
allocated in Lines 86–87 with a call to SafeArrayDestroy
.
The standard CCW does not provide a means for deterministic release of resources other than having COM clients call IDisposable.Dispose
explicitly (if the .NET object implements IDisposable
). This can be problematic when .NET objects implementing COM interfaces are exposed to pre-.NET COM clients that can’t be modified. Such clients may rely on the fact that resources must be released in a timely manner, and calling an additional method is not an option. When defining a custom CCW, however, you can insert a call to IDisposable.Dispose
that executes when the CCW’s reference count reaches zero.
Disposing a .NET object when its CCW’s reference count reaches zero should not be done for objects that can potentially be used simultaneously by COM clients and .NET clients. Any .NET clients using the object are invisible to the object’s CCW, so the object could get disposed prematurely while others are still using it.
Another problematic situation occurs if the same .NET instance is passed to COM using custom marshaling with some signatures and standard marshaling with other signatures (due to a consumer not using the appropriate MarshalAsAttribute
everywhere the type is exposed). In this case, COM clients using an interface pointer for the standard CCW can become victims of premature disposal because the custom CCW cannot track this usage.
Therefore, the approach demonstrated by this custom marshaler should only be used in controlled situations.
Writing a custom marshaler to add dispose functionality must be done on an interface-by-interface basis, because custom marshalers must statically implement interfaces exposed to COM users. In this example, the custom marshaler is written for the IPersistFile
COM interface.
The custom marshaler class looks just like the TypicalCustomMarshaler
class in Listing 20.5, but with the following implementation of MarshalNativeToManaged
and MarshalManagedToNative
:
Object* MarshalNativeToManaged(IntPtr pNativeData)
{
if (pNativeData == IntPtr::Zero) return NULL;
// Just use the standard RCW
return Marshal::GetObjectForIUnknown(pNativeData);
};
IntPtr MarshalManagedToNative(Object* ManagedObj)
{
if (ManagedObj == NULL) return IntPtr::Zero;
CustomCCW *ccw = new CustomCCW(static_cast<UCOMIPersistFile*>(ManagedObj));
IPersistFile* intf = NULL;
ccw->QueryInterface(IID_IPersistFile, (void**)&intf);
return (IntPtr)intf;
};
The complete source code is available on this book’s Web site. No special marshaling is required in the native-to-managed direction because all we care about is customizing the exposure of a .NET object to COM. Therefore, calling Marshal.GetObjectForIUnknown
inside MarshalNativeToManaged
acquires the standard RCW for the COM object.
The C++ CustomCCW
adapter class used by the custom marshaler in MarshalManagedToNative
is implemented in Listing 20.12. This is unique from the other custom marshalers because the source and target types appear to be the same to their users—System.Runtime.InteropServices.UCOMIPersistFile
on the .NET side and IPersistFile
on the COM side.
Listing 20.12. Disposing an Object on Its Last Release Inside a Custom CCW
This listing is straightforward because the IPersistFile
methods simply call their corresponding UCOMIPersistFile
methods in Lines 71–146. The implementation mimics the work that would normally be done by the standard Interop Marshaler. The unique part of the listing occurs inside the implementation of IUnknown.Release
(Lines 53–60). If the wrapped object implementing UCOMIPersistFile
also implements IDisposable
, the object will be disposed when the CCW reference count equals zero. Otherwise, object release is performed the standard way.
The adapter class doesn’t bother exposing a GetUnderlyingObject
method because this custom CCW is meant for COM clients that aren’t .NET-aware. If they could be modified to call GetUnderlyingObject
, then they could be modified to call IDisposable.Dispose
and avoid the need for the custom marshaler in the first place.
Even when implementing a custom marshaler (and possibly adapter objects) using Visual C++ .NET, the custom marshaling infrastructure has some limitations. Some of these limitations were mentioned earlier in the chapter, but here’s a summary:
• Value types can’t be custom marshaled. Only reference types are supported in version 1.0 of the .NET Framework. This applies to both the .NET and COM sides of marshaling so, as an example, custom marshaling a System.Decimal
type to anything unmanaged or custom marshaling anything managed to a by-value VARIANT
type cannot be done. Note that you can custom marshal a boxed value type defined as an Object
type, or a formatted reference type. The formal type of the unmanaged parameter does not matter as long as it’s pointer-sized, so you could custom marshal a managed type to a pointer to an unmanaged structure (similar to the SAFEARRAY
example). Attempting to use a custom marshaler on a parameter, return type, or field defined as a value type causes a MarshalDirectiveException
to be thrown at run time.
• The type library exporter can’t faithfully represent the unmanaged view of a custom marshaled type in an exported type library. This isn’t a big limitation for classes or interfaces being exported as IUnknown*
, but the behavior of exporting strings or arrays as long
can be problematic.
• The type library importer can’t be taught to use additional custom marshalers. The importer makes use of a handful of built-in custom marshalers shipped with the .NET Framework, but there’s no way to plug in additional custom marshalers. Adding custom marshaling to an Interop Assembly would have to be done using the techniques in Chapter 7 instead.
• Only uses of a type can be marked as custom marshaled rather than a type itself. Because there’s no way to force a data type to always be custom marshaled when used, MarshalAsAttribute
must be applied to every parameter, return type, and field that desires custom marshaling. A side effect of this is that if you export a class called MyStream
derived from Stream
, there’s no way to get the exporter to transform it into a MyStream
coclass that implements IStream
. Instead, any uses of MyStream
as a parameter, return type, or field would need to be marked with the custom marshaler. The same applies to import as well: a coclass implementing IEnumVARIANT
is imported as a class implementing IEnumVARIANT
, even though any IEnumVARIANT
parameters, return types, or fields in a type library are changed to a custom-marshaled IEnumerator
.
You can usually work around these limitations by expanding the scope of the custom marshaler. For example, because you can’t directly custom marshal a .NET parameter type to a VARIANT
with type VT_RECORD
, you can instead custom marshal the entire interface containing the method with the parameter you need to custom marshal.
Custom marshaling is useful for working around limitations of the built-in COM Interoperability support. The .NET Framework ships with a CustomMarshalers
assembly containing a handful of custom marshalers used by the CLR. These custom marshalers marshal between ITypeInfo
and System.Type
, IEnumVARIANT
and IEnumerator
, IDispatchEx
and System.Runtime.InteropServices.Expando.IExpando
or System.Reflection.IReflect
(based on a cookie value), and finally IDispatch
and IEnumerable
. The ITypeInfo
/System.Type
custom marshaler marshals by value, but the others all marshal by reference, using the adapter object approach covered throughout this chapter.
All the adapter objects used by the .NET Framework’s custom marshalers implement ICustomAdapter
. For an example of how this can be useful, consider a COM enumerator object (implementing IEnumVARIANT
) that holds onto limited resources until it’s destroyed. Suppose you want to use this object in managed code (which, thanks to a custom marshaler, now appears to implement IEnumerator
), but you also want to release it as soon as you’re finished. For example, in C#:
Calling Marshal.ReleaseComObject
on the enumerator object returned by GetEnumerator
doesn’t work because it’s not the original COM object. It’s not even a COM object at all; it’s a .NET adapter object returned by the built-in IEnumVARIANT
/IEnumerator
custom marshaler! To release the original COM object, you must cast the object implementing IEnumerator
to ICustomAdapter
. Then you can call Marshal.ReleaseComObject
on the object returned by GetUnderlyingObject
:
Using C#’s foreach
statement or VB .NET’s For Each
statement should not be used with such COM enumerators, because the object implementing IEnumerator
is never directly exposed to you.
The IStream
/Stream
custom marshaler covered in this chapter fulfills a common need when using COM Interoperability, but there are numerous other custom marshalers you could create for common scenarios. For example, you could write a “visibility marshaler” that transforms COM-invisible .NET classes into visible COM classes. Of course, such an approach would have to be done on a type-by-type basis, because a generic custom marshaler can’t be written unless you expose an IDispatch
implementation that internally uses reflection on the wrapped .NET type. As another example, you could support marshaling a .NET array to a varying conformant array, using the cookie string to enable users to specify first_is
and last_is
information. Or, you could create a custom RCW that stores the most recently returned HRESULT
in thread local storage (TLS) and expose it to managed code as a LastHResult
property (similar to Marshal.GetLastWin32Error
). With such a property, success HRESULT
s could be obtained without resorting to PreserveSig
to change the look of the .NET signature.
Another useful custom marshaler would be one that transforms an ADO.NET DataSet
into an ADO Recordset
and vice-versa. In one direction, you can use System.Data.OleDb.OleDbDataAdapter.Fill
to convert a Recordset
to a DataSet
. In the other direction, you could use DataSet.GetXml
to obtain an XML representation of the DataSet
, use an XSLT transform to convert it into the XML format usable by ADO, then load it into an ADO Recordset
. The possibilities are endless!