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

?????? 微软的.NET框架的优点之一是它提供了独立于语言的开发平台。你可以在VBC++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语言中,如果你传递参数LPARAMEnumWindowsWindows将它作为参数调用你的回调函数。通常lparam是包含了你要做的事情的上下文结构体或类指针,记住,在.NET中没有指针的概念!那该怎么做呢?上面的例子中,你可以申明lparamIntPtr类型,并且使用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
}

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值