微软的.NET框架的优点之一是它提供了独立于语言的开发平台。你可以在VB、C++、C#等语言中编写一些类,而在其它语言中使用(源于.NET中使用了CLS),你甚至可以从另一种语言编写的类中继承。但是你要是想调用以前的非托管DLL,那又会怎么样呢?你必须以某种方式将.NET对象转换为结构体、char *、函数指针等类型。这也就是说,你的参数必须被marshal(注:不知道中文名称该叫什么,英文中指的是为了某个目的而组织人或事物,参见这里,此处指的是为了调用非托管函数而进行的参数转换)。
C#中使用DLL函数之前,你必须使用DllImport声明要调用的函数:
public class Win32 {[DllImport("User32.Dll")]
public static extern void SetWindowText(int h, String s);// 函数原型为:BOOL SetWindowText(HWND hWnd, LPCTSTR lpString);
}
DllImport告诉编译器被调函数的入口在哪里,并且将该入口绑定到类中你声明的函数。你可以给这个类起任意的名字,我给它命名为Win32。你甚至可以将类放到命名空间中,具体参见图一。要编译Win32API.cs,输入:
csc /t:library /out:Win32API.dll Win32API.cs
这样你就拥有了Win32API.dll,并且你可以在任意的C#项目中使用它:
using Win32API;
int hwnd = // get it...String s = "I'm so cute."
Win32.SetWindowText(hwnd, s);
编译器知道去user32.dll中查找函数SetWindowText,并且在调用前自动将String转换为LPTSTR (TCHAR*)。很惊奇是吧!那么.NET是如何做到的呢?每种C#类型有一个默认的marshal类型,String对应LPTSTR。但你若是试着调用GetWindowText会怎么样呢(此处字符串作为out参数,而不是in参数)?它无法正常调用,是因为String是无法修改的,你必须使用StringBuilder:
using System.Text; // for StringBuilderpublic class Win32 {[DllImport("user32.dll")]
public static extern int GetWindowText(int hwnd,StringBuilder buf, int nMaxCount);
// 函数原型:int GetWindowText(HWND hWnd, LPTSTR lpString, int nMaxCount);
}
StringBuilder默认的marshal类型是LPTSTR,此时GetWindowText可以修改你的字符串:
int hwnd = // get it...StringBuilder cb = new StringBuilder(256);
Win32.GetWindowText(hwnd, sb, sb.Capacity);
如果默认的类型转换无法满足你的要求,比如调用函数GetClassName,它总是将参数转换为类型LPSTR (char *),即便在定义Unicode的情况下使用,CLR仍然会将你传递的参数转换为TCHAR类型。不过不用着急,你可以使用MarshalAs覆盖掉默认的类型:
[DllImport("user32.dll")]
public static extern int GetClassName(int hwnd,[MarshalAs(UnmanagedType.LPStr)] StringBuilder buf,int nMaxCount);
// 函数原型:int GetClassNameA(HWND hWnd, LPTSTR lpClassName, int nMaxCount);
这样当你调用GetClassName时,.NET将字符串作为ANSI字符传递,而不是宽字符。
结构体和回调函数类型的参数又是如何传递的呢?.NET有一种方法可以处理它们。举个简单的例子,GetWindowRect,这个函数获取窗口的屏幕坐标,C++中我们这么处理:
// in C/C++
RECT rc;HWND hwnd = FindWindow("foo",NULL);
::GetWindowRect(hwnd, &rc);
你可以使用C#结构体,只需使用另外一种C#属性StructLayout:
[StructLayout(LayoutKind.Sequential)]public struct RECT {public int left;public int top;public int right;public int bottom;}
一旦你定义了上面的结构体,你可以使用下面的函数声明形式 :
[DllImport("user32.dll")]
public static extern int
GetWindowRect(int hwnd, ref RECT rc);
// 函数原型:BOOL GetWindowRect(HWND hWnd, LPRECT lpRect);
使用ref标识很重要,以至于CLR(通用语言运行时)将RECT变量作为引用传递到函数中,而不是无意义的栈拷贝。定义了GetWindowRect之后,你就可以采用下面的方式调用:
RECT rc = new RECT();
int hwnd = // get it ...Win32.GetWindowRect(hwnd, ref rc);
注意你同样需要像声明中的那样使用ref关键字。C#结构体默认的marshal类型是LPStruct,因此没有必要使用MarshalAs。但如果你使用了类RECT而不是结构体RECT,那么你必须使用如下的声明形式:
// if RECT is a class, not struct
[DllImport("user32.dll")]
public static extern intGetWindowRect(int hwnd,
[MarshalAs(UnmanagedType.LPStruct)] RECT rc);
C#和C++一样,一件事情有很多中实现方式。System.Drawing中已经有Rectangle结构体,用来处理矩形,那有为什么要“重新发明轮子”呢?
[DllImport("user32.dll")]
public static extern int GetWindowRect(int hwnd, ref Rectangle rc);
最后,又是怎样从C#中传递回调函数到非托管代码中的呢?你所要做的就是委托(delegate)。
delegate bool EnumWindowsCB(int hwnd, int lparam);
一旦你声明了你的回调函数,那么你需要调用的函数声明为:
[DllImport("user32")]
public static extern intEnumWindows(EnumWindowsCB cb, int lparam);
// 函数原型:BOOL EnumWindows(WNDENUMPROC lpEnumFunc, LPARAM lParam);
由于上面的委托仅仅是声明了委托类型,你需要在你的类中提供实际的回调函数代码。
// in your class
public static bool MyEWP(int hwnd, int lparam) {// do something
return true;}
然后传递给相应的委托变量:
EnumWindowsCB cb = new EnumWindowsCB(MyEWP);
Win32.EnumWindows(cb, 0);
你可能注意到参数lparam。在C语言中,如果你传递参数LPARAM给EnumWindows,Windows将它作为参数调用你的回调函数。通常lparam是包含了你要做的事情的上下文结构体或类指针,记住,在.NET中没有指针的概念!那该怎么做呢?上面的例子中,你可以申明lparam为IntPtr类型,并且使用GCHandle来封装它:
// lparam is IntPtr now
delegate bool EnumWindowsCB(int hwnd, IntPtr lparam);// wrap object in GCHandle
MyClass obj = new MyClass();
GCHandle gch = GCHandle.Alloc(obj);EnumWindowsCB cb = new EnumWindowsCB(MyEWP);
Win32.EnumWindows(cb, (IntPtr)gch);gch.Free();
不要忘了使用完之后手动释放它!有时,你需要按照以前那种方式在C#中释放内存。可以使用GCHandle.Target的方式在你的回调函数中使用“指针”。
public static bool MyEWP(int hwnd, IntPtr param) {GCHandle gch = (GCHandle)param;MyClass c = (MyClass)gch.Target;// ... use it
return true;}
图2是将EnumWindows封装到数组中的类。你只需要按如下的方式使用即可,而不要纠结于委托和回调中。
WindowArray wins = new WindowArray();
foreach (int hwnd in wins) {// do something
}
关于委托代码和非委托代码之间交互的更多内容,你可以参考.NET文档中的“平台调用教程”。
参考资料:
1. C#中调用非托管的DLL