调用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对象,构造/析构它们并且在managed和unmanaged对象之间传递数数据。对于复杂的交互,用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属性
兄弟们,我很高兴告诉你们,我还没发现有什么“合法”的方式来marshal“char ****”,你们呢?事实依据如下:
※ 在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。要被marshal的class必须给出它的布局信息。在我们的这个例子中是没啥意义的,因为类中只有一个成员,但它是受限于MarshalAs属性class的。当然,我们必须看看,我们是如何在unmanged代码中将string看成是LPStr的。
现在我们声明这个函数:
[DllImport("TestDll.dll",EntryPoint="GetUserMessageHelper",CharSet=CharSet.Ansi)]
static extern void GetUserMessageHelper(
[MarshalAs(UnmanagedType.LPStruct)]
ref StringP str);
在本例中,EntryPoint和CharSet并不是必须的,因为我们详细讨论过,我们需要‘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存储区中分配了20个byte出来,它用“指向指针的指针的指针”调用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);
}
}
}
例2:GetUserName()
但是,你并不会每天都会碰上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:方法1—Marshal属性
开始时,你必须理解,我们不能这样定义:
[DllImport("Advapi32.dll"]
static extern bool GetUserName2Length(
[MarshalAs(UnmanagedType.LPStr)] string lpBuffer,
[MarshalAs(UnmanagedType.LPArray)] int& nSize );
原因如下:
※ 我们没有指针——int&是编译不通过的
※ 我们不能将lpBuffer 作为string来marshal,因为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组件
我将尽快撰文讨论这些议题!