Using more complicated PInvoke calls
In my last blog post I covered using a very basic PInvoke call to an unmanaged dll that returned an integer. For basic data types int, char, double, float, etc you don't need to do any manual marshaling. However, especially with the Win32 API you will find yourself needing to use more complicated types. Even getting a string back from an unmanaged dll takes more work, and then we have classes, structs, arrays, etc we need to deal with.
Let's look at some unmanaged dll signatures
Now here we have three functions; one fills a byte array, one returns a string, and another modifies a struct we've defined. Nothing complicated on the C++ side but we need to be careful when we start using pinvoke to call these functions. Let's look at some C# code that we can use to call these functions.
First let's fill that byte array with some data. Now I know you won't know the size of the array every time but after working with pinvoke arrays it's better if you can pass in the size of the array, it makes working with them much easier.
Let's start with the byte array
C/C++ treats arrays as pointers, so we're going to let the framework handle the memory management for us and just pass in an array with the array length. We can let the .net framework handle the memory mapping from our call to the actual dll memory use. The above function works very well for basic data types, but we often find ourselves needing to use more complicated types like strings and structs when dealing with unmanaged code. I will cover those items next.
Let's look at the function
You might think
Would be enough. Sadly dealing with strings gets complicated when making pinvoke calls. I have found when dealing with any strings in pinvoke calls it's simply better to use the IntPtr class with the Marshal static helper functions. Let's look at some code below.
Now let's take a look at the final example. Passing in a struct that the unmanaged code can read/modify etc. This one takes a bit more work but is very handy, especially since many of the Win32 APIs take their own struct definitions as input parameters. Now you will need to know the structure of the struct you plan to use. This isn't an issue usually when you have access to the source code or documentation but it needs to be noted.
First we need to have a struct that matches the struct in the unmanaged side. You can use a class if desired but I have found it easier to make a struct that matches exactly. I often find myself using an internal private struct to make the call and that using a helper class that converts the struct to a standard C# class.
So let's look at some C# code. First we need to create a struct that matches that unmanaged side struct. From above wherever we have string, use an IntPtr. For the int we can match the basic data type.
Let's look at some unmanaged dll signatures
extern "C"
{
struct
{
char* NAME,
char* ADDRESS,
int age
} PERSON;
void FillByteArray( unsigned char* byteArray, int arrayLength );
const char* GetLastErrorMessage();
void FillAStruct( PERSON* pPerson );
}
Now here we have three functions; one fills a byte array, one returns a string, and another modifies a struct we've defined. Nothing complicated on the C++ side but we need to be careful when we start using pinvoke to call these functions. Let's look at some C# code that we can use to call these functions.
First let's fill that byte array with some data. Now I know you won't know the size of the array every time but after working with pinvoke arrays it's better if you can pass in the size of the array, it makes working with them much easier.
Let's start with the byte array
[DllImport(MyDll.dll)]
public static extern void FillByteArray( byte[] array, int arrayLength );
C/C++ treats arrays as pointers, so we're going to let the framework handle the memory management for us and just pass in an array with the array length. We can let the .net framework handle the memory mapping from our call to the actual dll memory use. The above function works very well for basic data types, but we often find ourselves needing to use more complicated types like strings and structs when dealing with unmanaged code. I will cover those items next.
Let's look at the function
const char* GetLastErrorMessage();
You might think
[DllImport(MyDll.dll)]
public static extern string GetLastErrorMessage();
Would be enough. Sadly dealing with strings gets complicated when making pinvoke calls. I have found when dealing with any strings in pinvoke calls it's simply better to use the IntPtr class with the Marshal static helper functions. Let's look at some code below.
// declare the function
[DllImport(MyDll.dll)]
public static extern IntPtr GetLastErrorMessage();
// call the function and get back and IntPtr
IntPtr ptrResult = GetLastErrorMessage();
// always check we have a valid pointer before calling the marshal code
// now we need to convert this pointer back to a string
if( ptrResult != IntPtr.Zero )
{
// this will convert the pointerto an ansi string
// there are other methods, but we always use ansi strings
// look at the marshal documentation for the other string options
String errorMessage = Marshal.PtrToStringAnsi(ptrResult);
}
Console.WriteLine( errorMessage );
Now let's take a look at the final example. Passing in a struct that the unmanaged code can read/modify etc. This one takes a bit more work but is very handy, especially since many of the Win32 APIs take their own struct definitions as input parameters. Now you will need to know the structure of the struct you plan to use. This isn't an issue usually when you have access to the source code or documentation but it needs to be noted.
First we need to have a struct that matches the struct in the unmanaged side. You can use a class if desired but I have found it easier to make a struct that matches exactly. I often find myself using an internal private struct to make the call and that using a helper class that converts the struct to a standard C# class.
So let's look at some C# code. First we need to create a struct that matches that unmanaged side struct. From above wherever we have string, use an IntPtr. For the int we can match the basic data type.
public struct Person
{
public IntPtr name;
public IntPtr address;
public int age;
}
Comments