EDA驿站

去浮躁,求真知;重实干,勤与研

  博客园  :: 首页  :: 新随笔  ::  ::  :: 管理

调用unmanaged代码 第二部分——Marshal Class

在前一部分我们看到,我们是如何与unmanaged代码交互的并且给出了一个简单的例子。我们还发现了MarshalAs属性,它帮助我们应付一堆不同的unmanaged类型,例如LPStruct,LPArray,LPWStr等。

但是,你会问,这么多有可能出现的unmanaged类型,什么时候是个头呀?(毕竟,不同类型的MarshalAs变量是有限的)。那么,如何来marshal任何类型的unmanaged类型呢?如何管理非标准的应用定义类型,像char ****(指向字符串指针的指针的指针)?又或,再复杂一点的情况呢?

我可以给出3种可能的方式来在C#中解决这个问题:

1. MarshalAs属性

       在外部函数声明中,联合几个不同的用包含metadata的复杂的metadata属性特别定义的managed用户类型。在大多数情况下,这种方式表现良好。这种方式可以说是即干净又利落,但是在一些极为复杂的场合,它也是无能无力。

2. UnSafe代码

用同unmanaged库中一样的类型编写unsafe的外部函数声明并从unsafe代码中调用这些函数。这种方式倒是适用于任何情况,实现起来也很容易。不过,最好避免使用这种方式,毕竟,“unsafe”代码是不安全的嘛。

3. Marshal Class

有一个特别的类,它可以帮我们处理所有可能的与unmanaged代码的交互。在某些情况下,这并不好使用,但当你别无选择之时——这却是个最好的选择。它可以处理任何unmanaged对象,构造/析构它们并且在managedunmanaged对象之间传递数数据。对于复杂的交互,用Marshal class较好,不过MarshalAs属性是首选。

结论:

当你有几个函数需要以unmanaged代码方式调用时,首先试试用MarshalAs属性。对于一些简单的交互,这种方式易用、工作良好并且不会产生令人奇怪的感觉,因为它才是最标准的机制。对于一些复杂的交互,首先试着用第一种方式,当问题过于复杂或者已经花了很长时间来找一个合适的属性的联合,试试看第二种方式——这种方式足够简单且有更多的marshal,但是这种情况下,程序员得强制allocate/deallocate/copy unmanaged内存,这可能是不安全的。当然,第二种方式常常是开放的,小心使用就是了。

讲了那么多的理论,下面看几个例子吧。

例子1:打印char ****

想像我在C++中写了如下的unmanaged DLL,例如下面的代码:

extern "C" __declspec(dllexport) void GetUserMessage(char**** Message)

{

      strcpy(***Message,"Hello unmanaged");

}

再想像我们可以在C#中调用这个函数并在一个message Box中显示这串漂亮的字符串。应该指出,在我们调用这个函数之前,我们必须分配合适的内存空间,调用完之后,需将这些内存释放。

下面就让我们一个接一个地试试上述的三种方法吧。

1:方法1——MarshalAs属性

兄弟们,我很高兴告诉你们,我还没发现有什么“合法”的方式来marshalchar ****”,你们呢?事实依据如下:

    Beta1中有这样的方法(定义多重嵌套类),但在Beta2中没有

    没有方法来marshalchar ****

好吧,我们需要定义某个辅助方法,它将转型一层指针(cast down one level)

extern "C" __declspec(dllexport) void GetUserMessageHelper(char**** Message)

{   

      GetUserMessage(&Message);

}

现在,我们可以写一个.NET声明:

[StructLayout(LayoutKind.Sequential)]

public class StringP

{

      [MarshalAs(UnmanagedType.LPStr)]

      public string str;

}

这一声明将增加一层indirectance。注意,我们定义的是class,而不是struct,那是由于对于struct,我们只能通过值来marshal,而class可通过引用(reference)来marshal。要被marshalclass必须给出它的布局信息。在我们的这个例子中是没啥意义的,因为类中只有一个成员,但它是受限于MarshalAs属性class的。当然,我们必须看看,我们是如何在unmanged代码中将string看成是LPStr的。

现在我们声明这个函数:

[DllImport("TestDll.dll",EntryPoint="GetUserMessageHelper",CharSet=CharSet.Ansi)]

static extern void GetUserMessageHelper(

[MarshalAs(UnmanagedType.LPStruct)]

ref StringP str);

在本例中,EntryPointCharSet并不是必须的,因为我们详细讨论过,我们需要‘LPStr’并且定义我们是函数是“extern ‘c’”,重要的是我们定义参数为‘ref’意思是输入和输出(它增加了最后的,第三层indirectance),也说是,我们想用引用(reference)——LPStruct,而非值来marshal我们的class

这样的代码是可行的,仅仅如此调用:

private void button1_Click(object sender, System.EventArgs e)

{

 StringP strP=new StringP();

 strP.str=new string('*',20);

 GetUserMessageHelper(ref strP);

 MessageBox.Show(strP.str);

}

并不简单,但运行良好,不是吗?

1:方法2——unsafe代码

让我们定义我们的unsafe的外部函数:

[DllImport("TestDll.dll",EntryPoint="GetUserMessage")]

unsafe static extern void GetUserMessage2(byte**** str);

[DllImport("user32.dll")]

unsafe static extern int MessageBoxA(int hwnd,byte* Text,byte* Caption,int Type);

我定义了像“byte ****”这样的参数,而非“char ****”因为managed char是两个字节长。

我还明确说过,我的函数是unsafe的,否则我就不能使用指针了。

我还输出了一个MessageBox函数,因为managedBox类并不知如何操作byte指针,仅会操作managed string类。现可以这样用:

unsafe private void button2_Click(object sender, System.EventArgs e)

{

 byte* str=(byte*)Marshal.AllocCoTaskMem(20);

 byte** strP=&str;

 byte*** strPP=&strP;

 GetUserMessage2(&strPP);

 MessageBoxA(0,str,(byte*)0,0);

 Marshal.FreeCoTaskMem((IntPtr)str);

}

我使用Marshal class进行内存分配。作为一个好的样板,我必须从"ole32.dll"中输出"CoTaskMemAlloc" "CoTaskMemFree" 来,并使用它们。可我太懒了,请原谅我仅用了Marshal class。那这会发什么什么呢?我从unmanaged存储区中分配了20byte出来,它用“指向指针的指针的指针”调用GetUserMessage,利用结果并释放空间。非常,非常简单,正如所有unmanaged操作,它期待开发人员小心使用。

1:方法3——Marshal class

[DllImport("TestDll.dll",EntryPoint="GetUserMessage")]

static extern void GetUserMessage3(ref IntPtr str);

IntPtr正是系统定义的整数(就像C++中的int)。对Win32,它是4个字节长。

下面让我们使用Marshal class吧:

private void button3_Click(object sender, System.EventArgs e)

{

 // Ok, of course firstly allocate memory

IntPtr str=Marshal.AllocCoTaskMem(20);

 

// add level of indirection

IntPtr strP=Marshal.AllocCoTaskMem(4);

Marshal.StructureToPtr(str,strP,false);

 

// add one more level of indirection

IntPtr strPP=Marshal.AllocCoTaskMem(4);

Marshal.StructureToPtr(strP,strPP,false);

 

// call it

GetUserMessage3(ref strPP);

          

// remove 2 levels of indirection

strP=Marshal.ReadIntPtr(strPP);

            str=Marshal.ReadIntPtr(strP);

 

// get string class from pointer

MessageBox.Show(Marshal.PtrToStringAnsi(str));

 

// Free used memory

Marshal.FreeCoTaskMem(str);

Marshal.FreeCoTaskMem(strP);

Marshal.FreeCoTaskMem(strPP);

}

当然,唯有这样的代码是没有像C++中的‘&’一样的‘取址’运算符的。因此我们使用Marshal class来获取此功能。为了增加指针级别,我们必须为新的指针分配空间并要求Marshal class将这些指针放置到给定的位置。

这样的代码比起unsafe代码而言要安全多了。

下面是该例的全部代码,它表明了marshaling ‘char ****’的三种方法;

using System;

using System.Drawing;

using System.Collections;

using System.ComponentModel;

using System.Windows.Forms;

using System.Data;

using System.Runtime.InteropServices;

using System.Text;

 

namespace WindowsApplication8

{

      ///

      /// Summary description for Form1.

      ///

      public class Form1 : System.Windows.Forms.Form

      {

            private System.Windows.Forms.Button button1;

            private System.Windows.Forms.Button button2;

            private System.Windows.Forms.Button button3;

           ///

           /// Required designer variable.

           ///

            private System.ComponentModel.Container components = null;

 

           public Form1()

           {

                 //

                 // Required for Windows Form Designer support

                 //

                 InitializeComponent();

 

                 //

                 // TODO: Add any constructor code after InitializeComponent call

                 //

           }

 

           ///

           /// Clean up any resources being used.

           ///

            protected override void Dispose( bool disposing )

           {

                 if( disposing )

                 {

                       if (components != null)

                       {

                             components.Dispose();

                       }

                 }

                 base.Dispose( disposing );

           }

 

            #region Windows Form Designer generated code

           ///

            /// Required method for Designer support - do not modify

           /// the contents of this method with the code editor.

           ///

            private void InitializeComponent()

           {

                 this.button1 = new System.Windows.Forms.Button();

                 this.button2 = new System.Windows.Forms.Button();

                 this.button3 = new System.Windows.Forms.Button();

                 this.SuspendLayout();

                 //

                 // button1

                 //

                 this.button1.Location = new System.Drawing.Point(64, 40);

                 this.button1.Name = "button1";

                 this.button1.Size = new System.Drawing.Size(120, 56);

                 this.button1.TabIndex = 0;

                 this.button1.Text = "MarshalAs attribute";

                 this.button1.Click += new System.EventHandler(this.button1_Click);

                 //

                 // button2

                 //

                 this.button2.Location = new System.Drawing.Point(64, 112);

                 this.button2.Name = "button2";

                 this.button2.Size = new System.Drawing.Size(120, 56);

                 this.button2.TabIndex = 0;

                 this.button2.Text = "Unsafe code";

                 this.button2.Click += new System.EventHandler(this.button2_Click);

                 //

                 // button3

                 //

                 this.button3.Location = new System.Drawing.Point(64, 184);

                 this.button3.Name = "button3";

                 this.button3.Size = new System.Drawing.Size(120, 56);

                 this.button3.TabIndex = 0;

                 this.button3.Text = "Marshal class";

                 this.button3.Click += new System.EventHandler(this.button3_Click);

                 //

                 // Form1

                 //

                 this.AutoScaleBaseSize = new System.Drawing.Size(6, 15);

                 this.ClientSize = new System.Drawing.Size(240, 282);

                 this.Controls.AddRange(new System.Windows.Forms.Control[] {

                 this.button3,

                 this.button2,

                 this.button1});

                 this.Name = "Form1";

                 this.Text = "Form1";

                 this.ResumeLayout(false);

 

           }

            #endregion

 

           ///

           /// The main entry point for the application.

           ///

            [STAThread]

           static void Main()

           {

                 Application.Run(new Form1());

           }

 

            [StructLayout(LayoutKind.Sequential)]

           public class StringP

           {

                 [MarshalAs(UnmanagedType.LPStr)]

                 public string str;

           }

                [DllImport("TestDll.dll",EntryPoint="GetUserMessageHelper",CharSet=CharSet.Ansi)]

           static extern void GetUserMessageHelper(

                 [MarshalAs(UnmanagedType.LPStruct)]

                 ref StringP str);

            private void button1_Click(object sender, System.EventArgs e)

           {

                 StringP strP=new StringP();

                 strP.str=new string('*',20);

                 GetUserMessageHelper(ref strP);

                 MessageBox.Show(strP.str);

           }

 

            [DllImport("TestDll.dll",EntryPoint="GetUserMessage")]

            unsafe static extern void GetUserMessage2(byte**** str);

            [DllImport("user32.dll")]

            unsafe static extern int MessageBoxA(int hwnd,byte* Text,byte* Caption,int Type);

            unsafe private void button2_Click(object sender, System.EventArgs e)

           {

                 byte* str=(byte*)Marshal.AllocCoTaskMem(20);

                 byte** strP=&str;

                 byte*** strPP=&strP;

                 GetUserMessage2(&strPP);

                 MessageBoxA(0,str,(byte*)0,0);

                 Marshal.FreeCoTaskMem((IntPtr)str);

           }

 

            [DllImport("TestDll.dll",EntryPoint="GetUserMessage")]

           static extern void GetUserMessage3(ref IntPtr str);

            private void button3_Click(object sender, System.EventArgs e)

           {

                 IntPtr str=Marshal.AllocCoTaskMem(20);

                 IntPtr strP=Marshal.AllocCoTaskMem(4);

                 Marshal.StructureToPtr(str,strP,false);

                 IntPtr strPP=Marshal.AllocCoTaskMem(4);

                 Marshal.StructureToPtr(strP,strPP,false);

 

                 GetUserMessage3(ref strPP);

                

                 strP=Marshal.ReadIntPtr(strPP);

                 str=Marshal.ReadIntPtr(strP);

                  MessageBox.Show(Marshal.PtrToStringAnsi(str));

 

                 Marshal.FreeCoTaskMem(str);

                 Marshal.FreeCoTaskMem(strP);

                 Marshal.FreeCoTaskMem(strPP);

           }

      }

}

 

2GetUserName()

但是,你并不会每天都会碰上marshal ‘char ****’的问题。让我们看一个更为有意义的例子。试着想像你非常想知道当前登录入系统的用户名。到目前为止,我都不知道managed代码是怎样解决这个问题的。因此我们必须调用Windows API了。正如我们在MSDN中所见,这里有函数的帮助:

BOOL GetUserName(

        LPTSTR lpBuffer,  // name buffer

        LPDWORD nSize     // size of name buffer

      );

这个函数希望我,一个调用者,提供一个ANSI string来保存当前录入的用户名。用户名的长度将反映在第二个参数中,并以指针类型给出。当然,在调用函数之前,我不知道我需要多少空间。就因为此,我必须调用GetUserName()两次:

1. GetUserName(NULL,&Size); // Size is DWORD type

本次调用之后,Size将保存我所需要的buffer的长度。在我分配好buffer之后,我第二次调用该函数:

2. GetUserName(pointer_to_buffer,&Size);

此时,该指针buffer包含了所需的用户名。现在我们可以看到,我们是如何用前述的3种方法实现这个API调用的。然后,再见了:

2:方法1Marshal属性

开始时,你必须理解,我们不能这样定义:

[DllImport("Advapi32.dll"]

static extern bool GetUserName2Length(

[MarshalAs(UnmanagedType.LPStr)] string lpBuffer,

[MarshalAs(UnmanagedType.LPArray)] int& nSize );

原因如下:

       我们没有指针——int&是编译不通过的

       我们不能将lpBuffer 作为stringmarshal,因为marshaler推断它是通过值传递的,因此它将仅将string作为‘in’参数进行marshal,因此,调用之后,lpBuffer仍会保持不变。我了获取字符串,我们必须声明lpBuffer为‘out’或‘ref’,但在本例中我们将得到一个指向string指针的指针,然而GetUserName()要的是指针。

       在第一次调用时,我们必须marshall NULL,或者0,不像C++中,(char *0是可以的,这里并没有任何方式可以将0转型为string

这样的话,我们能做什么呢?我们将写两个声明:

[DllImport("Advapi32.dll", EntryPoint="GetUserName",

ExactSpelling=false, CharSet=CharSet.Ansi, SetLastError=true)]

static extern bool GetUserName2(

[MarshalAs(UnmanagedType.LPArray)] byte[] lpBuffer,

[MarshalAs(UnmanagedType.LPArray)] Int32[] nSize );

 

[DllImport("Advapi32.dll", EntryPoint="GetUserName",

ExactSpelling=false, CharSet=CharSet.Ansi,SetLastError=true)]

static extern bool GetUserName2Length(

[MarshalAs(UnmanagedType.I4)] int lpBuffer,

[MarshalAs(UnmanagedType.LPArray)] Int32[] nSize );

第一个参数将被作为‘int’来为第一次调用marshal,第二次调用时为byte array。第二个参数仅仅是int array(我我们例子中,它将仅包含一个成员)。在此这后的调用就简单了:

private void button2_Click(object sender, System.EventArgs e)

{

      // allocate 1-member array for length

      int[] len=new int[1];

      len[0]=0;

 

      // get length

      GetUserName2Length(0,len);

 

      // allocate byte array for ANSI string

      byte[] str=new byte[len[0]];

 

      // Get username to preallocated buffer

      GetUserName2(str,len);                

 

      // Use text decoder to get string from byte array of ASCII symbols

      MessageBox.Show(System.Text.Encoding.ASCII.GetString(str));

}

2:方法2——unsafe代码

在这种方式下,声明是很自然的:

[DllImport("Advapi32.dll", EntryPoint="GetUserName",

ExactSpelling=false, CharSet=CharSet.Ansi,SetLastError=true)]

unsafe static extern bool GetUserName3(byte* lpBuffer,int* nSize );

我们还需要MessageBox()声明来显示它:

unsafe static extern int MessageBoxA(int hwnd,byte* Text,byte* Caption,int Type);

可以很像在C++中的那样调用它:

unsafe private void button3_Click(object sender, System.EventArgs e)

{

      int Size=0;

      GetUserName3((byte*)0,&Size);

      byte* buffer=(byte*)Marshal.AllocCoTaskMem(Size);

      GetUserName3(buffer,&Size);

      MessageBoxA(0,buffer,(byte*)0,0);

      Marshal.FreeCoTaskMem((IntPtr)buffer);

}

我使用Marshal class进行空间分配,原因和char ****的例子一样:我已经没有权力声明外部的分配算符并使用它们。

2:方法3——Marshal class

当我们用Marshal class时,所有的有疑问的参数必须定义为IntPtr

回记一下,IntPtr不是一个指针类,它仅仅是一个平台相关的整数,就像是C++中的int

[DllImport("Advapi32.dll", EntryPoint="GetUserName", ExactSpelling=false,

CharSet=CharSet.Ansi,SetLastError=true)]

      static extern bool GetUserName1(

      IntPtr lpBuffer,

      [MarshalAs(UnmanagedType.LPArray)] Int32[] nSize );

          

      private void button1_Click(object sender, System.EventArgs e)

      {

           string str;

            Int32[] len=new Int32[1];

            len[0]=0;

            GetUserName1((IntPtr)0,len);

           IntPtr ptr=Marshal.AllocCoTaskMem(len[0]);

            GetUserName1(ptr,len);

            str=Marshal.PtrToStringAnsi(ptr);

            Marshal.FreeCoTaskMem(ptr);

            MessageBox.Show(str);

      }

正如你所见到的,我将0转型成IntPtr——这是没问题的,因为是值类型。等我知道了长度,我就可以分配必须的空间,然后再调用GetUserName(),要求Marshal由指向ASCII string的指针中获取string,释放空间。对于这样的问题使用Marshal是相当棒的。

在这个第二部分,我们讨论了在.NET中调用DLL库。而如何:

       调用COM服务

       unmanged代码中调用managed组件

我将尽快撰文讨论这些议题!

 

 

 

posted on 2004-06-17 17:44  易学  阅读(1893)  评论(1编辑  收藏  举报