具有內嵌字元數組的結構
某些函數接受具有內嵌字元數組的結構。例如,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() 這樣的函數,其中的函數指標將被儲存以便將來使用,您就需要確保在您的代碼中引用委託。如果不這樣做,函數可能表面上能執行,但在將來的記憶體回收處理中會刪除委託,並且會出現錯誤。
其他進階函數
迄今為止我列出的樣本都比較簡單,但是還有很多更複雜的 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 的使用。下面列出了所有這些選項:
DLLImportCallingConvention
您可以用它來告訴封送拆收器,函數使用了哪些呼叫慣例。您可以將它設定為您的函數的呼叫慣例。通常,如果此設定錯誤,代碼將不能執行。但是,如果您的函數是 Cdecl 函數,並且使用 StdCall(預設)來調用該函數,那麼函數能夠執行,但函數參數不會從堆棧中刪除,這會導致堆棧被填滿。
CharSet
控制調用 A 變體還是調用 W 變體。
EntryPoint
此屬性用於設定封送拆收器在 DLL 中尋找的名稱。設定此屬性後,您可以將 C# 函數重新命名為任何名稱。
ExactSpelling
將此屬性設定為 true,封送拆收器將關閉 A 和 W 的尋找特性。
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 位元組的整數。
- 字串類型設定不正確。