如何在C#中使用 Win32和其他庫 (轉)

如何在C#中使用 Win32和其他庫
這一次我們將深入探討如何在  C#  中使用  Win32  和其他現有庫。 

  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); 

 


 


http://www.AspxBoy.Com


 


--------------------------------------------------------------------------------
    Joined:: 2003-11   Posts: 886  Integral: 10981   State: offline  5 4 3 2 1 0 -1 -2 -3 -4 -5    1   
 

Huobazi

 

靶子哥哥

來自:  Post by: 2005-4-19 10:04:02                                       
--------------------------------------------------------------------------------

Re:如何在C#中使用 Win32和其他庫
處理結構 

  有時我需要確定我筆記本的電池狀況。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  版本。 

  通過示例能夠很好地說明字符串支持的一些精妙之處。 


 


http://www.AspxBoy.Com


 


--------------------------------------------------------------------------------
    Joined:: 2003-11   Posts: 886  Integral: 10981   State: offline  5 4 3 2 1 0 -1 -2 -3 -4 -5    2   
 

Huobazi

 

靶子哥哥

來自:  Post by: 2005-4-19 10:04:29                                       
--------------------------------------------------------------------------------

Re:如何在C#中使用 Win32和其他庫
簡單字符串

  下面是一個接受字符串參數的函數的簡單示例: 

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  機制的函數,對於這些函數必須採取不同的方法。 

 

 


http://www.AspxBoy.Com


 


--------------------------------------------------------------------------------
    Joined:: 2003-11   Posts: 886  Integral: 10981   State: offline  5 4 3 2 1 0 -1 -2 -3 -4 -5    3   
 

Huobazi

 

靶子哥哥

來自:  Post by: 2005-4-19 10:04:53                                       
--------------------------------------------------------------------------------

Re:如何在C#中使用 Win32和其他庫
字符串緩衝區

  .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  字符串,因此我採用了上述方法編碼。 


 


http://www.AspxBoy.Com


 


--------------------------------------------------------------------------------
    Joined:: 2003-11   Posts: 886  Integral: 10981   State: offline  5 4 3 2 1 0 -1 -2 -3 -4 -5    4   
 

Huobazi

 

靶子哥哥

來自:  Post by: 2005-4-19 10:05:14                                       
--------------------------------------------------------------------------------

Re:如何在C#中使用 Win32和其他庫
具有回調的函數

  當  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
);   

  前兩個參數的處理比較簡單:ulong  很簡單,並且可以使用  UnmanagedType.LPArray  來封送緩衝區。 

  但第三和第四個參數有一些問題。問題在於定義  ACL  的方式。ACL  結構僅定義了  ACL  標頭,而緩衝區的其餘部分由  ACE  組成。ACE  可以具有多種不同類型,並且這些不同類型的  ACE  的長度也不同。 

  如果您願意爲所有緩衝區分配空間,並且願意使用不太安全的代碼,則可以用  C#  進行處理。但工作量很大,並且程序非常難調試。而使用  C++  處理此  API  就容易得多。 

  屬性的其他選項

  DLLImport  和  StructLayout  屬性具有一些非常有用的選項,有助於  P/Invoke  的使用。下面列出了所有這些選項: 

  DLLImport

  CallingConvention

  您可以用它來告訴封送拆收器,函數使用了哪些調用約定。您可以將它設置爲您的函數的調用約定。通常,如果此設置錯誤,代碼將不能執行。但是,如果您的函數是  Cdecl  函數,並且使用  StdCall(默認)來調用該函數,那麼函數能夠執行,但函數參數不會從堆棧中刪除,這會導致堆棧被填滿。 

  CharSet

  控制調用  A  變體還是調用  W  變體。 

  EntryPoint

  此屬性用於設置封送拆收器在  DLL  中查找的名稱。設置此屬性後,您可以將  C#  函數重新命名爲任何名稱。 

  ExactSpelling

  將此屬性設置爲  true,封送拆收器將關閉  A  和  W  的查找特性。 

  PreserveSig

  COM  互操作使得具有最終輸出參數的函數看起來是由它返回的該值。此屬性用於關閉這一特性。 

  SetLastError

  確保調用  Win32  API  SetLastError(),以便您找出發生的錯誤。 

  StructLayout

  LayoutKind

  結構在默認情況下按順序佈局,並且在多數情況下都適用。如果需要完全控制結構成員所放置的位置,可以使用  LayoutKind.Explicit,然後爲每個結構成員添加  FieldOffset  屬性。當您需要創建  union  時,通常需要這樣做。 

  CharSet

  控制  ByValTStr  成員的默認字符類型。 

  Pack

  設置結構的壓縮大小。它控制結構的排列方式。如果  C  結構採用了其他壓縮方式,您可能需要設置此屬性。 

  Size

  設置結構大小。不常用;但是如果需要在結構末尾分配額外的空間,則可能會用到此屬性。 

  從不同位置加載

  您無法指定希望  DLLImport  在運行時從何處查找文件,但是可以利用一個技巧來達到這一目的。 

  DllImport  調用  LoadLibrary()  來完成它的工作。如果進程中已經加載了特定的  DLL,那麼即使指定的加載路徑不同,LoadLibrary()  也會成功。 

  這意味着如果直接調用  LoadLibrary(),您就可以從任何位置加載  DLL,然後  DllImport  LoadLibrary()  將使用該  DLL。 

  由於這種行爲,我們可以提前調用  LoadLibrary(),從而將您的調用指向其他  DLL。如果您在編寫庫,可以通過調用  GetModuleHandle()  來防止出現這種情況,以確保在首次調用  P/Invoke  之前沒有加載該庫。 

  P/Invoke  疑難解答

  如果您的  P/Invoke  調用失敗,通常是因爲某些類型的定義不正確。以下是幾個常見問題: 

  1.long  !=  long。在  C++  中,long  是  4  字節的整數,但在  C#  中,它是  8  字節的整數。

  2.字符串類型設置不正確。
 
 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章