博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

[转载来的]P/Invoke使用Win32非托管函数2

Posted on 2008-05-29 15:35  wuhang  阅读(303)  评论(0编辑  收藏  举报
  简单字符串

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

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 字符串,因此我采用了上述方法编码。