首先介绍下这个问题的背景,是来自很久前一个同事问我请教的问题,当时我也没搞清楚,还去88上问了下。现在我有些空余时间,在88上有看到了自己的提问,想想有必要研究清楚这个问题到底是怎么回事。
其次我要对中文MSDN的文档表达以下不满,正是由于MSDN的中文文档对这个函数的介绍的语义比较模糊,不精确,才导致我当时无法理解清楚这个函数的设计用意和用途是什么。
第三,我要顺便鄙视下.net的PInvoke和marshal机制,应该说用.net托管代码去调用非托管DLL,简直比单纯使用C/C++更痛苦。所以所有使用.net的同志,希望你有好运气,你一直不需要调用非托管代码!否则.net在内存上的模糊不清,和托管环境和native code之间的内存数据封送,一定会让你感到十分气恼,你需要控制那些你平时根本无法把握也不必了解的数据的内存布局,这根本就不是.net 想给予程序员的能力!
现在就来看下这是个什么问题:有下面这样一些代码,这些代码是什么意思?
-
IntPtr[] ptArray = new IntPtr[1];
-
-
ptArray[0] = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(TAX_ITEM)) * 6);
-
IntPtr pt = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(TAX_ITEM)));
-
Marshal.Copy(ptArray, 0, pt, 1);
很显然,TAX_ITEM是一个struct。这个问题的核心是最后一行代码该如何理解呢。我们为此再看下这个Copy函数的MSDN说明:
-
public static void Copy (
-
IntPtr[] source,
-
int startIndex,
-
IntPtr destination,
-
int length
-
)
“将数据从一维托管 IntPtr 数组复制到非托管内存指针。 ”,这是MSDN文档中的原话。正是这句话让当时的我产生了误解,因为它没有表达清楚一个重要的信息,就是这个函数的真正目的是什么,现在当然,我已经通过测试代码搞清楚了,现在就让我告诉你,这个函数版本(因为Copy有好多个重载版本,这里专指这一个)的目的是,把一个指针数组的内容,拷贝到另一个内存地址,显然,后者的含义也是指针数组。注意,一旦你理解了这是指针数组的拷贝,那么这个函数的目的就毫无歧义了,没拷贝一个IntPtr元素,即相当于拷贝了四个字节(对于win32来说)!!!每个元素都是一个指针变量(即内存地址)!
这里再说明一点,IntPtr这个变量,在C#里本质上就是Int整数类型,但是使用 IntPtr 来表示一个内存地址,通常就表示它是来自native code中的非托管内存地址,因为在 .net 里,(除了Marshal的成员函数能对它进行一些数据拷贝动作)你对它几乎做不了什么事!因此,第一个参数 IntPtr[] souce 可以这样理解,是一个非托管指针组成的数组,第三个参数 IntPtr destination 同样是一个非托管内存的地址,用于接收前者数组内的元素。
理解了上面这些,我们再看那段代码,它写的是有点问题的,就是它的问题加上MSDN的模糊表述让我产生的困惑。现在我们看上面的代码的问题在哪。
首先,第三行代码应该写成下面这样:因为它只拷贝一个元素,所以我们只需要能容纳一个指针的数组就够了!
IntPtr pt = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(InPtr))*1); 这里应该写成这样,这是指针数组的size
它的ptArray中的第一个元素指向了一个能容纳 6 个 TAX_ITEM 的内存,这个内存多大我们不去关心。然后第三行代码申请了 pt,大小是 TAX_ITEM,这里就是给我误解的地方,因为这里应该是指针的sizeof,决不是结构体本身。但是TAX_ITEM是一个明显大过指针(4 bytes)的结构体,所以身请的内存足够大,且有富裕,所以这段代码运行到第四行为止都是不会产生任何问题的。
现在就让我们看下测试代码:为此我需要新建两个项目,一个是C++ 的 DLL项目,我们写一个使用指针数组为参数的DLL导出函数:
--------------------------------------下面是C++ DLL 代码:
//我们先定义一个结构体:
-
typedef struct _test_struct
-
{
-
int index;
-
char text[48];
-
} TEST_STRUCT, *LPTESTSTRUCT;
//再来定义一个使用上面的结构体指针数组为参数的测试函数:
//注意,我们还需要一个参数是count,因为指针数组参数无法表示自己含有多少元素。
//对每个元素,我们把它指向的数据内容写到一个文本文件里。
-
void WINAPI TestFunc(LPTESTSTRUCT* ppDatas, int count)
-
{
-
int i;
-
char line[96];
-
FILE* stream = _tfopen(_T("C:\\TestMarshal.txt"), _T("w"));
-
-
for(i = 0; i<count; i++)
-
{
-
sprintf(line, "%d %s\n", ppDatas->index, ppDatas->text);
-
fputs(line, stream);
-
}
-
fclose(stream);
-
}
//最后用一个.def文件导出这个函数:
-
LIBRARY "MyTestDll"
-
EXPORTS
-
TestFunc @1
-----------------------------------------------------------------下面是C# Console程序的代码
//现在我们用托管代码去调用上面的DLL,先做一些必要的准备:
注意,这些必要的属性修饰,他们使这个结构体的内存布局和前面C++中的代码完全一致
尤其是如何定义C++结构体中的char[48],取决于CharSet,SizeConst等。
-
[StructLayout(LayoutKind.Sequential, CharSet=CharSet.Ansi)]
-
public struct TestStruct
-
{
-
public int index;
-
[MarshalAs(UnmanagedType.ByValTStr, SizeConst=48)]
-
public string text;
-
}
现在就是测试代码的主体:
-
[DllImport("<pre name="code" class="cpp">[StructLayout(LayoutKind.Sequential, CharSet=CharSet.Ansi)]
-
public struct TestStruct
-
{
-
public int index;
-
[MarshalAs(UnmanagedType.ByValTStr, SizeConst=48)]
-
public string text;
-
}
MyTestDll.dll")]
public extern static void TestFunc(IntPtr pDatas, int count);
static void Main(string[] args)
{
TestStruct data1 = new TestStruct();
data1.index = 101;
data1.text = "hello";
TestStruct data2 = new TestStruct();
data2.index = 102;
data2.text = "world";
IntPtr[] ptArray = new IntPtr[2];
ptArray[0] = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(TestStruct)));
ptArray[1] = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(TestStruct)));
Marshal.StructureToPtr(data1, ptArray[0], false);
Marshal.StructureToPtr(data2, ptArray[1], false);
IntPtr pt3 = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(IntPtr)) * 2);
Marshal.Copy(ptArray, 0, pt3, 2);
TestFunc(pt3, 2);
//释放
Marshal.FreeHGlobal(ptArray[0]);
Marshal.FreeHGlobal(ptArray[1]);
Marshal.FreeHGlobal(pt3); }
好了,现在我们解释下,我们在非托管堆上申请三块内存,然后把托管中创建的结构体,原样拷贝到ptArray[0], ptArray[1], 在这里使用的是 Marshal.StructureToPtr 。这相当于C++中的memcpy,由于 .net 知道托管对象的尺寸,所以我们不需要告诉它要复制多少字节。最后我们再把 ptArray 这个数组的元素拷贝到 pt3 指向的内存(该内存是一个能容纳两个指针 (8 bytes)的缓冲区),然后把 pt3 传递给DLL函数即可。
最后,不要忘记释放非托管堆上申请的内存,这时你的职责和C++程序员一样,必须自己对内存管理负责。
打开文本文件,我们即可看到我们在.net里初始化的内容,被写到文本文件中了:
-
101 hello
-
102 world
最后,再次强调下,注意细节。比如 Marshal.StructureToPtr 还有那些在托管代码中定义的等效结构体上的修饰,有些是原则性的固定的,例如:LayoutKind.Sequential。有些是没有固定原则的,比如托管中声明的导入函数的形式,参数类型等等,它们往往可能有多种定义和声明的方法,最终能殊途同归,这需要一定的经验和对底层的了解。
原文地址:http://blog.163.com/jinfd@126/blog/static/6233227720115296942623/