一个Marshal.Copy的问题

    首先介绍下这个问题的背景,是来自很久前一个同事问我请教的问题,当时我也没搞清楚,还去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/

  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值