UnmanagedType.Struct and VARIANT marshaling

Continuing yesterday's theme of marshaling directives, today I'll talk about UnmanagedType.Struct, another poorly-named (and therefore confusing) enumeration value. If you pretend that it's called "UnmanagedType.Variant," it should make more sense.

This marshaling directive is only valid for parameters/fields/return types defined as System.Object (not types derived from System.Object), and it instructs the marshaler to marshal the instance to unmanaged code as a VARIANT.

For both COM Interop and PInvoke scenarios, Object parameters and return types are already marshaled as VARIANTs by default. Therefore, using UnmanagedType.Struct is only necessary for structures with System.Object fields that you need to marshal as VARIANT fields. Without this directive, Object fields are marshaled as IUnknown pointers. Since structures with VARIANT fields are pretty rare (at least in Microsoft APIs), this marshaling directive is rarely needed.

How does Object/VARIANT marshaling work? Although this marshaling only applies when the parameter/field/return value is defined as System.Object in metadata (ignoring late-bound scenarios), the instance of the Object passed can, of course, be any derived type. When marshaling from managed code to unmanaged code, the Interop marshaler figures out what type of VARIANT to create based on the instance's type. If you're passing an instance of a System.String, it creates a VT_BSTR VARIANT. If you're passing an instance of a System.Boolean, it creates a VT_BOOL VARIANT. When marshaling from unmanaged code to managed code, the marshaler does the reverse. Some of the conversions are not as straightforward, but I'll leave that discussion for another time.

Here's a short C# PInvoke example that demonstrates Object/VARIANT marshaling in both directions by calling the VariantChangeType API:

HRESULT VariantChangeType(

  VARIANTARG* pvargDest,

  VARIANTARG* pvarSrc,

  unsigned short wFlags,

  VARTYPE vt

);

(VARIANTARG is just a typedef for VARIANT.)

In the code below, the source Object (whose type is System.Boolean) gets marshaled to VariantChangeType as a VT_BOOL VARIANT. The API sets the pvargDest VARIANT pointer to a VT_BSTR VARIANT, which gets marshaled back to managed code as the destination Object whose type is System.String. I decided not to mark the System.Object parameters with [MarshalAs(UnmanagedType.Struct)] simply because I prefer to only use MarshalAsAttribute when I'm overriding default marshaling behavior.

using System;

using System.Runtime.InteropServices;

public class VariantMarshaling

{

  [DllImport("oleaut32.dll", PreserveSig=false)]

  static extern void VariantChangeType(

    ref object pvargDest,

    [In] ref object pvarSrc,

    ushort wFlags,

    VarEnum vt

  );

  // From OLEAUTO.H

  const ushort VARIANT_ALPHABOOL = 2;

  public static void Main ()

  {

    object destination = null;

    object source = true;

    VariantChangeType(ref destination, ref source,

     VARIANT_ALPHABOOL, VarEnum.VT_BSTR);

    // Prints "True"

    Console.WriteLine(destination);

    // Prints "System.String"

    Console.WriteLine(destination.GetType());

  }

}

Recall that defining variables, parameters, etc. as object in C# is equivalent to defining them as System.Object. I won't get into the ongoing debate about which form is better, but I tend to use the language-specific identifiers in my code examples.