Code Focused

Access Non-.NET APIs with PInvoke

Use PInvoke to take advantage of Windows APIs that aren't part of the .NET Framework.

Technology Toolbox: VB.NET

The .NET Framework provides managed libraries for many Windows APIs, but there are still many Windows APIs that aren't in the .Net libraries. Today, you have to write wrappers for the unmanaged calls if you want to take advantage of Windows Vista's new APIs, create a VPN connection, or use RAS. You can do this by utilizing PInvoke (Platform Invoke), but there are a several caveats you'll have to keep in mind as you do so. I'll walk you through the process of making a PInvoke call and give you the information you need to tackle most of the PInvoke challenges you'll face.

The first step for creating a PInvoke solution is much like any solution process: begin by gathering the requirements and the resources. An important factor when using PInvoke is to determine what platforms you need to support. The .NET Framework 2.0 includes support for Windows 98, ME, 2000, XP, 2003, and Vista. Version 3.5 of the Framework drops support for 98, ME, and 2000. Often, you'll find that the unmanaged API for 98 and ME is different from XP or later operating systems, especially when it comes to Unicode support. Your task becomes a lot easier if you need to support only XP or later.

Next, you need to gather the resources. For Windows API calls, this means you should download the latest Platform SDK. This gives you the documentation and, more importantly, the C++ header files (.H files). If you have the full version of Visual Studio and have installed C++, you might already have many of the header files. In this case, you need to download the Platform SDK only for newer header files such as those for Windows 2008.

Use the MSDN documentation as your primary source of information, and then use the header files to get the structure layouts and values for constants. For example, the MSDN "RasEnumEntries Function" page describes a call to get a list of remote-access or dial-up connections. Figure 1a shows how the MSDN documentation defines the RasEnumEntries function.

You can define this function many different ways in VB. Here's a good signature for the function:

Declare Auto Function RasEnumEntries _
   Lib "rasapi32" Alias "RasEnumEntries" ( _
   ByVal null As IntPtr, ByVal Phonebook As String, _
   <[In](), Out()>    ByVal entries() _
   As RASENTRYNAME, ByRef buffersize As Int32, _
   ByRef count As Int32)    As UInt32

You're declaring the signature, not the actual function, so the function is declared using the Declare keyword and doesn't have an End function statement. The Auto keyword tells the runtime to look for Unicode functions first on Unicode systems; otherwise, it falls back to ANSI calls. If you're working only with XP or later, you can reduce your testing matrix by declaring the function as Unicode instead of Auto. When you do this, you need to ensure that you declare the Unicode function name in the alias:

Declare Unicode Function _
   RasEnumEntries _
   Lib "rasapi32" Alias _
   "RasEnumEntriesW" _

Note the W at the end of the alias name, which signifies the "wide" or Unicode version. An A instead of a W would signify an ANSI Version. The runtime appends the W or A for you when you use Auto.

You can give the function any name you like, and you can also declare the same function multiple times with different signatures, per the usual overloading rules. The important part you must get right is the Alias name and the Lib part. The Lib part refers to the DLL. Lib is short for library, but you should not confuse Lib in the Declare statement with C++ .LIB files. The Lib for this function is the rasapi32.dll; specifying the .DLL part is optional.

An alternative to using the Declare keyword is to declare the function as you would a normal function with an End Function statement. You leave the function empty and mark it with the DllImport attribute. The DllImport attribute gives you greater control over various aspects of the method call compared to the Declare syntax:

<DllImport("rasapi32", EntryPoint:= _
   "RasEnumEntries", CharSet:=CharSet.Auto)> _
   Private Shared Function RasEnumEntries( _
   ByVal null As IntPtr, ByVal Phonebook As String, _
   <[In](), Out()> ByVal entries() _
   As RASENTRYNAME, ByRef buffersize As UInt32, _
   ByRef count As UInt32) As UInt32
   ' no code allowed in here
   End Function

Personally, I prefer the Declare syntax because it's usually shorter and easier to write.

One part that people often get wrong is when retrieving the parameters and return types of the correct types. An essential reference for this is the "Windows Data Types" document in the MSDN library. This document tells you what DWORD, LPDWORD, LPCTSTR, and various other unmanaged types are.

After a while, you can decipher most of these type names for yourself. When it comes to marshalling values between managed and unmanaged code, size does matter. A WORD is a 16-bit unsigned value (UInt16). DWORD is a Double WORD, which gives you a UInt32. The name WORD derives from early x86 systems of the 16-bit Windows 3.x era (the 1980s). An LP means a long pointer, where long is defined as 32-bit on 32-bit systems and 64-bit on 64-bit systems. LPCTSTR means a long pointer (LP) to a constant (C) string (STR) that is Unicode on Unicode systems and ANSI on ANSI systems (T). Armed with this knowledge and the handy reference, the translation becomes reasonably straightforward.

The DWORD for the function return value is translated to As Uint32. The first parameter is defined as an LPCTSTR with the name of reserved. If you read the documentation, it says this parameter isn't used and you should pass in null. Thus, you could declare the parameter as a ByVal String, and it would get marshaled as an LPCTSTR for you. If you declare it as a String, be sure that you pass it only Nothing, not "" or String.Empty. To avoid any confusion, I prefer to declare the parameter as an IntPtr and pass to it IntPtr.Zero, which is the same as passing it a null reference.

The second parameter, the phonebook name, is defined as LPCTSTR. In this case, you translate the parameter to ByVal phonebook As String. The fourth and fifth parameters are defined as LPDWORDs, which technically translate to ByRef As UInt32. In this case, the numbers are never going to be so high as to cross the sign bit (&H80000000), so you can declare them as ByRef As Int32. This will make math and array declarations much easier and let you avoid a lot of unnecessary casts.

The third parameter takes a bit more work than the other four. It is defined as:

  __in_out   LPRASENTRYNAME lprasentryname

The "LP" part of the LPRASENTRYNAME tells you that it's a long pointer to a RASENTRYNAME structure. The MSDN documentation says the parameter is a "Pointer to a buffer that, on output, receives an array of RASENTRYNAME structures, one for each phonebook entry." This means that you're passing in the address of the first element in an array, not a pointer to the array itself. You must declare the array as ByVal, which will be the address of its value, the first element.

You now need the RASENTRYNAME structure declaration. Again, the MSDN documentation provides the C++ definition (see Figure 1b).

The #if #end if block tells you that the structure has two extra fields on Windows versions later than 0x500 (Windows 2000): dwFlags and szPhonebookPath. Likewise, you can use conditional compilation blocks in your VB declaration and define a compiler #Const WinVer. The problem you then face is that you have different compilations for different platforms. In that case, your support matrix and distribution complexities have increased. Alternatively, you can use the old structure and omit the extra two fields on the presumption that, generally, the older definition will be supported on later platforms. I don't think either of these options is the best choice. Instead, I think the best approach here is to look carefully at what platforms you want to support. Do you need to support Windows 98 and ME? You also need to take into consideration issues such as .NET 3.5 not being available on 98 and ME. I think a reasonable minimum platform requirement today is XP or 2003 (winver 501). This simplifies the structure declaration to this:

<StructLayout( _
   LayoutKind.Sequential, _
   CharSet:=CharSet.Auto)> _
Public Structure RASENTRYNAME

   Public Size As Int32

   <MarshalAs( _
      UnmanagedType.ByValTStr, _
      SizeConst:= _
      RAS_MaxEntryName + 1)> _
      Public EntryName As String

   Public Flags As _
      RasEntryNameFlags

   <MarshalAs( _
      UnmanagedType.ByValTStr, _
      SizeConst:=MAX_PATH + 1)> _
      Public PhonebookPath As String

End Structure

Public Enum RasEntryNameFlags _
   As Int32
   User = 0
   AllUsers = 1
End Enum

Note that the Structure has a StructLayout applied to it. This tells the runtime to lay the fields out sequentially. When the fields are packed sequentially, you can also specify the Pack value for the StructLayout attribute. By default, the Pack is 4 bytes (dword) on 32-bit systems. The Pack value tells the compiler how to align the structure's fields. Each field is aligned so that it starts on a multiple of either its natural size or the Pack value, whichever is smallest. The natural size of integers is their width. The natural size of an Int32 is 4 bytes, the natural size of an Int16 is 2 bytes, and so on. The natural size of marshaled strings is the width of a character, which is 1 byte for ANSI and 2 bytes for Unicode. In this particular case, compiling the RASENTRYNAME structure with a Pack value of 4 causes padding to be placed between the second and third fields to ensure the third field starts on the proper boundary. The compiler adds two more bytes of padding at the end of the structure to ensure that an array of the structure is properly aligned (see Figure 2). If you change the Pack value, you can cause the fields to misalign. Often the header files will have an included statement that indicates the packing to be used. In the ras.h file the pack value is 4, as indicated by: #include <pshpack4.h>.

If you want to, you can create an explicit structure layout by changing the LayoutKind to LayoutKind.Explicit, and then apply the FieldOffset attribute to each field. You supply the location as a constant for each field to the FieldOffset attribute. Hence, you must decide at design time if your structure is Unicode or ANSI strings, as this will impact the offsets. Typically, you should avoid using an explicit field layout unless there's a particular problem you're trying to solve, such as overlaying values on top of each other.

An important thing to note here is that if you change the CharSet value on the structure, you should change it on all Declare and PInvoke declarations that use that structure.

Note that I modified the Size and Flags fields away from UInt32. With Size, UInt32 is definitely more correct because the size can't be negative. However, the common way of determining the size is to use the Marshal.SizeOf method, which returns an Int32; therefore, defining Size as Int32 makes it easier to use. It also makes the Flags field easier to use if the defined values are in an Enum.

The EntryName and PhonebookPath fields are inline Chars, but to make them easier to work with, you define them as Strings and apply the MarshalAs attribute to them. ByValTStr means the value of the string is placed inline. The T again means the size per character depends on whether Unicode or ANSI is being used as defined by the StructLayout's CharSet value. As these strings are fixed-length strings, you need to define the SizeConst values for the MarshalAs attribute, such as RAS_MaxEntryName + 1 or MAX_PATH + 1. The MSDN documentation doesn't give you the values for RAS_MaxEntryName or MAX_PATH; instead, you search for them in the header files in the Platform SDK.

Learn the Ins and Outs
When I walked you through the RasEnumEntries function declaration, I skipped over the In and Out attributes. Before covering them, I wanted you to see the RASENTRYNAME structure and get an understanding of its layout in unmanaged code. In managed code, the RASENTRYNAME's layout is four fields, with the strings being pointers, not the inline values. It's important to understand that the structure requires marshalling, so it's referred to as a non-blittable type. Strings are also non-blittable types. Blittable types are simple types such as UInt32 or Int32 that are identical in both managed and unmanaged code. For non-blittable types, you usually need to specify the marshalling direction: the Ins and Outs.

The In and Out attributes modify the default behavior for marshalling. By default, parameters declared as ByVal are marshaled only In, and those ByRef are marshaled In/Out. For the RASENTRYNAME array, you need to pass the values In and get the values Out, which differs from the default behavior applied to parameters declared as ByVal. This means you must explicitly add the Out attribute and the In attribute to the declaration.

For the first, fourth, and fifth parameters, adding the In and Out attribute doesn't change anything because they're blittable types. I declared the second parameter ByVal As String. VB adds its own special touch of magic here and defines the string as being marshaled as an UnmanagedType.VBByRefStr. VB changes the ByVal to ByRef and adds the In and Out attributes. Consider the parameter declaration:

 ByVal Phonebook As String 

VB changes it to this when it compiles:

 <[In](), Out(), MarshalAs(UnmanagedType.VBByRefStr)> _ 
   ByRef Phonebook As String 

You might be scratching your head and wondering how on earth the original definition (_in LPCTSTR lpszPhonebook) maps to all that. When most people see a definition like __in LPCTSTR they map that to an UnmanagedType.LPTStr. In fact, you can declare the parameter like this:

<MarshalAs(UnmanagedType.LPTStr)> ByVal Phonebook As String 

This declaration will work, but it isn't safe.

The problem is that .NET strings are immutable, but all bets are off as soon as you pass them out of managed code. There's no guarantee the code being called won't change a given string, and in doing so, make the string suddenly mutable. VBByRefStr marshalling creates a new string and then assigns that to the parameter, ensuring that immutable behavior is preserved. The simple VB syntax is robust and secure. By contrast, it's quite scary to consider how much C# code out there doesn't isolate string declarations properly when using LPTStr marshalling. Of course, in C# you could change the parameter to ref and use VBByRefStr, or alternatively ensure a new string is created and is not interned.

With your declarations in place, i's dotted and t's crossed, making the call is reasonably straightforward. Usually, you need to check function return values for success or failure codes because the function won't throw an exception if things go wrong. In the RasEnumEntries example, you're responsible for allocating a suitable buffer for the RAS entries: You can't know ahead of time exactly how many entries there will be, so you need to handle those error codes and resize your array as needed (see Listing 1).

.NET provides a declarative approach to marshalling that allows you to specify how you want the variables to be mapped from managed to unmanaged memory and back. There are times when you need to do a little more than this, such as when allocating and de-allocating unmanaged memory blocks, which I'll cover in next month's column, along with other advanced PInvoke tips. PInvoke calls don't have to be hard, but they do require that you take a systematic approach.

About the Author

Bill McCarthy is an independent consultant based in Australia and is one of the foremost .NET language experts specializing in Visual Basic. He has been a Microsoft MVP for VB for the last nine years and sat in on internal development reviews with the Visual Basic team for the last five years where he helped to steer the languageā€™s future direction. These days he writes his thoughts about language direction on his blog at http://msmvps.com/bill.

comments powered by Disqus

Featured

  • Compare New GitHub Copilot Free Plan for Visual Studio/VS Code to Paid Plans

    The free plan restricts the number of completions, chat requests and access to AI models, being suitable for occasional users and small projects.

  • Diving Deep into .NET MAUI

    Ever since someone figured out that fiddling bits results in source code, developers have sought one codebase for all types of apps on all platforms, with Microsoft's latest attempt to further that effort being .NET MAUI.

  • Copilot AI Boosts Abound in New VS Code v1.96

    Microsoft improved on its new "Copilot Edit" functionality in the latest release of Visual Studio Code, v1.96, its open-source based code editor that has become the most popular in the world according to many surveys.

  • AdaBoost Regression Using C#

    Dr. James McCaffrey from Microsoft Research presents a complete end-to-end demonstration of the AdaBoost.R2 algorithm for regression problems (where the goal is to predict a single numeric value). The implementation follows the original source research paper closely, so you can use it as a guide for customization for specific scenarios.

  • Versioning and Documenting ASP.NET Core Services

    Building an API with ASP.NET Core is only half the job. If your API is going to live more than one release cycle, you're going to need to version it. If you have other people building clients for it, you're going to need to document it.

Subscribe on YouTube