C#中调用非托管的DLL及参数传递 (一)

来源:互联网 发布:js shift 多选 编辑:程序博客网 时间:2024/06/06 04:12
微软的.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 StringBuilder
public 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 int 
  GetWindowRect(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 int 
  EnumWindows(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
}
原创粉丝点击