.NET interop marshaling made easierThis 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 problemDllImport 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 :
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 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 Longand 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 solutionAgainst 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
If you browse the functions, you'll see that you can come up with simple translation rules :
3. Addendum : simple rules3.1 Using a non-const LPSTR in a function callHere 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 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 structsHere 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 structsThe .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"] The call is NONCLIENTMETRICS ncm = new NONCLIENTMETRICS(); 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 ( 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 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 |