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;
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 )
{
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;
const int newsize = 5, newwidth = 20;
wchar_t** newArray = (wchar_t**)CoTaskMemAlloc( sizeof(wchar_t*) * newsize);
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);
[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;
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]);
}
}
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
{
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]);
Marshal.FreeCoTaskMem(OutPointers[i]);
}
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];
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);
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];
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) };
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