Share via


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!

Comments

  • Anonymous
    April 23, 2003
    I want to add one comment about the trick of pasting a PInvoke signature into an interface and exporting it. This can be misleading, because the marshaler has different default behavior for COM interface methods vs. PInvoke signatures. For example, a String is marshaled as a BSTR by default on a COM interface, but it's marshaled as an LPSTR by default for a PInvoke signature. (The defaults are geared toward common usage in COM vs. the plain Win32 world.) So do this with caution!
  • Anonymous
    April 23, 2003
    Welcome to blogging Adam. You seem to have a problem with your RSS feed and subscribing to it:The resource cannot be found. Description: HTTP 404. The resource you are looking for (or one of its dependencies) could have been removed, had its name changed, or is temporarily unavailable. Please review the following URL and make sure that it is spelled correctly.Requested Url: /USERNAME/blogxbrowsing.asmx/GetRss
  • Anonymous
    April 23, 2003
    Welcome to blogging! Nice to have you here. The link to your RSS feed is wrong: you should replace "USERNAME" with "anathan" and it will work for all subscribers (not only for me ;-).
  • Anonymous
    April 23, 2003
    Adam Nathan said in a comment "I want to add one comment about the trick of pasting a PInvoke signature into an interface and exporting it. This can be misleading, because the marshaler has different default behavior for COM interface methods vs. PInvoke signatures".Do you mean that building a WIN32 API type-library is utterly useless ? I am dreaming of having the entire WIN32 API (prototypes, structures, enums, constants) under Intellisense, and have actually started it. But this late comment of you makes me think may be in the end what I'll get is barely useable.
  • Anonymous
    April 24, 2003
    I've fixed the link to my RSS feed. Thanks for noticing, everyone!As for the WIN32 API type library, there are different roadblocks, depending on what you have in mind. Are you talking about starting with a type library and importing it into managed code or the reverse? (Or simply manually creating an assembly filled with managed definitions of the WIN32 API by compiling source code in your favorite .NET language?)For import, the differences in default marshaling don't matter because the type library importer (TLBIMP.EXE) marks all parameters/fields/etc. with the appropriate marshaling directives so the metadata matches the unmanaged type information. For export, you can completely control the marshaling behavior with MarshalAsAttribute and therefore override the marshaling defaults.Assuming you're talking about the import case, here are some roadblocks:1) The type library importer doesn't import modules. Yet if you were creating a "WIN32 type library" you'd want to place the APIs inside modules (as opposed to inside COM interfaces). Although if you're familiar with the type library APIs (LoadTypeLibEx, ITypeLib, ITypeInfo, etc.) and with reflection emit or the unmanaged CLR metadata APIs, you could imagine writing your own type library importer that pays attention to type library modules.2) Defining a managed signature for a WIN32 API often requires knowledge about the API's semantics - who is supposed to allocate memory, free memory, etc. The only way for an automated tool to get a signature correct is to "dumb down" such parameters to IntPtr or other constructs that put the burden of marshaling on the caller rather than the Interop marshaler.Of course, nothing prevents you from writing an assembly filled with PInvoke signatures for every WIN32 API. It's just a lot of hard work. In my book (which I linked to my name below), I have an appendix that provides C# PInvoke signatures for all exports from GDI32.DLL, KERNEL32.DLL, OLE32.DLL, SHELL32.DLL, and USER32.DLL. Although I didn't have the space (or the time!) to define the corresponding structures, delegates, etc.
  • Anonymous
    April 24, 2003
    "Are you talking about starting with a type library and importing it into managed code or the reverse?"I am talking about importing the type-library. It's true that modules won't get imported, but my point was to put the API prototypes within faked interfaces, import everything, and finish the job by manually editing the imported assembly, to remove the interfaces.That said, it's possible I am quicker off taking the current sscli tlbimp implementation and upgrade it to support the module tags.In addition, I am still trying to figure out the best option to "smarter up" the Win32 prototypes so that users would find it more intuitive to use from the managed point of view. For instance, anytime a LPTSTR is an input parameter rather than output, while the prototype provides no way to decide whether the parameter should actually be ref-ed, out-ed or not.Of course, a step forward could simply consist in manually adding [in] or [out] attributes, so to show the real meaning of a LPTSTR in a given prototype.All in all, while a couple of things should be manually lifted up, the interop process would be mostly automated. This would provide a relevant technique for the Win32 API, and for any other API of interest.Taking it the other way means it is entirely manual, which is the worst case I can think of. I agree this leads to something that works and which can be shared, though.
  • Anonymous
    May 03, 2004
    Hey. I was just going to use that, but decided to do a google first only to find your blog just before wasting a lot of time. Though i already did waste 6 hours, but guess I could account that for "learning" interop..
  • Anonymous
    June 04, 2004
    good!
  • Anonymous
    August 11, 2006
    funny ringtones
  • Anonymous
    March 14, 2009
    原始文献地址:http://blogs.msdn.com/adam_nathan/archive/2003/04/23/56635.aspx