Introduction
This article presents an example of how to create a VB.NET program that retrieves data in the form of a variable length array of structs from a DLL file written in C. The challenge here is how to pass data between the safe world of managed code executed by the Common Language Runtime (CLR) and unmanaged code.
Background
Code which is developed using the .NET Framework is known as managed code. This code is directly executed by CLR with the help of managed code execution. Any code that is written in .NET Framework is managed code. Managed code uses CLR which in turns looks after your applications by managing memory, handling security, allowing cross-language debugging, and so on. Code which is developed outside the .NET Framework is known as unmanaged code. Applications that do not run under the control of the CLR are said to be unmanaged, and therefore don’t have the benefits of the CLR. I won’t get any deeper into the definitions of managed and unmanaged code since that is out of the scope for this article and there are plenty of sources for that on the internet. Just Google it :)
Marshalling
The solution for passing data between managed and unmanaged code is of course marshalling. What made the assignment a bit tricky was that some of the unmanaged functions returned an array of structs whose size I didn’t know when I was calling the function.
The Code
I will begin with the unmanaged DLL. Here is the struct declaration in C:
typedef struct {
Char id1[10];
Char id2[20];
}unmanagedStruct;
Here is the declaration of the function:
extern "C" __declspec(dllexport) unmanagedStruct* unmanagedFunction(int &size)
{
unmanagedStruct *outStruct = new
unmanagedStruct[myStructSize];
size = myStructSize;
return outStruct;
}
As you can see, the function returns a pointer since what I need on the managed side is a pointer to the unmanaged memory I have allocated. The next thing to note is that I am taking the integer size as a reference call. This variable enables me to get the size of the array on the managed side. How the elements of the array are retrieved will be explained in detail when I present the managed code. Now I will just show the relevant parts of the unmanaged function. Of course, the function should do a lot more such as fill the array with values and so on, but that is out of the scope for this article. So as you can see, I am declaring my array of structs and allocating memory for it. Then I set the size
variable to the size of the array so that the managed code will be able to know the size. Finally, I return the pointer to the caller of the function.
Now on the managed side, in Visual Studio, I start by declaring the struct I intent to use. Notice that I am using the MarshalAs
attribute class to specify the length of my fields in the structure. These lengths have to be the same as the lengths declared in the unmanaged DLL.
<StructLayout(LayoutKind.Sequential, CharSet:=CharSet.Ansi)> _
Public Structure managedStruct
<MarshalAs(UnmanagedType.ByValTStr, SizeConst:=10)> _
Public id1 As String
<MarshalAs(UnmanagedType.ByValTStr, SizeConst:=20)> _
Public id2 As String
End Structure
I continue by declaring a DllImport
to my unmanaged DLL and the function which I intend to use.
<DllImport("myUnmanagedDll.dll")> _
Public Shared Function unmanagedFunction(ByRef size As Integer) As IntPtr
End Function
And finally, here comes the part where I call the unmanaged function from my managed code, retrieve the pointer to the unmanaged array of structs, and retrieve the values.
Dim p1 As IntPtr Dim cleanUpPtr as IntPtr
Dim numberOfResults As Integer Dim resultArray() As managedStruct
p1 = unmanagedFunction(numberOfResults)
cleanUpPtr = p1
ReDim resultArray(numberOfResults - 1)
Dim size As Integer = Marshal.SizeOf(GetType(managedStruct))
Dim start As Integer = (CType(p1, Integer) + _
(Marshal.SizeOf(GetType(managedStruct)) - size))
p1 = New IntPtr(start) Dim i As Integer = 0
Do While (i < numberOfResults)
resultArray(i) = CType(Marshal.PtrToStructure(p1, _
GetType(managedStruct)), managedStruct)
If (i < (count - 1)) The
p1 = New IntPtr((CType(p1, Integer) + size))
End If
i = (i + 1)
Loop
After the call to the unmanaged function, I have the number of posts I have retrieved in the array. In the variable called size
, I store the size of every array element so I know how many bytes I have to increment to retrieve the next post. Then I proceed to set the pointer p1
to the first array element, and finally use Marshal.PtrToStructure
to retrieve the values. Once I have retrieved an array element, I continue to set the pointer to the next element. This step is repeated until I have retrieved all the elements and now I have the entire unmanaged array in the managed memory.
Now as a last thing, remember to clean up the unmanaged memory. That’s why I created an extra pointer called cleanupPtr
since I will loop over the array with the original pointer. cleanUpPtr
is still pointing to the beginning of the unmanaged memory. The cleanup operation can be performed directly from managed memory by using the CoTaskMemFree()
attribute or by using the delete[]
operator in the unmanaged code.
Final Words
As a final word, I would like to answer the question I guess many of you would ask: Why do it in VB.NET, and the answer to that question is that was part of the requirement when I got the task. If it was up to me, I would have done it in C#. I am a C# guy.
History
- 24 July, 2011: Fixed some copy-paste error in the code part of the article.