如何在C#中使用Win32和其他庫之三)

來源:互聯網
上載者:User
具有內嵌字元數組的結構

某些函數接受具有內嵌字元數組的結構。例如,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。這是因為,它也沒有 AW 變體,而始終使用 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);

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

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

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

屬性的其他選項

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

DLLImportCallingConvention

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

CharSet

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

EntryPoint

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

ExactSpelling

將此屬性設定為 true,封送拆收器將關閉 AW 的尋找特性。

PreserveSig

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

SetLastError

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

StructLayoutLayoutKind

結構在預設情況下按循序配置,並且在多數情況下都適用。如果需要完全控制結構成員所放置的位置,可以使用 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 調用失敗,通常是因為某些類型的定義不正確。以下是幾個常見問題:

  • long != long。在 C++ 中,long 是 4 位元組的整數,但在 C# 中,它是 8 位元組的整數。
  • 字串類型設定不正確。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.