65.9K
CodeProject 正在变化。 阅读更多。
Home

通过引用传递零终止字符串或结构的数组

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.32/5 (13投票s)

2007 年 2 月 1 日

CPOL

3分钟阅读

viewsIcon

81169

downloadIcon

1007

通过引用传递零终止字符串或结构的数组。

Sample image

引言

有时,您需要在托管代码和非托管代码之间编组字符串数组。
微软提供了一些好的方法和属性,但有时它们在复杂情况下(例如通过引用编组)会失效。
在本文中,我将仅处理以零结尾的本机`string`。

背景

您可能熟悉这些MSND示例,并且可能尝试过使用它们。
这是我基于它们的示例,我将其放入我的项目中。

[DllImport("NativeCDll.dll", CharSet=CharSet.Unicode)]
extern static int TakesArrayOfStrings([In][Out][MarshalAsAttribute
					(UnmanagedType.LPArray, 
ArraySubType=UnmanagedType.LPWStr)] string[] str, ref int size);

如果您不关心从调用中返回或更改文本到
DLL,那么这是一种非常简单且优雅的方式。
如下面的 C++ 代码所示,您可以更改`string`的内容,但如果您尝试增加数组的大小并添加新的`string`,您就无能为力了。
但是,您可以重用数组中现有`string`的槽,或者伪造一个不同的返回大小,该大小不得大于数组的大小。
如果您期望在非托管调用中构建一个数组,您将无法得到它。

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;
}

为了在非托管代码中构建一个`string`数组,您需要通过引用调用,这需要使用`IntPtr`数据类型。

通过引用调用数组

编组器对此的支持很少,我们需要为数组构建一个内存块,并为每个`string`构建单独的内存块,这些内存块将被视为`char`数组。
这些块将被来回编组,以创建返回的`string`数组。
让我们从 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;
}

您可能已经注意到,传入数组与传出数组完全不同。
还要注意此函数的参数中另一个级别的间接性 - `wchar_t**& ppArray`。
在托管代码端,`extern`导入也发生了变化

[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);

构建调用的参数

您可能希望通过引用而不是按值传递参数。
这意味着您必须构建一个指向内存区域的指针,该区域将
被`TakesRefArrayOfStrings`使用。为了避免编写用于宽`char`和多字节`string`的两种方法,我将这些方法通用化。
下面的 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;
} 

从结果构建新的字符串数组

进行调用后,我们需要执行相反的操作,并从内存块创建`string`数组

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;
}   

重建任何`string`时,请注意它的结尾以 '\0' 终止,这使我们能够更有效地检索`string`。

Using the Code

除了输入/输出`string`数组之外,您还需要使用泛型参数`byte`或`char`指定如何编组`string`。

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");

处理结构体数组

编组结构体数组与`string`的情况类似,但它更适合泛型。

设置编组参数时,仍然会发生黑魔法

[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;
    }
}

创建`IntPtr`的代码应该不足为奇

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;
}   

这些方法的使用也应该很简单

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);
}

结束语

将零终止的`string`数组扩展到 BSTR 数组应该很明显。
该项目不使用不安全模式,这使其更容易与其他代码集成。
`IntPtrToStringArray`和`StuctArrayFromIntPtr`接受一个大小参数,但如果代码约定是最后一个数组元素始终为`null`,则可以消除它。

历史

  • 2007 年 2 月 1 日:初始帖子
© . All rights reserved.