Click here to Skip to main content
15,886,362 members
Articles / Programming Languages / C#
Article

Marshal an Array of Zero Terminated Strings or Structs by Ref

Rate me:
Please Sign up or sign in to vote.
4.32/5 (14 votes)
1 Feb 2007CPOL3 min read 79.7K   956   29   10
Marshal an array of zero terminated strings or structs by reference
Sample image

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 strings 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.

C#
[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 strings, but you are out of luck if you try to increase the size of the array and add new strings.
You can however reuse the slots of the existing strings 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.

C++
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 strings 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 chars.
These blocks will be marshaled back and forth to create the returned string array.
Let's start with the C++ code:

C++
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:

C#
[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 chars and multibyte strings, I've made the methods generic.
The C# code below is pretty self explanatory:

C#
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:

C#
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 strings more memory efficiently.

Using the Code

In addition to the input/output array of strings, you need to specify how to marshal the strings with a generic parameter byte or char.

C#
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:

C#
[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:

C#
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:

C#
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 strings 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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior)
United States United States
Decebal Mihailescu is a software engineer with interest in .Net, C# and C++.

Comments and Discussions

 
Questionq Pin
Member 1581766625-Feb-24 5:40
Member 1581766625-Feb-24 5:40 
GeneralSource: GenericMarshaller.cs - mixed up calls to Marshal.StringToCoTaskMemUni() and Marshal.StringToCoTaskMemAnsi() Pin
e42014-Dec-08 8:13
e42014-Dec-08 8:13 
GeneralRe: Source: GenericMarshaller.cs - mixed up calls to Marshal.StringToCoTaskMemUni() and Marshal.StringToCoTaskMemAnsi() Pin
dmihailescu15-Dec-08 17:22
dmihailescu15-Dec-08 17:22 
GeneralGreat article. Pin
rayala9-Jun-07 16:16
rayala9-Jun-07 16:16 
QuestionDo we need to Marshal.FreeCoTaskMem(a string) and how? Pin
tina_us20-Apr-07 15:07
tina_us20-Apr-07 15:07 
AnswerRe: Do we need to Marshal.FreeCoTaskMem(a string) and how? Pin
dmihailescu23-Apr-07 10:22
dmihailescu23-Apr-07 10:22 
GeneralRe: Do we need to Marshal.FreeCoTaskMem(a string) and how? Pin
FromTheWest24-Apr-07 5:47
FromTheWest24-Apr-07 5:47 
GeneralRe: Do we need to Marshal.FreeCoTaskMem(a string) and how? [modified] Pin
FromTheWest24-Apr-07 6:24
FromTheWest24-Apr-07 6:24 
GeneralRe: Do we need to Marshal.FreeCoTaskMem(a string) and how? Pin
dmihailescu26-Apr-07 5:44
dmihailescu26-Apr-07 5:44 
GeneralRe: Do we need to Marshal.FreeCoTaskMem(a string) and how? Pin
FromTheWest30-Apr-07 9:23
FromTheWest30-Apr-07 9:23 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.