在此原型中,我们用“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 ); |