一个Marshal.Copy的问题

2019/10/16 11:59
阅读数 19

首先介绍下这个问题的背景,是来自很久前一个同事问我请教的问题,当时我也没搞清楚,还去88上问了下。现在我有些空余时间,在88上有看到了自己的提问,想想有必要研究清楚这个问题到底是怎么回事。

    其次我要对中文MSDN的文档表达以下不满,正是由于MSDN的中文文档对这个函数的介绍的语义比较模糊,不精确,才导致我当时无法理解清楚这个函数的设计用意和用途是什么。

    第三,我要顺便鄙视下.net的PInvoke和marshal机制,应该说用.net托管代码去调用非托管DLL,简直比单纯使用C/C++更痛苦。所以所有使用.net的同志,希望你有好运气,你一直不需要调用非托管代码!否则.net在内存上的模糊不清,和托管环境和native code之间的内存数据封送,一定会让你感到十分气恼,你需要控制那些你平时根本无法把握也不必了解的数据的内存布局,这根本就不是.net 想给予程序员的能力!

    现在就来看下这是个什么问题:有下面这样一些代码,这些代码是什么意思?

  1.  
    IntPtr[] ptArray = new IntPtr[1];
  2.  
     
  3.  
    ptArray[ 0] = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(TAX_ITEM)) * 6);
  4.  
    IntPtr pt = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(TAX_ITEM)));
  5.  
    Marshal.Copy(ptArray, 0, pt, 1);

    很显然,TAX_ITEM是一个struct。这个问题的核心是最后一行代码该如何理解呢。我们为此再看下这个Copy函数的MSDN说明:

 

  1.  
    public static void Copy (
  2.  
    IntPtr[] source,
  3.  
    int startIndex,
  4.  
    IntPtr destination,
  5.  
    int length
  6.  
    )

    “将数据从一维托管 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 代码:

//我们先定义一个结构体:

 

  1.  
    typedef struct _test_struct
  2.  
    {
  3.  
    int index;
  4.  
    char text[48];
  5.  
    } TEST_STRUCT, *LPTESTSTRUCT;

//再来定义一个使用上面的结构体指针数组为参数的测试函数:

//注意,我们还需要一个参数是count,因为指针数组参数无法表示自己含有多少元素。

//对每个元素,我们把它指向的数据内容写到一个文本文件里。

  1.  
    void WINAPI TestFunc(LPTESTSTRUCT* ppDatas, int count)
  2.  
    {
  3.  
    int i;
  4.  
    char line[96];
  5.  
    FILE* stream = _tfopen(_T( "C:\\TestMarshal.txt"), _T("w"));
  6.  
     
  7.  
    for(i = 0; i<count; i++)
  8.  
    {
  9.  
    sprintf(line, "%d %s\n", ppDatas->index, ppDatas->text);
  10.  
    fputs(line, stream);
  11.  
    }
  12.  
    fclose(stream);
  13.  
    }

//最后用一个.def文件导出这个函数:

 

  1.  
    LIBRARY "MyTestDll"
  2.  
    EXPORTS
  3.  
    TestFunc @ 1

-----------------------------------------------------------------下面是C# Console程序的代码

//现在我们用托管代码去调用上面的DLL,先做一些必要的准备:

注意,这些必要的属性修饰,他们使这个结构体的内存布局和前面C++中的代码完全一致

尤其是如何定义C++结构体中的char[48],取决于CharSet,SizeConst等。

 

  1.  
    [StructLayout(LayoutKind.Sequential, CharSet=CharSet.Ansi)]
  2.  
    public struct TestStruct
  3.  
    {
  4.  
    public int index;
  5.  
    [MarshalAs(UnmanagedType.ByValTStr, SizeConst= 48)]
  6.  
    public string text;
  7.  
    }

               现在就是测试代码的主体:

 

  1.  
    [DllImport( "<pre name="code" class="cpp">[StructLayout(LayoutKind.Sequential, CharSet=CharSet.Ansi)]
  2.  
    public struct TestStruct
  3.  
    {
  4.  
    public int index;
  5.  
    [MarshalAs(UnmanagedType.ByValTStr, SizeConst=48)]
  6.  
    public string text;
  7.  
    }


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里初始化的内容,被写到文本文件中了:

 

  1.  
    101 hello
  2.  
    102 world

    最后,再次强调下,注意细节。比如 Marshal.StructureToPtr 还有那些在托管代码中定义的等效结构体上的修饰,有些是原则性的固定的,例如:LayoutKind.Sequential。有些是没有固定原则的,比如托管中声明的导入函数的形式,参数类型等等,它们往往可能有多种定义和声明的方法,最终能殊途同归,这需要一定的经验和对底层的了解。

原文地址:http://blog.163.com/jinfd@126/blog/static/6233227720115296942623/

展开阅读全文
打赏
0
0 收藏
分享
加载中
更多评论
打赏
0 评论
0 收藏
0
分享
返回顶部
顶部