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






4.32/5 (13投票s)
通过引用传递零终止字符串或结构的数组。

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