Dealing with DllImport (Part 1 of 2)

Learning to program for the .NET Framework has been an interesting experience. I use the term “interesting” loosely: it has sometimes been amazingly easy, and sometimes unexpectedly frustrating.

For instance, I was writing a simple chat application with Windows Forms in C# and thought it would be good to grab the users’ workstation and login names to identify each participant, rather than generate yet another CB-type handle like the instant messenger programs do. “Simple,” I thought, “I’ll just call GetUserName and GetComputerName.” But when I searched the .NET Framework documentation for those Win32 API functions, I couldn’t find them.

After a little bit of browsing, I gave up. “Well,” I thought, “I can always use P/Invoke,” meaning the Platform Invocation facility in .NET used to call DLL functions. “That shouldn’t take more than a couple of minutes to set up.”

I looked up the first Win32 API function:

    BOOL GetUserName(     LPTSTR lpBuffer,  // name buffer     LPDWORD nSize     // size of name buffer   );

The GetUserName function is implemented in advapi32.dll, so I added the following code to my C# program:

    using System.Runtime.InteropServices;    //...   [DllImport("advapi32.dll")]   public static extern bool GetUserName( _      Byte[] lpBuffer, int nSize);   //...   Byte[] b=new Byte[100];   int n=100;   bool rc=GetUserName(b, n); //kaboom!

Here System.Runtime.InteropServices is the namespace that supports DllImport, and DllImport is an attribute that means you’re importing an unmanaged function from a DLL.

When I ran that code, I discovered that it threw a System.NullReferenceException when it tried to call GetUserName.I tried a dozen different things to get this to work, wasting most of a day, and finally asked an expert for help. Before looking at the answer, you might want to see if you can spot my mistake.

The expert told me how to write the DLLImport correctly (well, almost correctly) off the top of his head, and then asked, by the way, was there any special reason why I wasn’t using the System.Environment.Username property. Well, duh! I’d completely overlooked the System.Environment class in the .NET Framework, which provides all the information I happened to want for that part of my program.

Even though I wound up using the System.Environment class in my program, it’s useful to look at the full, correct P/Invoke call to GetUserName:

   using System;   using System.Runtime.InteropServices;   using System.Text;      namespace TestGetUserName   {      class Class1      {   [DllImport("advapi32.dll")]   public static extern bool GetUserName      (StringBuilder lpBuffer,    ref int nSize);      [STAThread]   static void Main(string[] args)   {        StringBuilder b=new StringBuilder(100);        int n=b.Capacity;        bool rc=GetUserName(b, ref n);        Console.WriteLine(b);    }       }   } 

What’s changed? Two things: one that matters, and one that doesn’t matter quite so much. The one that matters is that the second argument to GetUserName needs to be a reference in C#, since the underlying API calls for an LPDWORD, in other words for a pointer to a DWORD.

What was the other change? The best C# type to use for the LPTSTR lpBuffer argument of the underlying API call is a StringBuilder, not a Byte[]. The StringBuilder returned is directly usable as a string; the Byte[] would have to be explicitly converted.

If I’d had more experience with DllImport, I might have realized that the System.NullReferenceException I got originally was telling me that it needed a reference type where I was passing a value type. Of course, the bug wasn’t where I was looking, because I was looking at the wrong place.

There’s a rule or three you can take away from this example. The first rule is that a Win32 pointer type translates to a corresponding .NET reference type. The second rule is that a Win32 string pointer generally translates to a .NET StringBuilder type (or, sometimes, to a String type). The third rule is to look for a pointer/value mismatch if you get a System.NullReferenceException from a P/Invoke call.

So how would you do the same thing in Visual Basic .NET? The syntax is a little different, but the general idea is the same:

The old VB 6 Declare facility still exists, but it now maps to the new DllImport attribute.
    Imports System.Runtime.InteropServices   Imports System.Text      Module Module1       _      Public Function GetUserName( _         ByVal lpBuffer As StringBuilder, _         ByRef nSize As Integer) As Boolean      End Function         Sub Main()         Dim b As StringBuilder = New StringBuilder(100)         Dim n As Integer = b.Capacity         Dim rc As Boolean = GetUserName(b, n)         Console.WriteLine(b)      End Sub   End Module 

As you can see, the square brackets for the DLLImport attribute in C# have become angle brackets in Visual Basic .NET, and the static extern declaration of the function in C# has gone away. Of course, the ref modifier in C# becomes ByRef in Visual Basic .NET, and bool becomes Boolean. The semicolons between statements are gone in Visual Basic .NET, at the expense of needing underscores for line continuations, and the curly brackets to delimit blocks have been replaced by explicit End statements.

The old VB 6 Declare facility still exists, but it now maps to the new DllImport attribute. For example, you could declare the ANSI form of GetUserName as:

    Declare Ansi Function GetUserNameA Lib _      "advapi32.dll" (ByVal lpBuffer As _      StringBuilder, ByRef nSize As Integer) _      As Boolean 

The existence of the Declare statement isn’t as useful as you might think. The parameter passing mechanism has changed from VB 6 to Visual Basic .NET: parameters formerly defaulted to ByRef, but they now default to ByVal. Also, some of the types have changed. So, you can’t blindly use the VB 6 API viewer. For example, the declaration you’ll find for GetUserName in the VB 6 API viewer is:

    Declare Function GetUserName _      Lib "advapi32.dll" Alias "GetUserNameA" _      (ByVal lpBuffer As String, nSize As Long) As Long 

That won’t work, principally for the same reasons that my original effort in C# didn’t work: the first argument must be a StringBuilder, and the second argument needs to be changed to a reference.

How P/Invoke Works
P/Invoke, as you’ve already seen, relies on the DllImport attribute from the System.Runtime.InteropServices namespace. The DllImport attribute specifies that the target method is an export from an unmanaged shared library such as the Win32 API. In the simple case you’ve seen here, all you need to do is declare the name of the DLL from which you’re importing the call and the parameters of the call itself.

The compiler emits the DllImport information directly into the MSIL (Microsoft Intermediate Language) executable file format. The runtime recognizes this information and uses the Marshal class of the System. Runtime.InteropServices namespace to get the parameter and return data across the boundary between managed and unmanaged code. The runtime is actually quite clever about doing the right thing when marshaling the data back and forth between managed and unmanaged types. It copies data when it needs to, pins managed data reference types in memory so that they can be used by unmanaged code, and when needed, converts strings from ANSI to Unicode or vice-versa.

Sometimes, merely declaring a DLL name and function prototype does not suffice to describe the necessary behavior for marshalling. For such cases, the DllImport attribute has additional fields to specify the imported function’s calling convention and character set, among others. You can also use the MarshalAs attribute to control how specific parameters are passed, and the StructLayout attribute to define structures for marshaling. I’ll discuss those in Part 2 of this article series.

What happens, though, if the API you’re calling does something really weird?

For example, take the Windows GetPrivateProfileSectionNames API. This function exists in the Win32 API purely for compatibility with 16-bit Windows, so it wasn’t a top compatibility priority for the .NET Framework designers. The weird thing about GetPrivateProfileSectionNames is that it returns a flattened array of strings in a single buffer. Each string is terminated by a null character, and the final string is terminated by two nulls in a row.

If you try to import that into .NET using a StringBuffer, you’ll get the first string only. If you try to muck with the marshalling, all you’re likely to do is to wind up with a corrupted form of the first string.

The weird thing about GetPrivateProfileSectionNames is that it returns a flattened array of strings in a single buffer.

The solution to this dilemma (and a big tip o’ the hat to Russell Jones for figuring this out) is to return the strings into a byte array, call GetString from the System.Text namespace to convert that into a single string containing with internal nulls, and finally use the String class’s Split method to separate the individual name strings.

As you’ll see in the code that follows, we specified the ANSI form of the API function, so that it would work both on the Windows 9x and NT families.

   using System;   using System.Runtime.InteropServices;   using System.Text;      namespace ProfileFunctions   {      class Class1      {   // DWORD GetPrivateProfileSectionNames(   // LPTSTR lpszReturnBuffer,  // return buffer   // DWORD nSize,      // size of return buffer   // LPCTSTR lpFileName    // initialization file name   // );     [DllImport("kernel32.dll",CharSet=CharSet.Ansi)]        static extern uint         GetPrivateProfileSectionNamesA(        byte[] lpszReturnBuffer,        int nSize,        String lpFileName);         [STAThread]      static void Main(string[] args)      {            byte[] buff=new byte[1024];            GetPrivateProfileSectionNamesA(buff,                buff.Length,"win.ini");            String s =                System.Text.Encoding.Default.GetString               (buff);            String[] names=s.Split('');            foreach(String name in names)            {               if(name=="")                  break;               Console.WriteLine(name);            }         }      }   } 

Essentially the same method will also work in Visual Basic .NET. When you run the downloadable code for this article, you’ll get a list of the section names in the current machine’s win.ini file. The exact list depends on what’s been installed on the machine.

In the next part of this article, I’ll discuss how to use the MarshalAs and the StructLayout options to control precisely how the runtime passes data to and from unmanaged code.

Share the Post:
Share on facebook
Share on twitter
Share on linkedin

Overview

The Latest

Top 5 B2B SaaS Marketing Agencies for 2023

In recent years, the software-as-a-service (SaaS) sector has experienced exponential growth as more and more companies choose cloud-based solutions. Any SaaS company hoping to stay ahead of the curve in this quickly changing industry needs to invest in effective marketing. So selecting the best marketing agency can mean the difference

technology leadership

Why the World Needs More Technology Leadership

As a fact, technology has touched every single aspect of our lives. And there are some technology giants in today’s world which have been frequently opined to have a strong influence on recent overall technological influence. Moreover, those tech giants have popular technology leaders leading the companies toward achieving greatness.

iOS app development

The Future of iOS App Development: Trends to Watch

When it launched in 2008, the Apple App Store only had 500 apps available. By the first quarter of 2022, the store had about 2.18 million iOS-exclusive apps. Average monthly app releases for the platform reached 34,000 in the first half of 2022, indicating rapid growth in iOS app development.