You’re a VB.NET developer. You’re writing code, and you need to integrate with a machine tool, automation system, sensor, scanner, or some other bit of hardware.
Lucky for you, the manufacturer provides a library. But wait… it’s not a .NET library. You can’t just “Add Reference…” the DLL.
If you’ve never used P/Invoke, this is where you start.
So what’s P/Invoke, anyway?
P/Invoke, or more officially, “Platform Invocation Services”, is the .NET way of calling into older C-style DLLs. Back in the ’90s, this typically would have meant the Windows API or other device libraries.
Today, new .NET developers may go for years without encountering one of these older libraries. But machining is a conservative industry, and sooner or later you’re likely to need to P/Invoke a non-.NET DLL.
(And hey, if you’re coding .NET on Linux, it works there, too.)
How do I go about P/Invoking?
First, how lucky are you?
If you’re really lucky, the manufacturer included sample code that includes the P/Invoke function declarations that you need. (Don’t relax yet. Sometimes the manufacturer code is wrong. The Fanuc FOCAS library, for example, contains many functions that return arrays of structs incorrectly.)
If you’re less lucky, you have a C header file (file extension .h), which are meant to be included in C code. Don’t worry: this still contains all the information you need to declare a P/Invoke function, but you’ll have to do a little work to convert it.
So what does a P/Invoke declaration look like? Well, VB.NET actually allows two different methods of declaring a P/Invoke function.
Declare Syntax (Old Style)
This syntax is an older style, carried over from VB6. Despite
It’s still a good idea to be familiar with this syntax, because some manufacturers provide P/Invoke declarations that were auto-upgraded from pre-.NET versions of VB, and these are the ones most likely to need tweaking.
Public Declare Unicode Function FindWindow Lib "user32" Alias "FindWindowW" (lpClassName As String, lpWindowName As String) As IntPtr
Almost looks like a normal function definition, with a couple changes.
First, that “Declare” keyword indicates that this is a P/Invoke declaration.
The “Unicode” specifier tells .NET that strings should be marshaled as Unicode (other options are “Ansi” and “Auto”). If you don’t specify it, the default is “Ansi”, which is probably what you want for most C libraries.
The “Lib” specifier tells .NET which library to look in: in this case, “user32”, which is a Windows library. If you’re using a non-Windows library, specify the filename, e.g. “fwlib.dll”.
An “Alias” is optional. In this case, I named the function “FindWindow”, but it’s actually called “FindWindowW” in the WinAPI (the W indicates a Unicode, or “wide character” function). This lets you give the function a friendly name, but alias it to the “real” name of the function in the library.
And that’s all. Everything else is a normal function declaration… except that there’s no function body and no “End Function”. (And yes, you can also P/Invoke a Sub — that would be a function with a “void” return in C.)
Attribute Syntax (.NET Style)
There’s another P/Invoke declaration syntax that’s used in .NET. This syntax uses the DllImport attribute. Here’s the same declaration as before, in the newer syntax:
<DllImport("user32.dll", Charset:=CharSet.Unicode, EntryPoint:="FindWindowW">
Public Function FindWindow(lpClassName As String, lpWindowName As String) As IntPtr
The DllImport attribute is applied to what is otherwise a normal function declaration. (Don’t try to add any code to the function body: that’s an error.) The attribute optionally lets you specify the Charset (Unicode) and the EntryPoint (Alias in the old syntax).
(The DllImport attribute, along with most of the other classes needed for P/Invoke, are in the System.Runtime.InteropServices namespace, so if you’re getting an error, be sure to Import that namespace at the top of your code file.)
In the previous example, you probably noticed that the parameters were prefixed with “lp”. Many older libraries use some variant of Hungarian notation as a hint about the variable’s type.
Some common prefixes are:
- n – Integer, usually a Short or Integer in VB.
- w – Word, again, possibly a Short or Integer.
- l – Long, usually an Integer or Long in VB.
- dw – Double Word, again, possibly an Integer or Long.
- f – “floating point” – a Single in VB.
- db – “double” – a Double in VB.
- sz – “zero-terminated string”, a C-style ANSI string.
- u – “unsigned”, usually used along with the other numeric prefixes.
- h – A “handle” to some resource. In WinAPI, you’ll often see hWnd (window handle), hDC (device context handle), hFile (file handle), hDevice (device handle), etc.
- p, ptr – a pointer
And “lpClassName”? That’s a long pointer to ClassName. The ANSI version of the function probably named the variable lpszClassName, “long pointer to zero-terminated string ClassName”.
(Nowadays we can just hover over the variable with the mouse pointer, so we don’t use Hungarian prefixes.)
For a convenient table of unmanaged types, and their managed equivalents, see this MSDN article. This may come in handy if all you have is a header file: match the C types in the header file with the .NET types in the table. (But keep in mind that a VOID function in C is a Sub in VB.)
Passing More Complicated Types
The .NET runtime already knows how to pass primitive types like integers, booleans, and floating-point numbers. More complicated types require a little more work.
String (and StringBuilder) types can be passed, but by default, the .NET runtime marshals these as BStr. This is a Basic-style string, or COM string: that is, a length prefix that specifies how long the string is, followed by that number of Unicode characters.
Most C libraries will expect either an LPStr or an LPWstr. LPStr is a C-style string: zero or more ANSI characters, terminated with a null (\0) character. LPWStr is similar, but with Unicode (wide) characters.
To do this, decorate the string parameter with a MarshalAs attribute. In the MarshalAs attribute, specify the appropriate UnmanagedType: usually UnmanagedType.LPStr.
There are a few other types of strings, but LPStr is the most common when working with C libraries. The MSDN article on string marshaling gives more information
Just as Basic and C specify strings in different ways, .NET and C specify arrays differently. In .NET, an array is an object that stores information about the array’s type and size. In C, an array is just a pointer to the first element in the array.
Once again, though, most of the time you’ll just be marshaling to UnmanagedType.LPArray. Yep, a pointer to the first element in the array.
If you’re passing an array into the P/Invoke function, the .NET runtime can determine the size of the array. However, if the function is passing an array back out to you (probably as a ByRef parameter), you must specify either SizeConst or SizeParamIndex on the MarshalAs attribute. This tells the .NET runtime to expect an array of a fixed size (SizeConst), or to check the value of another parameter to determine the array size (SizeParamIndex).
On some occasions, you may also need to specify the ArraySubType, to tell the .NET runtime how to marshal the elements of the array. For more information, see the MSDN article on array marshaling.
You can pass structures to and from P/Invoke functions, but generally the structures need to be blittable: roughly, contain only primitive numeric types or structures that contain only primitive numeric types.
You may also need to worry about layout: the arrangement of the structure’s fields in memory. By default, .NET will assume sequential layout, aligned on word boundaries.
Quite often, the return values of these library functions are error codes. In WinAPI, these may be typed as HRESULT: in other libraries, the value may be an integer type.
The library documentation should describe the meaning of the error code, but generally, zero or positive indicates a success, and a negative value indicates a failure.
Be sure to check the return values if they are documented as error codes: P/Invoke functions will not throw exceptions, so this is your only indication that something has gone wrong.
For actual returned objects, it is more typical for a library to set a ByRef parameter with the return object. In this case, the return value is probably going to be a count (number of items / bytes returned in the ByRef parameter), or again, negative for an error.
Ok, so you’ve declared the P/Invoke function. You’ve figured out all the parameter types, and how they need to be marshaled, and what the function will return. That’s it, right?
Just one more thing. You should be aware that some libraries use different calling conventions. A calling convention specifies how parameters are passed to the function, and in what order, and who cleans up afterward, and… 99% of the time you don’t have to worry about this.
But for that 1% of the time, you should know that by default, .NET assumes the STDCALL calling convention. This is also sometimes referred to as the WINAPI convention (although technically on WinCE this is incorrect).
If your library documentation mentions STDCALL or WINAPI, or the function declarations in the header file specify WINAPI or __stdcall, this is fine.
Some C libraries use the CDECL calling convention. To call these, you will have to specify CallingConvention.CDecl in the DllImport attribute.
There are other, less common, calling conventions that you probably will not run across. If the library uses FASTCALL, this is unfortunately not supported by .NET.
Calling the Function
This is the easy part. All the complexity was in the setup. Now you just call it like any other function.
Or it doesn’t work. Darn.
Here are some problems that are common, but easy to overlook.
Check the error codes
First, I’ve already mentioned this, but return values are often error codes. If you are able to call the function, but it’s not returning the expected result, check the return value to see if it indicates a specific error.
Is it single-threaded?
Some libraries are single-threaded by nature. From experience, this is particularly likely for libraries that require an initial call to an Init() function. In this case, you will want to make all calls to the library from the same STAThread.
When is an Integer an Integer?
For some very old libraries, the P/Invoke declarations were migrated from VB6 or earlier. In VB6, Integer was 16-bit, and Long was 32-bit. When VB.NET came along, Integer was promoted to 32-bit, Long to 64-bit, and we gained a 16-bit Short data type.
If the manufacturer-provided declarations use the old “Declare” syntax, and accepts or returns integer types, it’s possible the manufacturer forgot to upgrade the integer types.
Pin your Structs
The .NET garbage collector runs frequently in the background, getting rid of old objects that are no longer in use anywhere. In the process, it can move existing objects around in memory.
For your .NET application, this isn’t a problem: references to an object follow the object around as the GC moves them.
If you’ve passed a reference into an unmanaged DLL, though, then when the GC moves that object, it pulls the rug out from under the unmanaged code.
This is generally a bad thing.
Prior to passing an array or object to the unmanaged DLL, you may need to pin the object.
That’s a quick whirlwind overview of P/Invoke. It’s by no means comprehensive: the MSDN documentation is much more in-depth, and the manufacturer documentation is very important in order to get up and running.
When you’re working with non-.NET libraries, having some knowledge of P/Invoke lets you understand the limitations of the library. Sometimes it even allows you to correct mistakes in the manufacturer’s P/Invoke declarations.
Although I keep referencing “older libraries”, C-style DLLs continue to be a least-common-denominator interface even for libraries newly-developed in languages other than C. Until the day that all of these libraries are replaced with WebAPIs (or whatever comes afterward), we’re likely to continue using these for as long as we’re developing code in the manufacturing industry.