Marshal an Array of Zero Terminated Strings or Structs by Ref






4.32/5 (13 votes)
Marshal an array of zero terminated strings or structs by reference

Introduction
Sometimes you have to marshal array of strings between managed and unmanaged code.
Microsoft offers some good methods and attributes but sometimes they fall short in complex situations like marshalling by reference.
In this article, I will deal only with native string
s that are zero terminated.
Background
You might be familiar with these MSND samples and you probably tried to use them.
Here is my sample based on their example that I've dropped into my project.
[DllImport("NativeCDll.dll", CharSet=CharSet.Unicode)]
extern static int TakesArrayOfStrings([In][Out][MarshalAsAttribute
(UnmanagedType.LPArray,
ArraySubType=UnmanagedType.LPWStr)] string[] str, ref int size);
If you are not concerned about returning or changing the text from the call into
the DLL, then this is a very easy and elegant way to go.
As the C++ code below shows, you can change the content of the string
s, but you are out of luck if you try to increase the size of the array and add new string
s.
You can however reuse the slots of the existing string
s from the array, or fake a different returned size that must be no greater than the array's size.
If you expect an array build in the unmanaged call, you won't get it.
int TakesArrayOfStrings( wchar_t* ppArray[], int* pSize)
{
const int newsize =*pSize, newwidth = 20;
//check the incoming array
wprintf(L"\nstrings received in native call:\n");
for( int i = 0; i < *pSize; i++ )
{
wprintf(L" %s",ppArray[i]);
CoTaskMemFree((ppArray)[ i ]);
}
wprintf(L"\nstrings created in native call:\n");
for( int i = 0; i < *pSize; i++ )
{
ppArray[ i ] = (wchar_t*)CoTaskMemAlloc( sizeof(wchar_t) * newwidth );
::ZeroMemory(ppArray[ i ],sizeof(wchar_t) * newwidth );
swprintf(ppArray[ i ],newwidth,L"unmanagstr%d",i);
wprintf(L" %s",ppArray[ i ]);
}
*pSize = newsize;
return 1;
}
In order to get an array of string
s build in the unmanaged code, you need to call by reference, and that requires the use of IntPtr
data type.
Calling the Array by Reference
There is little support from the marshaller to do this, and we would have to build a block of memory for the array and separate blocks of memory for each string
that would be seen as an array of char
s.
These blocks will be marshaled back and forth to create the returned string
array.
Let's start with the C++ code:
int TakesRefArrayOfStrings( wchar_t**& ppArray, int* pSize )
{
//check the incoming array
wprintf(L"\nstrings received in native call:\n");
for( int i = 0; i < *pSize; i++ )
{
wprintf(L" %s",ppArray[i]);
CoTaskMemFree((ppArray)[ i ]);
}
CoTaskMemFree( ppArray );
ppArray = NULL;
*pSize = 0;
// CoTaskMemAlloc must be used instead of new operator
// since code on managed side will call Marshal.FreeCoTaskMem
// to free this memory
const int newsize = 5, newwidth = 20;
wchar_t** newArray = (wchar_t**)CoTaskMemAlloc( sizeof(wchar_t*) * newsize);
// populate the output array
wprintf(L"\nstrings created in native call:\n");
for( int j = 0; j < newsize; j++ )
{
newArray[ j ] = (wchar_t*)CoTaskMemAlloc( sizeof(wchar_t) * newwidth );
::ZeroMemory(newArray[ j ],sizeof(wchar_t) * newwidth );
swprintf(newArray[ j ],newwidth,L"unmanagstr %d",j);
wprintf(L" %s",newArray[ j ]);
}
ppArray = newArray;
*pSize = newsize;
return 1;
}
As you probably noticed, the incoming array is completely different from the outgoing array.
Also notice another level of indirection in the argument for this function - wchar_t**& ppArray
.
On the managed code site, the extern
import has changed too:
[DllImport("NativeCDll.dll")]
static extern int TakesRefArrayOfStrings(ref IntPtr array, ref int size);
//or for the multibyte strings
[DllImport("NativeCDll.dll")]
static extern int TakesRefArrayOfMBStrings(ref IntPtr array, ref int size);
Building the Argument for the Call
It is possible you want to pass the argument by reference not by value.
That means that you have to build the pointer to the memory region that would
be consumed by TakesRefArrayOfStrings
. To avoid writing two methods for wide char
s and multibyte string
s, I've made the methods generic.
The C# code below is pretty self explanatory:
public static IntPtr StringArrayToIntPtr<GenChar>(string[]
InputStrArray)where GenChar : struct
{
int size = InputStrArray.Length;
//build array of pointers to string
IntPtr[] InPointers = new IntPtr[size];
int dim = IntPtr.Size * size;
IntPtr rRoot = Marshal.AllocCoTaskMem(dim);
Console.WriteLine("input strings in managed code:");
for (int i = 0; i < size; i++)
{
Console.Write(" {0}", InputStrArray[i]);
if (typeof(GenChar) == typeof(char))
{
InPointers[i] = Marshal.StringToCoTaskMemUni(InputStrArray[i]);
}
else if (typeof(GenChar) == typeof(byte))
{
InPointers[i] = Marshal.StringToCoTaskMemAnsi(InputStrArray[i]);
}
}
//copy the array of pointers
Marshal.Copy(InPointers, 0, rRoot, size);
return rRoot;
}
Building the New String Array from the Result
Once the call is made, we need to do the reverse and create the string
array from a block of memory:
public static string[] IntPtrToStringArray<GenChar>
(int size, IntPtr rRoot)where GenChar : struct
{
//get the output array of pointers
IntPtr[] OutPointers = new IntPtr[size];
Marshal.Copy(rRoot, OutPointers, 0, size);
string[] OutputStrArray = new string[size];
for (int i = 0; i < size; i++)
{
if (typeof(GenChar) == typeof(char))
OutputStrArray[i] = Marshal.PtrToStringUni(OutPointers[i]);
else
OutputStrArray[i] = Marshal.PtrToStringAnsi(OutPointers[i]);
//dispose of unneeded memory
Marshal.FreeCoTaskMem(OutPointers[i]);
}
//dispose of the pointers array
Marshal.FreeCoTaskMem(rRoot);
return OutputStrArray;
}
When rebuilding any string
, notice that the end of it is terminated with '\0
' and that allows us to retrieve the string
s more memory efficiently.
Using the Code
In addition to the input/output array of string
s, you need to specify how to marshal the string
s with a generic parameter byte
or char
.
int size = 3;
string[] InputStrArray = new string[size];
//build some input strings
for (int i = 0; i < size; i++)
{
InputStrArray[i] = string.Format("managed str {0}", i);
}
IntPtr rRoot = GenericMarshaller< byte >.StringArrayToIntPtr(InputStrArray);
int res = TakesRefArrayOfMBStrings(ref rRoot, ref size);
if (size > 0)
{
string[] OutputStrArray = GenericMarshaller< byte >.IntPtrToStringArray(size, rRoot);
//show the array of strings
Console.WriteLine("\nreturned by TakesRefArrayOfMBStrings:");
foreach (string s in OutputStrArray)
{
Console.Write(" {0}", s);
}
}
else
Console.WriteLine("Array after call is empty");
Dealing with an Array of Structs
Marshaling an array of structs is similar with the string
case, but it's a better candidate for generics.
The black magic still takes place when setting the marshalling parameters:
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
public class MyStruct
{
public String buffer;
public int size;
public MyStruct(String b, int s)
{
buffer = b;
size = s;
}
public MyStruct()
{
buffer = "";
size = 0;
}
}
The code to create the IntPtr
should be no surprise:
public static IntPtr IntPtrFromStuctArray<T>(T[] InputArray) where T : new()
{
int size = InputArray.Length;
T[] resArray = new T[size];
//IntPtr[] InPointers = new IntPtr[size];
int dim = IntPtr.Size * size;
IntPtr rRoot = Marshal.AllocCoTaskMem(Marshal.SizeOf(InputArray[0])* size);
for (int i = 0; i < size; i++)
{
Marshal.StructureToPtr(InputArray[i], (IntPtr)(rRoot.ToInt32() +
i*Marshal.SizeOf(InputArray[i])), false);
}
return rRoot;
}
public static T[] StuctArrayFromIntPtr<T>(IntPtr outArray, int size) where T : new()
{
T[] resArray = new T[size];
IntPtr current = outArray;
for (int i = 0; i < size; i++)
{
resArray[i] = new T();
Marshal.PtrToStructure(current, resArray[i]);
Marshal.DestroyStructure(current, typeof(T));
int structsize = Marshal.SizeOf(resArray[i]);
current = (IntPtr)((long)current + structsize);
}
Marshal.FreeCoTaskMem(outArray);
return resArray;
}
The use of these methods should be easy too:
int size = 3;
MyStruct[] inArray = { new MyStruct("struct 1", 1),
new MyStruct("struct 2", 2), new MyStruct("struct 3", 3) };
//build some input strings
IntPtr outArray = GenericMarshaller.IntPtrFromStuctArray<MyStruct>(inArray);
TakesArrayOfStructsByRef(ref size, ref outArray);
MyStruct[] manArray = GenericMarshaller.StuctArrayFromIntPtr<MyStruct>(outArray, size);
Console.WriteLine();
for (int i = 0; i < size; i++)
{
Console.WriteLine("Element {0}: {1} {2}", i,
manArray[i].buffer, manArray[i].size);
}
Final Words
Extending the zero terminated string
s array to BSTR array should be obvious.
The project does not use the unsafe mode and that makes it easier to integrate with other code.IntPtrToStringArray
and StuctArrayFromIntPtr
take a size argument, but you can eliminate it if the code convention is that the last array element is always null
.
History
- 1st February, 2007: Initial post