具有內嵌字符數組的結構
某些函數接受具有內嵌字符數組的結構。例如,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 的使用。下面列出了所有這些選項:
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 調用失敗,通常是因為某些類型的定義不正確。以下是幾個常見問題:
long
!=long
。在 C++ 中,long
是 4 字節的整數,但在 C# 中,它是 8 字節的整數。- 字符串類型設置不正確。