如何在C#中使用 Win32和其他库[2]

[入库:2005年8月18日] [更新:2007年3月24日]

本文简介:选择自 dtqgfnet 的 blog


  在此原型中,我们用“ref”指明将传递结构指针而不是结构值。这是处理通过指针传递的结构的一般方法。

  此函数运行良好,但是最好将 aclinestatus 和 batteryflag 字段定义为 enum:

  enum aclinestatus: byte
  {
   offline = 0,
   online = 1,
   unknown = 255,
  }

  enum batteryflag: byte
  {
   high = 1,
   low = 2,
   critical = 4,
   charging = 8,
   nosystembattery = 128,
   unknown = 255,
  }

  请注意,由于结构的字段是一些字节,因此我们使用 byte 作为该 enum 的基本类型。

  字符串

  虽然只有一种 .net 字符串类型,但这种字符串类型在非托管应用中却有几项独特之处。可以使用具有内嵌字符数组的字符指针和结构,其中每个数组都需要正确的封送处理。

  在 win32 中还有两种不同的字符串表示:

  ansi
  unicode

  最初的 windows 使用单字节字符,这样可以节省存储空间,但在处理很多语言时都需要复杂的多字节编码。windows nt? 出现后,它使用双字节的 unicode 编码。为解决这一差别,win32 api 采用了非常聪明的做法。它定义了 tchar 类型,该类型在 win9x 平台上是单字节字符,在 winnt 平台上是双字节 unicode 字符。对于每个接受字符串或结构(其中包含字符数据)的函数,win32 api 均定义了该结构的两种版本,用 a 后缀指明 ansi 编码,用 w 指明 wide 编码(即 unicode)。如果您将 c++ 程序编译为单字节,会获得 a 变体,如果编译为 unicode,则获得 w 变体。win9x 平台包含 ansi 版本,而 winnt 平台则包含 w 版本。

  由于 p/invoke 的设计者不想让您为所在的平台操心,因此他们提供了内置的支持来自动使用 a 或 w 版本。如果您调用的函数不存在,互操作层将为您查找并使用 a 或 w 版本。

  通过示例能够很好地说明字符串支持的一些精妙之处。
 简单字符串

  下面是一个接受字符串参数的函数的简单示例:

bool getdiskfreespace(
lpctstr lprootpathname,     // 根路径
lpdword lpsectorspercluster,  // 每个簇的扇区数
lpdword lpbytespersector,    // 每个扇区的字节数
lpdword lpnumberoffreeclusters, // 可用的扇区数
lpdword lptotalnumberofclusters // 扇区总数
);

  根路径定义为 lpctstr。这是独立于平台的字符串指针。

  由于不存在名为 getdiskfreespace() 的函数,封送拆收器将自动查找“a”或“w”变体,并调用相应的函数。我们使用一个属性来告诉封送拆收器,api 所要求的字符串类型。

  以下是该函数的完整定义,就象我开始定义的那样:

[dllimport("kernel32.dll")]
static extern bool getdiskfreespace(
 [marshalas(unmanagedtype.lptstr)]
 string rootpathname,
  ref int sectorspercluster,
  ref int bytespersector,
  ref int numberoffreeclusters,
  ref int totalnumberofclusters);


  不幸的是,当我试图运行时,该函数不能执行。问题在于,无论我们在哪个平台上,封送拆收器在默认情况下都试图查找 api 的 ansi 版本,由于 lptstr 意味着在 windows nt 平台上会使用 unicode 字符串,因此试图用 unicode 字符串来调用 ansi 函数就会失败。

有两种方法可以解决这个问题:一种简单的方法是删除 marshalas 属性。如果这样做,将始终调用该函数的 a 版本,如果在您所涉及的所有平台上都有这种版本,这是个很好的方法。但是,这会降低代码的执行速度,因为封送拆收器要将 .net 字符串从 unicode 转换为多字节,然后调用函数的 a 版本(将字符串转换回 unicode),最后调用函数的 w 版本。

  要避免出现这种情况,您需要告诉封送拆收器,要它在 win9x 平台上时查找 a 版本,而在 nt 平台上时查找 w 版本。要实现这一目的,可以将 charset 设置为 dllimport 属性的一部分:

  [dllimport("kernel32.dll", charset = charset.auto)]

  在我的非正式计时测试中,我发现这一做法比前一种方法快了大约百分之五。

  对于大多数 win32 api,都可以对字符串类型设置 charset 属性并使用 lptstr。但是,还有一些不采用 a/w 机制的函数,对于这些函数必须采取不同的方法。

 字符串缓冲区

  .net 中的字符串类型是不可改变的类型,这意味着它的值将永远保持不变。对于要将字符串值复制到字符串缓冲区的函数,字符串将无效。这样做至少会破坏由封送拆收器在转换字符串时创建的临时缓冲区;严重时会破坏托管堆,而这通常会导致错误的发生。无论哪种情况都不可能获得正确的返回值。

  要解决此问题,我们需要使用其他类型。stringbuilder 类型就是被设计为用作缓冲区的,我们将使用它来代替字符串。下面是一个示例:

[dllimport("kernel32.dll", charset = charset.auto)]
public static extern int getshortpathname(
  [marshalas(unmanagedtype.lptstr)]
  string path,
  [marshalas(unmanagedtype.lptstr)]
  stringbuilder shortpath,
  int shortpathlength);

  使用此函数很简单:

stringbuilder shortpath = new stringbuilder(80);
int result = getshortpathname(
@"d:\test.jpg", shortpath, shortpath.capacity);
string s = shortpath.tostring();

  请注意,stringbuilder 的 capacity 传递的是缓冲区大小。

  具有内嵌字符数组的结构

  某些函数接受具有内嵌字符数组的结构。例如,gettimezoneinformation() 函数接受指向以下结构的指针:

typedef struct _time_zone_information {
  long    bias;
  wchar   standardname[ 32 ];
  systemtime standarddate;
  long    standardbias;
  wchar   daylightname[ 32 ];
  systemtime daylightdate;
  long    daylightbias;
} time_zone_information, *ptime_zone_information;

  在 c# 中使用它需要有两种结构。一种是 systemtime,它的设置很简单:

  struct systemtime
  {
   public short wyear;
   public short wmonth;
   public short wdayofweek;
   public short wday;
   public short whour;
   public short wminute;
   public short wsecond;
   public short wmilliseconds;
  }

  这里没有什么特别之处;另一种是 timezoneinformation,它的定义要复杂一些:

[structlayout(layoutkind.sequential, charset = charset.unicode)]
struct timezoneinformation
{
  public int bias;
  [marshalas(unmanagedtype.byvaltstr, sizeconst = 32)]
  public string standardname;
  systemtime standarddate;
  public int standardbias;
  [marshalas(unmanagedtype.byvaltstr, sizeconst = 32)]
  public string daylightname;
  systemtime daylightdate;
  public int daylightbias;
}

  此定义有两个重要的细节。第一个是 marshalas 属性:

  [marshalas(unmanagedtype.byvaltstr, sizeconst = 32)]

  查看 byvaltstr 的文档,我们发现该属性用于内嵌的字符数组;另一个是 sizeconst,它用于设置数组的大小。

  我在第一次编写这段代码时,遇到了执行引擎错误。通常这意味着部分互操作覆盖了某些内存,表明结构的大小存在错误。我使用 marshal.sizeof() 来获取所使用的封送拆收器的大小,结果是 108 字节。我进一步进行了调查,很快回忆起用于互操作的默认字符类型是 ansi 或单字节。而函数定义中的字符类型为 wchar,是双字节,因此导致了这一问题。

  我通过添加 structlayout 属性进行了更正。结构在默认情况下按顺序布局,这意味着所有字段都将以它们列出的顺序排列。charset 的值被设置为 unicode,以便始终使用正确的字符类型。

  经过这样处理后,该函数一切正常。您可能想知道我为什么不在此函数中使用 charset.auto。这是因为,它也没有 a 和 w 变体,而始终使用 unicode 字符串,因此我采用了上述方法编码。

具有回调的函数

  当 win32 函数需要返回多项数据时,通常都是通过回调机制来实现的。开发人员将函数指针传递给函数,然后针对每一项调用开发人员的函数。

  在 c# 中没有函数指针,而是使用“委托”,在调用 win32 函数时使用委托来代替函数指针。

  enumdesktops() 函数就是这类函数的一个示例:

bool enumdesktops(
 hwinsta hwinsta,       // 窗口实例的句柄
 desktopenumproc lpenumfunc, // 回调函数
 lparam lparam        // 用于回调函数的值
);

  hwinsta 类型由 intptr 代替,而 lparam 由 int 代替。desktopenumproc 所需的工作要多一些。下面是 msdn 中的定义:

bool callback enumdesktopproc(
 lptstr lpszdesktop, // 桌面名称
 lparam lparam    // 用户定义的值
);

  我们可以将它转换为以下委托:

delegate bool enumdesktopproc(
 [marshalas(unmanagedtype.lptstr)]
  string desktopname,
  int lparam);

  完成该定义后,我们可以为 enumdesktops() 编写以下定义:

[dllimport("user32.dll", charset = charset.auto)]
static extern bool enumdesktops(
  intptr windowstation,
  enumdesktopproc callback,
  int lparam);

  这样该函数就可以正常运行了。

  在互操作中使用委托时有个很重要的技巧:封送拆收器创建了指向委托的函数指针,该函数指针被传递给非托管函数。但是,封送拆收器无法确定非托管函数要使用函数指针做些什么,因此它假定函数指针只需在调用该函数时有效即可。

  结果是如果您调用诸如 setconsolectrlhandler() 这样的函数,其中的函数指针将被保存以便将来使用,您就需要确保在您的代码中引用委托。如果不这样做,函数可能表面上能执行,但在将来的内存回收处理中会删除委托,并且会出现错误。

  其他高级函数

  迄今为止我列出的示例都比较简单,但是还有很多更复杂的 win32 函数。下面是一个示例:

dword setentriesinacl(
 ulong ccountofexplicitentries,      // 项数
 pexplicit_access plistofexplicitentries, // 缓冲区
 pacl oldacl,               // 原始 acl
 pacl *newacl               // 新 acl
);

本文关键:如何在C#中使用 Win32和其他库
 

本站最佳浏览方式为 分辨率 1024x768 IE 6.0(或更高版本的 IE浏览器)

go top