欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

从C#程序中调用非受管DLLs的方法

程序员文章站 2023-12-18 21:44:04
本文实例讲述了从c#程序中调用非受管dlls的方法。分享给大家供大家参考。具体方法如下: 前言: 从所周知,.net已经渐渐成为一种技术时尚,那么c#很自然也成为一种编...

本文实例讲述了从c#程序中调用非受管dlls的方法。分享给大家供大家参考。具体方法如下:

前言:

从所周知,.net已经渐渐成为一种技术时尚,那么c#很自然也成为一种编程时尚。如何利用浩如烟海的win32 api以及以前所编写的 win32 代码已经成为越来越多的c#程序员所关注的问题。本文将介绍如何从c#代码中调用非受管dlls。如果某个函数是一个带有串类型(char*)输出参数的win32 api 或者是dll输出函数,那么从c#中如何调用它呢?对于输入参数的情形问题到不大,但如何获取从参数中返回的串呢?此外,如何调用有结构(struct)和回调(callback)作为参数的函数,如getwindowsrect 和enumwindows?那我们又如何将参数从c++和mfc中转换成c# 所要的类型呢?下面就让我们来一一解决这些问题。

微软.net的一个最主要的优势是它提供一个语言无关的开发系统。我们可以用visual basic、c++、c#等等语言来编写类,然后在其它语言中使用,我们甚至可以用不同的语言来派生类。但是如何调用以前开发的非受管dll呢?方法是必须将.net对象转化成结构、char*以及c语言的指针。用行话说就是参数必须被列集(marshal)。说到列集,用一两句话也说不清楚。所幸的是实现列集并不要我们知道太多的东西。

为了从c# 中调用dll函数,首先必须要有一个声明,就象长期以来使用visual basic的程序员所做的那样,只不过在c#中使用的是dllimport关键字:

复制代码 代码如下:
using system.runtime.interopservices; // dllimport所在的名字空间
public class win32 {
  [dllimport("user32.dll")]
  public static extern void setwindowtext(int h, string s);
}

在c#中,dllimport关键字作用是告诉编译器入口点在哪里,并将打包函数捆绑在一个类中。我们可以为这类取任何名字,这里不妨将类名取为 win32。我们甚至可以将这个类放到一个名字空间中,就象下面的代码这样:

win32api.cs 源代码

复制代码 代码如下:
// win32api: 此为名字空间,打包所选的win32 api 函数
// 编译方法:
//    csc /t:library /out:win32api.dll win32api.cs
//
using system;
using system.drawing;
using system.text;
using system.runtime.interopservices;

/////////////////////////////////////////////////////////////////
// 包装win32 api函数的名字空间。想用哪个win32 api,往里添加即可。
//
namespace win32api {
   [structlayout(layoutkind.sequential)]
   public struct point {
      public point(int xx, int yy) { x=xx; y=yy; }
      public int x;
      public int y;
      public override string tostring() {
         string s = string.format("({0},{1})", x, y);
         return s;
      }
   }

   [structlayout(layoutkind.sequential)]
   public struct size {
      public size(int cxx, int cyy) { cx=cxx; cy=cyy; }
      public int cx;
      public int cy;
      public override string tostring() {
         string s = string.format("({0},{1})", cx, cy);
         return s;
      }
   }

   [structlayout(layoutkind.sequential)]
   public struct rect {
      public int left;
      public int top;
      public int right;
      public int bottom;
      public int width()      { return right - left; }
      public int height()     { return bottom - top; }
      public point topleft()  { return new point(left,top); }
      public size  size()     { return new size(width(), height()); }
      public override string tostring() {
         string s = string.format("{0}x{1}", topleft(), size());
         return s;
      }
   }

   public class win32 {
      [dllimport("user32.dll")]
      public static extern bool iswindowvisible(int hwnd);

      [dllimport("user32.dll")]
      public static extern int getwindowtext(int hwnd,
         stringbuilder buf, int nmaxcount);

      [dllimport("user32.dll")]
      public static extern int getclassname(int hwnd,
         [marshalas(unmanagedtype.lpstr)] stringbuilder buf,
         int nmaxcount);

      [dllimport("user32.dll")]
      public static extern int getwindowrect(int hwnd, ref rect rc);

      [dllimport("user32.dll")]
      // 注意,运行时知道如何列集一个矩形
      public static extern int getwindowrect(int hwnd, ref rectangle rc);
   }
}

用下面的命令行可以编译这段代码: csc /t:library /out:win32api.dll win32api.cs    
成功编译后,我们就有了一个可以在c#工程中使用的动态库了(win32api.dll)。

复制代码 代码如下:
using win32api;
int hwnd = // get it
string s = "i''''m so cute." ;
win32.setwindowtext(hwnd, s);

 
编译器知道在user32.dll中找到setwindowtext,并在调用前自动将串转换为lptstr (tchar*)。真是神奇!.net是如何实现的呢?其实,每一个c#类型都有一个缺省的列集类型。对于串来说,它的列集类型就是lptstr。但如果调用的是getwindowtext,它的串参数是一个输出参数,而非输入参数,因为串是不变的,再象上面这样处理就行不通了。我们可能一点都没有注意到,不论什么时候处理一个串时,都会创建一个新串。要想修改这个串,必须用stringbuilder:

复制代码 代码如下:
using system.text; // stringbuilder所在的名字空间

public class win32 {
  [dllimport("user32.dll")]
  public static extern int getwindowtext(int hwnd,
    stringbuilder buf, int nmaxcount);
}

stringbuilder缺省的列集类型是lptstr,但是getwindowtext现在可以修改实际的串。

复制代码 代码如下:
int hwnd = // get it
stringbuilder sb = new stringbuilder(256);
win32.getwindowtext(hwnd, sb, sb.capacity);

所以我们第一个问题的答案就是:使用stringbuilder。 前面讨论的方法固然可以行得通,但有一种情况没有考虑,那就是如果缺省的列集类型不是你想要的类型怎么办?例如想要调用getclassname,windows编程高手都知道,getclassname的参数与大多数其它的api函数的参数有所不同,它的串参数是lpstr (char*),甚至是unicode串。如果传递一个串,公共语言运行时(clr)将把它转换成tchars——是不是很糟啊!不用害怕,我们可以用marshalas来改写缺省的处理:

复制代码 代码如下:
[dllimport("user32.dll")]
public static extern int getclassname(int hwnd,
  [marshalas(unmanagedtype.lpstr)] stringbuilder buf,
  int nmaxcount);

现在我们调用getclassname,.net将串作为ansi字符传递,而不是宽字符,搞掂! 以上我们解决了如何获取函数载参数中返回的字符串。下面我们来看看结构参数和回调参数的情形。不用说,.net肯定有办法处理它们。就拿getwindowrect为例。这个函数用窗口屏幕坐标填充一个rect。

在c/c++中

复制代码 代码如下:
rect rc;
hwnd hwnd = findwindow("foo",null);
::getwindowrect(hwnd, &rc);     
在c#中如何调用呢?如何传递rect呢?方法是将它作为一个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);


 
注意这里用到了ref,这一点很重要,clr会将rect作为引用传递,以便函数可以修改我们的对象,而不是无名字的堆栈拷贝。定义了getwindowrect之后,我们可以象下面这样调用:

复制代码 代码如下:
rect rc = new rect();
int hwnd = // get it
win32.getwindowrect(hwnd, ref rc);


注意这里必须声明并使用ref——罗嗦!c# 结构的缺省列集类型还能是什么?——lpstruct,所以就不必再用marshalas了。但如果rect是个类,而非结构的话,那就必须象下面这样实现打包:

复制代码 代码如下:
// 如果rect 是个类,而不是结构
[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);

运行时既然已经知道如何将rectangle作为win32 rect进行列集。请注意,在实际的代码中就没有必要再调用getwindowrect(get/setwindowtext亦然),因为windows.forms.control类已具有这样的属性:用control.displayrectangle获取窗口矩形,用control.text设置/获取控件文本

复制代码 代码如下:
rectangle r = mywnd.displayrectangle;
mywnd.text = "i''''m so cute";

如果出于某种原因已知的是某个hwnd,而不是一个控件派生对象,那么只需要象示范的那样来打包api。以上我们已经搞掂了串、结构以及矩形……还有什么呢?对了,还有回调(callbacks)。如何将回调从c#传递到非受管代码呢?记住只要用委托(delegate)即可: delegate bool enumwindowscb(int hwnd,     int lparam);     
一旦声明了委托/回调类型,就可以象下面这样打包:

复制代码 代码如下:
[dllimport("user32")]
public static extern int
  enumwindows(enumwindowscb cb, int lparam);

 
上面的delegate仅仅是声明了一个委托类型,我们还必须在类中提供一个实际的委托实现:
 
复制代码 代码如下:
// 在类中
public static bool myewp(int hwnd, int lparam) {
  // do something
  return true;
}

然后对它进行打包处理:

复制代码 代码如下:
enumwindowscb cb = new enumwindowscb(myewp);
win32.enumwindows(cb, 0);

聪明的读者回注意到我们这里掩饰了lparam的问题,在c中,如果你给enumwindows一个lparam,则windows会用它通知回调函数。一般典型的lparam是一个结构或类指针,其中包含着我们需要的上下文信息。但是记住,在.net中绝对不能提到"指针"!那么如何做呢?这是可以将lparam声明为intptr并用gchandle对它进行打包:

复制代码 代码如下:
// 现在lparam 是 intptr
delegate bool enumwindowscb(int hwnd,     intptr lparam);

// 在gchandle中打包对象
myclass obj = new myclass();
gchandle gch = gchandle.alloc(obj);
enumwindowscb cb = new enumwindowscb(myewp);
   win32.enumwindows(cb, (intptr)gch);
   gch.free();

最后不要忘了调用free! c#中有时也需要与以往一样必须要我们自己释放占用的内存。为了存取载枚举器中的lparam"指针",必须使用

复制代码 代码如下:
gchandle.target。 public static bool myewp(int hwnd, intptr param) {
  gchandle gch = (gchandle)param;
  myclass c = (myclass)gch.target;
  //  use it
  return true;
}

下面是一个窗口数组类:

winarray.cs

复制代码 代码如下:
// winarray: 用enumwindows 产生顶层窗口的清单arraylist
//
using system;
using system.collections;
using system.runtime.interopservices;

namespace winarray {

   public class windowarray : arraylist {
      private delegate bool enumwindowscb(int hwnd, intptr param);

      // 这里声明的是private类型的委托,因为只有我使用它,其实没必要这样做。
      [dllimport("user32")]
      private static extern int enumwindows(enumwindowscb cb,
         intptr param);

      private static bool myenumwindowscb(int hwnd, intptr param) {
         gchandle gch = (gchandle)param;
         windowarray itw = (windowarray)gch.target;
         itw.add(hwnd);
         return true;
      }

      // 这是唯一的public 类型方法,你需要调用的唯一方法
      public windowarray() {
         gchandle gch = gchandle.alloc(this);
         enumwindowscb ewcb = new enumwindowscb(myenumwindowscb);
         enumwindows(ewcb, (intptr)gch);
         gch.free();
      }
   }
}

这个类将enumwindows封装在一个数组中,不用我们再去进行繁琐的委托和回调,我们可以象下面这样轻松使用这个类:

复制代码 代码如下:
windowarray wins = new windowarray();
foreach (int hwnd in wins) {
 // do something
}

是不是很帅啊!我们甚至还可以在受管c++中使用dllimport风格的包装类。在.net环境中,只要能进行相应的转换,便可以在受管和非受管世界之间随心所欲地聘驰, 大多数情况下的转换是自动的,不必关心太多的事情。需要进行marshalas或者打包gchandle的情况很少。有关c#和非受管c++之间的平台调用的其它细节问题,可以参考.net的有关文档。 下面是本文提供的一个带有开关的控制台小程序listwin。它的功能是列出所有顶层窗口,输出可以显示hwnds、窗口类名、窗口标题以及窗口矩形,用rect或rectangle。这些内容的显示可用开关控制。

希望本文所述对大家的c#程序设计有所帮助。

上一篇:

下一篇: