.NET interop marshaling made easier

This article shows a determinist technique aimed to solve the .NET DllImport native function call issue.

The remainder of the article is intended to be readable and workable for all .NET developers (C#, VB.NET, ...), even though for lazy reasons I am mostly going to focus on getting C# samples to work.

 

1. The problem

DllImport is sometimes referred to as Platform Invoke[^] (P/Invoke) in the documentation or interop marshaling[^], and is essentially a way to let the CLR know about the signature of the function you intend to call from a native DLL.

For obvious reasons, most native calls are geared toward the WIN32 API, to fill the gap with missing features we are used to. That's something we address here.

But there is a problem. You know the low-level signature of the function, but what the .NET compiler (C#, VB.NET, ...) expects from you is the managed parameter type declaration, which is different and non-obvious. Unfortunately, the VS.NET environment is of no help at this point.

At compilation-time, you could even pass whatever type you might think of, it would be ok for the .NET compiler since it has no knowledge of the actual underlying function signature. Of course, at run-time, it's going to throw an exception. Or worse, do absolutely nothing, as if the function call was bypassed (in fact, most of the time that's a silently catched exception case).

In short, there are 2 issues :

  • be able to find appropriate managed type declarations
  • be able to call it

Facing those 2 issues, we are left all alone, with no debugger or helper.

Well, almost....

In fact, (and I believe a lot of anti-VB people are going to resent me), VB comes to the rescue ! As part of the engine, VB does automatic type and value mapping when assigning variables and passing params to methods. For instance, if we want to use the WIN32 ::SendMessage() function from VB code, we declare it like this :

VB declaration

Declare Function SendMessage Lib "user32" Alias "SendMessageA" (
                          ByVal hwnd As Long, 
                          ByVal wMsg As Long, 
                          ByVal wParam As Long, 
                          lParam As Any )  As Long
and we can use it then. In other words, the Long and Any type declarations are automatically mapped back and forth at run-time to match the actual WIN32 ::SendMessage() function here :
WIN32 API declaration

LRESULT SendMessage(
  HWND hWnd,      // handle to destination window
  UINT Msg,       // message
  WPARAM wParam,  // first message parameter
  LPARAM lParam   // second message parameter
);

The VB declaration hides the pointers and other C/C++ types, and makes it look coherent wrt the VB language. And that's exactly the same idea behind the .NET managed types. Thanks to the VB declaration, it is straight forward to come up with a .NET-compatible DllImport declaration. Here it is :

C# declaration

[DllImport("user32.dll", CharSet=CharSet.Auto)]
public static extern int SendMessage(int hWnd, int msg, int wParam, IntPtr lParam);

which in turn gives us the ability to use it without worries :
C# sample code

public const int WM_SYSCOMMAND = 0x0112;
public const int SC_CLOSE = 0xF060;

...
int hNext = FindWindowEx(hParent,hNext,sClassNameFilter,IntPtr.Zero);

SendMessage(hNext, WM_SYSCOMMAND, SC_CLOSE, IntPtr.Zero);

 

What would be fine is that, whenever we have to make a call to the WIN32 API, we can easily get our hands on the intermediate VB "wrapper", so as part of the process of declaring compatible managed types in DllImport...

 

2. The solution

Against all odds, the entire WIN32 API has been wrapped in a VB-style for us. Can't wait to know where to find this file ? Give a look at this :

VB WIN32 wrapper : <vc7installdir>\Common7\Tools\Bin\Win32API.Txt

There you'll find all WIN32 functions including SendMessage, constants, and everything else you'll need for WIN32 API calls.

Most function parameters are passed ByVal, and this modifier is not needed with .NET code since that's the default behaviour.

ByRef should most of the time be replaced by either the ref or out modifier. See next section for further details.

If you browse the functions, you'll see that you can come up with simple translation rules :

WIN32 VB C#
int, long, byte Long int
LPSTR, LPTSTR, LPCTSTR String String
void*, xxx* Any IntPtr

 

3. Addendum : simple rules

3.1 Using a non-const LPSTR in a function call

Here is a useful example : WIN32 ::GetWindowText(), used whenever you need to get the title of a UI control. In WIN32, the declaration is :

WIN32 API declaration

int GetWindowText(
  HWND hWnd,        // handle to window or control
  LPTSTR lpString,  // text buffer
  int nMaxCount     // maximum number of characters to copy
);

The Win32API.Txt VB helper tells us that, without pointers it could be seen like this :

Declare Function GetWindowText Lib "user32" Alias "GetWindowTextA" (
                                    ByVal hwnd As Long, 
                                    ByVal lpString As String, 
                                    ByVal cch As Long) As Long

The mapping to C# is straight forward, but there is something special, we do take into account the fact that the lpString and cch are dependent on each other. We are going to use the .NET StringBuilder class :

C# declaration
using System.Text; // System.Text.StringBuilder class

[DllImport("user32.dll", CharSet=CharSet.Auto)]
public static extern int GetWindowText(int hWnd, StringBuilder lpString, int cch);

C# sample code

StringBuilder sb = new StringBuilder(260); // create a fix-size string
GetWindowText(hWnd, sb, 260); // WIN32 native call
String s = sb.ToString(); // Get the return string

 

 

3.2 Interoperating fix-size strings in structs

Here is a sample WIN32 structure :

WIN32 API declaration

#define LF_FACESIZE 32

typedef struct tagLOGFONT { 
  LONG lfHeight; 
  LONG lfWidth; 
  LONG lfEscapement; 
  LONG lfOrientation; 
  LONG lfWeight; 
  BYTE lfItalic; 
  BYTE lfUnderline; 
  BYTE lfStrikeOut; 
  BYTE lfCharSet; 
  BYTE lfOutPrecision; 
  BYTE lfClipPrecision; 
  BYTE lfQuality; 
  BYTE lfPitchAndFamily; 
  TCHAR lfFaceName[LF_FACESIZE]; // fix-size string
} LOGFONT, *PLOGFONT;

The VB wrapper tells us to map it like this :

VB declaration

Type LOGFONT
  lfHeight As Long
  lfWidth As Long
  lfEscapement As Long
  lfOrientation As Long
  lfWeight As Long
  lfItalic As Byte
  lfUnderline As Byte
  lfStrikeOut As Byte
  lfCharSet As Byte
  lfOutPrecision As Byte
  lfClipPrecision As Byte
  lfQuality As Byte
  lfPitchAndFamily As Byte
  lfFaceName(1 To LF_FACESIZE) As Byte // array of bytes
End Type

This doesn't really show us the way to map the fix-size string, unfortunately. Here is how to do it : we can help the marshaller using an attribute and tell it that's a fix-string (in fact, that's an array of bytes) :

C# declaration

[StructLayout(LayoutKind.Sequential)]
public class LOGFONT
{
  public int lfHeight;
  public int lfWidth;
  public int lfEscapement;
  public int lfOrientation;
  public int lfWeight;
  public byte lfItalic;
  public byte lfUnderline;
  public byte lfStrikeOut;
  public byte lfCharSet;
  public byte lfOutPrecision;
  public byte lfClipPrecision;
  public byte lfQuality;
  public byte lfPitchAndFamily;
  [MarshalAs(UnmanagedType.ByValTStr, SizeConst=32)] public string lfFaceName;
}

For a full list of marshal attributes, click the MSDN link here.

 

3.3 Passing structs

The .NET 1.0 run-time has an internal limitation with the marshaling (automatic type mapping) of complex types. I have already discussed one earlier with the MS Internet Explorer OnBeforeNavigate2 fix[^], and I would like to give an example of trap you may fall into if you don't pay attention to what's going on. This example is from a recent post[^] in the CodeProject C# forum :


[DllImport("User32.dll"]
public static extern bool SystemParametersInfo(int Action,
        int Param,
        NONCLIENTMETRICS Param,
        int WinIni);


The call is

NONCLIENTMETRICS ncm = new NONCLIENTMETRICS();
bool b = SystemParametersInfo(SPI_GETNONCLIENTMETRICS, ncm.cbSize, ncm, 0);



And now I'm getting confused:

b is true, that means the call succeeded, but ncm has not been touched. Is there anything wrong with the structure layout?? At the end I tried all combinations, instead of a class I declared NCM as a structure and used ref, I tried the IntPtr version... nothing didn't help. And I know that it works since MS does similar to build the SystemInformation members (Menufont is contained in NCM, too).


Somewhere I'm blocked. Can anybody help??
Thanks and bye
Matze

Matze is having troubles passing a struct (ncm) as a parameter. And it's a bit weird since there is no exception raised at all, so it's impossible to figure out what's wrong and Matze could go round and round for years with that bloody line of code. Let's solve the issue.

First of all, let's check out the VB wrapper for the SystemParametersInfo WIN32 function :

VB wrapper

Declare Function SystemParametersInfo Lib "user32" Alias "SystemParametersInfoA" (
                                ByVal uAction As Long, 
                                ByVal uParam As Long, 
                                ByRef lpvParam As Any, 
                                ByVal fuWinIni As Long) As Long

What we see is that the 3rd parameter is known as Any, which is a translation of the fact that a void* pointer is expected at the WIN32 API level. The fact is, the built-in marshaller of .NET 1.0 has troubles marshaling LPStruct, the internal type for a pointer to a struct.

What we are going to do then is help the marshaller by marshaling the structure ourselves, allocating a memory block, copying the structure content, get a pointer from it (using the .NET IntPtr type), call the function, and then copy the filled structure to our initial structure, and finally free the memory block. Code for this is as follows :

C# declaration

[DllImport("User32.dll")]
public static extern bool SystemParametersInfo(int Action,
                                               int Param,
                                               IntPtr lpParam,      
                                               int WinIni);

C# usage

using System.Runtime.InteropServices;
...

const int SPI_GETNONCLIENTMETRICS = 41; // const value from Win32API.Text

// instantiate our struct
NONCLIENTMETRICS ncm = new NONCLIENTMETRICS();

// get its size (and initialize the cbSize field as requested by the MSDN WIN32 doc)
int size = ncm.cbSize = Marshal.SizeOf(typeof(NONCLIENTMETRICS));

// allocate a temporary memory block, and get the pointer (unsafe)
IntPtr pncmetrics = Marshal.AllocHGlobal(size);

// copy the initial structure
Marshal.StructureToPtr(ncm, pncmetrics, true);

// actual native call
bool b = SystemParametersInfo(SPI_GETNONCLIENTMETRICS, size, pncmetrics, 0);

// copy the return structure
Marshal.PtrToStructure(pncmetrics,ncm); 

// clean up
Marshal.FreeHGlobal(pncmetrics);

 

This article has presented a determinist technique in order to solve the .NET marshaling troubles. Hope it helps you as much as it did me so far.

In addition to figuring out managed types, I have clearly said that the current .NET 1.0 marshaller has limitations. It's unclear at the moment whether the .NET 1.1 will be entirely fixed regarding these issues, or only partially fixed. I will update this article accordingly to the release of newer major .NET run-time builds.

Of course, this should be regarded as a temporary way of doing things manually, until someone comes up with a full-fledge Platform Invoke debugger. I have been looking the shared source .NET implementation (sscli), and so far I haven't seen any way of either hooking Invoke calls or tracing the stuff out. May be you guys out there can come up with a solution and help the entire .NET dev community.

 

Stéphane Rodriguez - Nov 26, 2002.

 


Home
Blog