在进行托管代码与非托管代码之间的交互时,字符串数组的处理是一个常见且复杂的问题。本文将探讨如何在C#和C++之间传递和管理字符串数组。
在处理跨语言调用时,字符串数组的传递是一个挑战,尤其是当涉及到内存管理和数组大小变化时。虽然微软提供了一些方法和属性来辅助这一过程,但在复杂情况下,如通过引用传递数组时,这些工具可能不足以满足需求。
以下是一个基于微软示例的C#代码,用于调用非托管DLL中的函数,该函数接受一个字符串数组:
[DllImport("NativeCDll.dll", CharSet = CharSet.Unicode)]
extern static int TakesArrayOfStrings([In][Out][MarshalAs(UnmanagedType.LPArray, ArraySubType = UnmanagedType.LPWStr)] string[] str, ref int size);
如果不需要从DLL调用中返回或更改文本,上述方法是一个简单而优雅的解决方案。然而,如果需要在非托管代码中构建数组,就需要使用IntPtr数据类型,并进行更复杂的内存管理。
为了在非托管代码中构建字符串数组,需要通过引用调用,这要求为数组和每个字符串分别构建内存块。这些内存块将在托管代码和非托管代码之间来回传递,以创建返回的字符串数组。
以下是C++代码示例,展示了如何在非托管代码中处理传入的数组,并构建一个新的数组:
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;
// 使用CoTaskMemAlloc而不是new操作符
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;
}
在托管代码中,需要构建一个指向内存区域的指针,该内存区域将被TakesRefArrayOfStrings函数使用。为了避免为宽字符和多字节字符串编写两种方法,使方法通用化。
以下是C#代码示例,用于将字符串数组转换为IntPtr,并从结果中构建新的字符串数组:
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;
}
调用完成后,需要做相反的操作,从内存块中创建字符串数组:
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;
}
在使用这些代码时,除了输入/输出字符串数组外,还需要指定如何使用泛型参数byte或char来处理字符串。
处理结构体数组与字符串数组类似,但它更适合使用泛型。设置Marshalling参数时仍然需要一些技巧:
[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;
}
使用这些方法应该很容易:
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);
}