科技行者

行者学院 转型私董会 科技行者专题报道 网红大战科技行者

知识库

知识库 安全导航

至顶网软件频道基础软件在C#程序设计中使用Win32类库

在C#程序设计中使用Win32类库

  • 扫一扫
    分享文章到微信

  • 扫一扫
    关注官方公众号
    至顶头条

C# 用户经常提出两个问题:“我为什么要另外编写代码来使用内置于 Windows 中的功能?

作者:dtqgfnet 来源:CSDNBLOG 2007年11月13日

关键字:

  • 评论
  • 分享微博
  • 分享邮件
字符串

  虽然只有一种 .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() 这样的函数,其中的函数指针将被保存以便将来使用,您就需要确保在您的代码中引用委托。如果不这样做,函数可能表面上能执行,但在将来的内存回收处理中会删除委托,并且会出现错误。
    • 评论
    • 分享微博
    • 分享邮件
    邮件订阅

    如果您非常迫切的想了解IT领域最新产品与技术信息,那么订阅至顶网技术邮件将是您的最佳途径之一。

    重磅专题
    往期文章
    最新文章