C#win32API编程(超详细)(转载)
C# win32 API编程
C# ⽤户经常提出两个问题:“我为什么要另外编写代码来使⽤内置于 Windows 中的功能?在框架中为什么没有相应的内容可以为我完成这⼀任务?”当框架⼩组构建他们的 .NET 部分时,他们评估了为使 .NET 程序员可以使⽤ Win32 ⽽需要完成的⼯作,结果发现 Win32 API 集⾮常庞⼤。他们没有⾜够的资源为所有 Win32 API 编写托管接⼝、加以测试并编写⽂档,因此只能优先处理最重要的部分。许多常⽤操作都有托管接⼝,但是还有许多完整的 Win32 部分没有托管接⼝。
平台调⽤ (P/Invoke) 是完成这⼀任务的最常⽤⽅法。要使⽤ P/Invoke,您可以编写⼀个描述如何调⽤函数的原型,然后运⾏时将使⽤此信息进⾏调⽤。另⼀种⽅法是使⽤ Managed Extensions to C++ 来包装函数,这部分内容将在以后的专栏中介绍。
要理解如何完成这⼀任务,最好的办法是通过⽰例。在某些⽰例中,我只给出了部分代码;完整的代码可以通过下载获得。
简单⽰例
在第⼀个⽰例中,我们将调⽤ Beep() API 来发出声⾳。⾸先,我需要为 Beep() 编写适当的定义。查看
MSDN 中的定义,我发现它具有以下原型:
BOOL Beep(
DWORD dwFreq, // 声⾳频率
DWORD dwDuration // 声⾳持续时间
);
要⽤ C# 来编写这⼀原型,需要将 Win32 类型转换成相应的 C# 类型。由于 DWORD 是 4 字节的整数,因此我们可以使⽤ int 或 uint 作为 C# 对应类型。由于 int 是 CLS 兼容类型(可以⽤于所有 .NET 语⾔),以此⽐ uint 更常⽤,并且在多数情况下,它们之间的区别并不重要。bool 类型与 BOOL 对应。现在我们可以⽤ C# 编写以下原型:
public static extern bool Beep(int frequency, int duration);
这是相当标准的定义,只不过我们使⽤了 extern 来指明该函数的实际代码在别处。此原型将告诉运⾏时如何调⽤函数;现在我们需要告诉它在何处到该函数。
我们需要回顾⼀下 MSDN 中的代码。在参考信息中,我们发现 Beep() 是在 kernel32.lib 中定义的。这意味着运⾏时代码包含在kernel32.dll 中。我们在原型中添加 DllImport 属性将这⼀信息告诉运⾏时:
[DllImport("kernel32.dll")]
这就是我们要做的全部⼯作。下⾯是⼀个完整的⽰例,它⽣成的随机声⾳在⼆⼗世纪六⼗年代的科幻电影中很常见。
using System;
using System.Runtime.InteropServices;
namespace Beep
{
class Class1
{
[DllImport("kernel32.dll")]
public static extern bool Beep(int frequency, int duration);
static void Main(string[] args)
{
Random random = new Random();
for (int i = 0; i < 10000; i++)
{
Beep(random.Next(10000), 100);
}
}
}
}
它的声响⾜以刺激任何听者!由于 DllImport 允许您调⽤ Win32 中的任何代码,因此就有可能调⽤恶意代码。所以您必须是完全受信任的⽤户,运⾏时才能进⾏ P/Invoke 调⽤。
枚举和常量
Beep() 可⽤于发出任意声⾳,但有时我们希望发出特定类型的声⾳,因此我们改⽤ MessageBeep()。MSDN 给出了以下原型:
BOOL MessageBeep(
UINT uType // 声⾳类型
);
这看起来很简单,但是从注释中可以发现两个有趣的事实。
⾸先,uType 参数实际上接受⼀组预先定义的常量。
其次,可能的参数值包括 -1,这意味着尽管它被定义为 uint 类型,但 int 会更加适合。
对于 uType 参数,使⽤ enum 类型是合乎情理的。MSDN 列出了已命名的常量,但没有就具体值给出
任何提⽰。由于这⼀点,我们需要查看实际的 API。
如果您安装了 Visual Studio? 和 C++,则 Platform SDK 位于 \Program Files\Microsoft Visual Studio .NET\Vc7\PlatformSDK\Include 下。
为查这些常量,我在该⽬录中执⾏了⼀个 findstr。
findstr "MB_ICONHAND" *.h
它确定了常量位于 winuser.h 中,然后我使⽤这些常量来创建我的 enum 和原型:
public enum BeepType
{
SimpleBeep = -1,
IconAsterisk = 0x00000040,
IconExclamation = 0x00000030,
IconHand = 0x00000010,
IconQuestion = 0x00000020,
Ok = 0x00000000,
}
[DllImport("user32.dll")]
public static extern bool MessageBeep(BeepType beepType);
现在我可以⽤下⾯的语句来调⽤它: MessageBeep(BeepType.IconQuestion);
处理结构
有时我需要确定我笔记本的电池状况。Win32 为此提供了电源管理函数。
搜索 MSDN 可以到 GetSystemPowerStatus() 函数。
BOOL GetSystemPowerStatus(
LPSYSTEM_POWER_STATUS lpSystemPowerStatus
)
;
此函数包含指向某个结构的指针,我们尚未对此进⾏过处理。要处理结构,我们需要⽤ C# 定义结构。我们从⾮托管的定义开始:
typedef struct _SYSTEM_POWER_STATUS {
BYTE ACLineStatus;
BYTE BatteryFlag;
BYTE BatteryLifePercent;
BYTE Reserved1;
DWORD BatteryLifeTime;
DWORD BatteryFullLifeTime;
} SYSTEM_POWER_STATUS, *LPSYSTEM_POWER_STATUS;
然后,通过⽤ C# 类型代替 C 类型来得到 C# 版本。
struct SystemPowerStatus
{
byte ACLineStatus;
byte batteryFlag;
byte batteryLifePercent;
byte reserved1;
int batteryLifeTime;
int batteryFullLifeTime;
}
这样,就可以⽅便地编写出 C# 原型:
[DllImport("kernel32.dll")]
public static extern bool GetSystemPowerStatus(
ref SystemPowerStatus systemPowerStatus);
在此原型中,我们⽤“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字符串数组怎么转成byte
{
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 函数。下⾯是⼀个⽰例:
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论