The confusing UnmanagedType.LPStruct marshaling directive

MarshalAsAttribute controls marshaling behavior for managed data types that can have multiple unmanaged representations. Often, the challenge of using MarshalAsAttribute is choosing the right value from the UnmanagedType enumeration to pass to the attribute's constructor. Probably the UnmanagedType value that I've seen misused the most is LPStruct. And I don't blame people who misuse it! Its name is, at best, incredibly misleading. I'm sure we'd change the name if backwards compatibility wasn't so important.

So as a service to developers out there, I'd like to clarify the meaning of this marshaling directive, so you can either use it correctly or (preferably) know to stay away from it altogether.

UnmanagedType.LPStruct is only supported for one specific case: treating a System.Guid value type as an unmanaged GUID with an extra level of indirection. In other words, this directive makes the marshaler add a level of indirection to System.Guid when marshaling it from managed to unmanaged code, and remove a level of indirection from GUID when marshaling it from unmanaged to managed code.

You can see this behavior by exporting the following C# interface to a type library:

  public interface IUseGuids

  {

    void ByValGuid (System.Guid g);

    void ByRefGuid (ref System.Guid g);

    void ByValGuidWithLPStruct (

      [MarshalAs(UnmanagedType.LPStruct)] System.Guid g);

    void ByRefGuidWithLPStruct (

      [MarshalAs(UnmanagedType.LPStruct)] ref System.Guid g);

  }

The result is (as seen by OLEVIEW.EXE):

  interface IUseGuids : IDispatch

  {

    [id(0x60020000)]

    HRESULT ByValGuid([in] GUID g);

    [id(0x60020001)]

    HRESULT ByRefGuid([in, out] GUID* g);

    [id(0x60020002)]

    HRESULT ByValGuidWithLPStruct([in, out] GUID* g);

    [id(0x60020003)]

    HRESULT ByRefGuidWithLPStruct([in, out] GUID** g);

  };

Note that the type library exporter (TLBEXP.EXE) is a great tool for statically understanding how managed parameters/fields/return types get marshaled, since the signatures created by the exporter are required to match what the marshaler does at run-time. Even if you're wondering about the parameters of a PInvoke method, you can do this trick by temporarily pasting the method into a public interface (removing the "static", "extern", etc.) then running TLBEXP.EXE on your assembly.

When would you consider using UnmanagedType.LPStruct? When calling unmanaged APIs that expect an [in] GUID*, like CoCreateInstanceEx, defined on MSDN as:

  HRESULT CoCreateInstanceEx(

  REFCLSID rclsid,

  IUnknown* punkOuter,

  DWORD dwClsCtx,

  COSERVERINFO* pServerInfo,

  ULONG cmq,

  MULTI_QI* pResults

  );

with the first parameter described as follows:

  rclsid

    [in] CLSID of the object to be created.

Therefore, using UnmanagedType.LPStruct, you could define a C# PInvoke signature like this:

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

  static extern void CoCreateInstanceEx(

    [MarshalAs(UnmanagedType.LPStruct)] Guid rclsid,

   [MarshalAs(UnmanagedType.IUnknown)] object punkOuter,

    uint dwClsCtx,

   [In] ref COSERVERINFO pServerInfo,

    uint cmq,

    [In, Out] MULTI_QI [] pResults

  );

instead of:

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

  static extern void CoCreateInstanceEx(

  ref Guid rclsid,

  [MarshalAs(UnmanagedType.IUnknown)] object punkOuter,

    uint dwClsCtx,

    [In] ref COSERVERINFO pServerInfo,

    uint cmq,

    [In, Out] MULTI_QI [] pResults

  );

The former can be called like:

  Guid clsid = ...;

  ...

  CoCreateInstanceEx(clsid, null, CLSCTX_REMOTE_SERVER, ref serverInfo, 1, results);

Which, to some people, is a little less clumsy than how you would have to call the latter method:

  Guid clsid = ...;

  ...

  CoCreateInstanceEx(ref clsid, null, CLSCTX_REMOTE_SERVER, ref serverInfo, 1, results);

especially because passing the Guid parameter by-reference could lead the caller to believe that the Guid could be changed, when it really won't be.

Of course, this LPStruct trick only works if the GUID* parameter is in-only because any change to the GUID in unmanaged code would not be reflected back in the by-value System.Guid value type passed in. But it is a fairly common pattern for unmanaged APIs to use [in] GUID* as a parameter. Even though such APIs don't change the GUID, it's more efficient for the caller to pass a pointer rather than the whole structure on the stack.

So what's the moral of the story (which was much longer than I intended it to be)? You should probably just stay away from UnmanagedType.LPStruct. The confusion it can cause is not worth the marginal benefits it provides!