devxlogo

Dealing with DllImport, (Part 2 of 2)

Dealing with DllImport, (Part 2 of 2)

In the first part of this article, I explained the basics of calling functions in unmanaged DLLs from managed .NET code. In this part, I’ll show you how to use the MarshalAs and StructLayout options to control precisely how the runtime passes data to and from unmanaged code.

For example, the main Win32 API used to retrieve operating system version information, GetVersionEx, is exported from Kernel32.dll, and requires a pointer to an OSVERSIONINFO structure. The OSEVERSIONINFO structure includes an embedded, fixed-length character string, szCSDVersion. The first member of the structure contains the size of the structure, which must be set on input. On output, the function fills all the members of the structure within the declared size.

   //GetVersionEx exported from Kernel32.dll.    // BOOL GetVersionEx(LPOSVERSIONINFO lpVersionInfo);      //The original structure passed to the function       //contains the following elements:   //typedef struct _OSVERSIONINFO   //        {    //           DWORD dwOSVersionInfoSize;    //           DWORD dwMajorVersion;    //           DWORD dwMinorVersion;    //           DWORD dwBuildNumber;    //           DWORD dwPlatformId;    //           TCHAR szCSDVersion[ 128 ];    //        } OSVERSIONINFO;   //

Using the StructLayoutKind Attribute
You can describe this structure as a class using the StructLayout attribute. The LayoutKind.Sequential value in the StructLayout constructor means that the object members are laid out sequentially in the order in which they appear when the framework marshals the class instance to unmanaged memory. The members themselves are laid out according to the packing size specified in StructLayoutAttribute.Pack field. The default packing size is 8, which will work just fine for this structure.

   using System;   using System.Runtime.InteropServices;      namespace OSInfo   {      class Class1      {         [ StructLayout( LayoutKind.Sequential )]            public class OSVersionInfo          {            public int dwOSVersionInfoSize;            public int dwMajorVersion;            public int dwMinorVersion;            public int dwBuildNumber;            public int dwPlatformId;            [ MarshalAs( UnmanagedType.ByValTStr,                SizeConst=128 )]                public String szCSDVersion;         }         ...

Each unmanaged DWORD corresponds to a managed int. The embedded fixed-length string TCHAR szCSDVersion[128] must be marshaled by value. The MarshalAs attribute can set the type of string marshaling to use as well as the length of the string. The most appropriate unmanaged type for this case is ByValTStr, which was designed specifically to be used for in-line fixed length character arrays that appear within a structure.

You don’t have to use LayoutKind.Sequential. Another possible LayoutKind arrangement is [StructLayout(LayoutKind.Explicit)], in which you can explicitly control the precise position of each member of an object in unmanaged memory by specifying the offset of that member from the start of the object’s memory.

You don’t have to use LayoutKind.Sequential. Another possible LayoutKind arrangement is [StructLayout(LayoutKind.Explicit)], in which you can explicitly control the precise position of each member of an object in unmanaged memory by specifying the offset of that member from the start of the object’s memory. In an explicit layout, you must add a FieldOffset attribute to each member to indicate the position of that field within the type. In this example, you don’t need explicit field offset control for this structure, although you could use it, particularly if you wanted to skip fields or describe an irregularly packed structure. For the OSVERSIONINFO structure a LayoutKind.Explicit arrangement would look like this:

   [StructLayout( LayoutKind.Explicit )]    public class OSVersionInfo2     {      [FieldOffset(0)] public int dwOSVersionInfoSize;      [FieldOffset(4)]public int dwMajorVersion;      [FieldOffset(8)]public int dwMinorVersion;      [FieldOffset(12)]public int dwBuildNumber;      [FieldOffset(16)]public int dwPlatformId;      [MarshalAs(UnmanagedType.ByValTStr,         SizeConst=128)]          [FieldOffset(20)]public String szCSDVersion;    }

To declare the GetVersionEx function, you can pretty much follow the same DllImport pattern as you saw in first part of this article. One new twist is that the code applies both In and Out direction attributes to the OSVersionInfo parameter, so that the managed caller can see the changes the unmanaged code makes to the structure. A ref attribute would also have worked.

      [ DllImport( "kernel32" )]      public static extern bool GetVersionEx(          [In, Out] OSVersionInfo osvi ); 

You can now instantiate the OSVersionInfo class just like any other class. The Marshal.SizeOf() method determines the size of the unmanaged structure, in bytes, which is what the API function need to know.

         [STAThread]         static void Main(string[] args)         {            OSVersionInfo osvi = new OSVersionInfo();            osvi.dwOSVersionInfoSize =             Marshal.SizeOf( osvi );               GetVersionEx( osvi ); 

Printing out the results of the call is now a trivial exercise:

               Console.WriteLine( "GetVersionEx results");            Console.WriteLine( "Class size:    {0}",                osvi.dwOSVersionInfoSize );            Console.WriteLine( "Major Version: {0}",                osvi.dwMajorVersion );            Console.WriteLine( "Minor Version: {0}",                osvi.dwMinorVersion );            Console.WriteLine( "Build Number:  {0}",                osvi.dwBuildNumber );            Console.WriteLine( "Platform Id:   {0}",                osvi.dwPlatformId );            Console.WriteLine( "CSD Version:   {0}",                osvi.szCSDVersion ); 

For most purposes, you won’t need to go through all of that to find out what OS platform and version is running: the Environment.OSVersion class contains the Platform Id and the version/build string, although not the CSD version string (which indicates the service pack level).

               Console.WriteLine(                "
Environment.OSVersion results");            Console.WriteLine( "Platform:      {0}",                Environment.OSVersion.Platform );            Console.WriteLine( "Version:       {0}",                Environment.OSVersion.Version );         }      }   } 

On my Windows 2000 machine, this code produces the following output:

   GetVersionEx results   Class size:    148   Major Version: 5   Minor Version: 0   Build Number:  2195   Platform Id:   2   CSD Version:   Service Pack 2      Environment.OSVersion results   Platform:      Win32NT   Version:       5.0.2195.0 

For a good idea of how to accomplish the same thing in VB.NET, you should consult the .NET OSInfo sample, which was my starting point for the fleshed-out sample above.

The Win32 API makes heavy use of callback functions when enumerating Windows or registry items for example. That raises the question of how you can use callbacks from native code into managed code without function pointers? Fortunately, that has not been overlooked.

In the .NET Framework, delegates serve the role of function pointers, but in a type-safe way. The Platform Invoke machinery knows how to convert managed delegates to function pointers that can be used by unmanaged code. In addition, the machinery knows that it should pin managed buffers (keep them fixed in a particular memory location) for the duration of a call to unmanaged code.

Consider the EnumWindows Win32 API function:

   BOOL EnumWindows(WNDENUMPROC lpEnumFunc,       LPARAM lParam) 

In that function, lpEnumFunc is a pointer for the callback function, which wants two arguments, a Window handle and a long parameter (lParam), both of which correspond to managed integers. To create a delegate, you need to match the callback parameter types. A delegate for a callback function with this signature would look like this:

   delegate bool CallBack(int hwnd, int lParam); 

After defining the delegate, you can use it in the definition of a DLLImport prototype for EnumWindows:

   DllImport("user32")]   static extern int EnumWindows(CallBack x, int y);  

At runtime, you need to create an instance of the delegate before passing it to the wrapped Win32 API:

   CallBack myCallBack = new CallBack(ListTitle);   EnumWindows(myCallBack, 0); 

Now you just have to implement the managed callback function, called ListTitle in this case. Its job is to get and print out the caption of each window enumerated, which gives you a rather raw indication of the currently running applications. The callback function receives the handle of each enumerated window and can use that to find the window caption using another Win32 API, GetWindowText. If there’s a managed equivalent of GetWindowText, I don’t know about it, but I do know how to write its DllImport wrapper. Its unmanaged signature is:

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

So you can write an import wrapper for it as

   [DllImport("user32")]   static extern int GetWindowText(     int hWnd, //handle to window or control     StringBuilder lpString,  //text buffer     int nMaxCount //maximum number of characters to copy     ); 

With that in place, the callback function implementation is trivial:

   static bool ListTitle(int hWnd, int lParam)    {       StringBuilder buf=new StringBuilder(100);      if ( GetWindowText(hWnd, buf, 100)==0 )         return true;      Console.WriteLine(buf);      return true;   } 

Altogether, the code looks like this:

   using System;   using System.Runtime.InteropServices;   using System.Text;      namespace RunningApps   {      ///       /// Obtain and display running applications by         /// enumerating windows      ///       class Class1      {      public delegate bool CallBack(int hwnd,          int lParam);            [DllImport("user32")]         public static extern int EnumWindows(            CallBack x, int y);             [DllImport("user32")]         public static extern int GetWindowText(            int hWnd, // handle to window or control            StringBuilder lpString,  // text buffer            int nMaxCount  // max characters to copy         );               ///          /// The main entry point for the application.         ///          [STAThread]         static void Main(string[] args)         {            CallBack myCallBack = new CallBack(ListTitle);            EnumWindows(myCallBack, 0);         }            public static bool ListTitle(            int hWnd, int lParam)          {             StringBuilder buf=new StringBuilder(100);            if ( GetWindowText(hWnd, buf, 100)==0 )               return true;            Console.WriteLine(buf);               return true;         }      }   } 

When you run this code, you’ll see a list all the current windows in the system?both visible and invisible. You may be surprised at what shows up, especially if you’re running a complicated application such as Visual Studio .NET at the time!

While many other subtleties can crop up when you try to import unmanaged code, you’ve covered the most common issues. You know enough now to write you own DLLImport statements, and most importantly, you know that you should look for equivalent classes in the .NET Framework before you go to the trouble of importing a Win32 API function.

devxblackblue

About Our Editorial Process

At DevX, we’re dedicated to tech entrepreneurship. Our team closely follows industry shifts, new products, AI breakthroughs, technology trends, and funding announcements. Articles undergo thorough editing to ensure accuracy and clarity, reflecting DevX’s style and supporting entrepreneurs in the tech sphere.

See our full editorial policy.

About Our Journalist