1.平臺互操作性和不安全的代碼:C#功能強大,但有些時候,它的表現仍然有些“力不從心”,所以我們只能摒棄它所提供的所有安全性,轉而退回到記憶體地址和指針的世界。 C#通過3種方式對此提供支持。 (1)第一種方式是通過平臺調用(Platform Invoke,P/Invoke)來調用非托管代碼DLL所公 ...
1.平臺互操作性和不安全的代碼:C#功能強大,但有些時候,它的表現仍然有些“力不從心”,所以我們只能摒棄它所提供的所有安全性,轉而退回到記憶體地址和指針的世界。
C#通過3種方式對此提供支持。
(1)第一種方式是通過平臺調用(Platform Invoke,P/Invoke)來調用非托管代碼DLL所公開的API。
(2)第二種方式是通過不安全的代碼,它允許我們訪問記憶體指針和地址。很多情況下,代碼需要綜合運用這兩種方式。
(3)第三種方式是通過COM Interop(COM互操作)。
2.平臺調用:
(1)外部函數的聲明:確定了要調用的目標函數以後,P/Invoke的下一步便是用托管代碼聲明函數,和普通方法一樣,必須在一個類中聲明目標API,但要為它添加extern修飾符,從而把它聲明為外部函數,extern方法始終是靜態方法,因此不包含任何實現。相反,附加在方法聲明之前的DllImport特性指向實現。該特性需要定義該函數的DLL的“名稱”,導入的DLL必須在路徑內,其中包含可執行文件的目錄,以使其能夠載入成功。“運行時”根據方法名來判斷函數名,然而也可以用EntryPoint具名參數來重寫預設行為,明確提供一個函數名。
(2)參數的數據類型:在確定目標DLL和導出函數,那麼要標識或創建與外部函數中的非托管數據類型對應的托管數據類型。
3.為順序佈局使用StructLayoutAttribute:有些API涉及的類型沒有對應的托管類型,要調用這些API,需要托管代碼重新聲明類型。例如,可以使用托管代碼來聲明非托管的COLORREF struct,如ColorRef結構清單。代碼中聲明的關鍵之處在於StructLayoutAttribute,預設情況下,托管代碼可以優化類型的記憶體佈局,所以,記憶體佈局可能不是從一個欄位到另一個欄位順序存儲。為了強制順序佈局,使類型能夠直接映射,而且可以在托管和非托管代碼之間逐位地複製,你需要添加StructLayoutAttribute特性,並指定LayoutKind.Sequential枚舉值。
4.平臺調用(P/Invoke)的錯誤處理:Win32 API編程的一個不便之處在於,錯誤經常以不一致的方式來報告,如有API返回0、1、false等,有API以out參數來處理,非托管代碼中的Win32錯誤報告很少通過異常來生成。P/Invoke設計者為此提供了相應的處理機制,要啟用這一機制,DllImport特性的SetLastError具名參數要設為true,這樣就可以實例化一個System.ComponentModel.Win32Exception。在P/Invoke調用之後,會自動用Win32錯誤數據來初始化它,如VirtualMemoryManger類的代碼清單。這樣一來,開發人員就可以提供每個API使用的自定義錯誤檢查,同時仍然可以使用一種標準方式來報告錯誤。
5.使用SafeHandle:很多時候,P/Invoke會涉及一個資源,比如視窗句柄(Window handle),等等。在用完此類資源之後,代碼需要清理它們。但是,不要強迫開發人員記住這一點,並每次都人工編寫代碼,而是應該提供實現IDisposable介面和終結器的類。為了對此提供內建的支持,如下麵的VirtualMemoryPtr類,該類派生自System.Runtime.InteropServices.SafeHandle。SafeHandle類包含兩個抽象成員:IsInvalid和ReleaseHandle()。在後者中,你可以放入對資源進行清理的代碼,前者則指出是否執行了資源清理代碼。可查看VirtualMemoryPtr類代碼清單。
6.P/Invoke指導原則:
(1)核實確實沒有托管類型已經公開你想要的API。
(2)將API外部方法定義為private,或者在簡單的情況下定義為Internal。
(3)圍繞外部方法提供公共包裝方法,執行數據類型轉換和錯誤處理。
(4)重載包裝方法,並通過為外部方法調用插入預設值,減少所需的參數數目。
(5)在聲明API的同時,使用enum或const為API提供常量值。
(6)針對支持GetLastError()的所有P/Invoke方法,務必將SetLastError命名特性的值設為true。這樣一來,就可以通過System.ComponentModel.Win32Exception報告錯誤。
(7)將句柄之類的資源包裝,包裝在從System.Runtime.InteropServices.SafeHandle派生或者支持IDisposable的類中。
(8)非托管代碼中的函數指針映射到托管代碼中的委托實例。通常,這需要聲明一個特定的委托類型,它與非托管函數指針的簽名是匹配的。
(9)將輸入/輸出參數和輸出參數映射到ref參數,而不是依賴於指針。
7.不安全的代碼:可以使用unsafe用作類型或者類型內部的特定成員的修飾符。unsafe修飾符對生成的CIL代碼本身沒有影響。它只是一個預編譯指令,作用是向編譯器指明允許在不安全的代碼塊內操作指針和地址。
8.指針的聲明:由於指針(本身只是恰好指向記憶體地址的一些整形值)不會被垃圾回收,所以C#不允許非托管類型之外的被引用物類型。換言之,類型不能是引用類型,不能是泛型類型,而且內部不能包含引用類型。如 byte* pData;指針是一種全新的類型,和結構、枚舉、類不同,指針的終極基類不是System.Object,甚至不能轉換成System.Object,相反,它們能轉換成System.IntPtr(後者能轉換成System.Object)。
9.指針的賦值:我們需要使用地址運算符(&)來獲取值類型的地址。無論哪種方法,為了將一些數據的地址賦值給一個指針,要求如下。
(1)數據必須屬於一個變數。
(2)數據必須是一個非托管類型。
(3)變數需要用fixed固定,不能移動。
如 byte* pData = &bytes[0];//編譯錯誤,數據可能發生移動,需要固定。
如 byte[] bytes = new bytes[24]; fixed (byte* pData = &bytes[0]){}//編譯正確
10.指針的解引用:為了訪問指針引用的一個類型值,要求你解引用指針,即在指針類型之前添加一個間接定址運算符*。如 byte data = *pData;不能對void*類型的指針應用解引用運算符,void*數據類型代表的是指向一個未知類型的指針。由於數據類型未知,所以不能解引用到另一種類型。相反,為了訪問void*引用的數據,必須把它轉換成其他任何指針類型的變數,然後對後一種類型執行解引用。
[StructLayout(LayoutKind.Sequential)] public struct ColorRef { public byte Red; public byte Green; public byte Blue; private byte Unused; public ColorRef(byte red, byte green, byte blue) : this() { Red = red; Green = green; Blue = blue; Unused = 0; } } public class VirtualMemoryManger { [DllImport("kernel32.dll", SetLastError = true)] private static extern bool VirtualFreeEx(IntPtr hProcess, IntPtr lpAddress, IntPtr dwSize, IntPtr dwFreeType); [DllImport("kernel32.dll", EntryPoint = "GetCurrentProcess")] internal static extern IntPtr GetCurrentProcessHandle(); [DllImport("kernel32.dll", SetLastError = true)] private static extern IntPtr VirtualAllocEx(IntPtr hProcess, IntPtr lpAddress, IntPtr dwSize, AllocationType flAllocationType, uint flProtect); [DllImport("kernel32.dll", SetLastError = true)] private static extern bool VirtualProtectEx(IntPtr hPorcess, IntPtr lpAddress, IntPtr dwSize, uint flNewProtect, ref uint lpflOldProtect); public static IntPtr AllocExecutionBlock(int size, IntPtr hProcess) { IntPtr codeBytesPtr = VirtualAllocEx(hProcess, IntPtr.Zero, (IntPtr)size, AllocationType.Reserve | AllocationType.Commit, (uint)ProtectionOptions.PageExecuteReadWrite); if (codeBytesPtr == IntPtr.Zero) { throw new Win32Exception(); } uint lpflOldProtect = 0; if (!VirtualProtectEx(hProcess, codeBytesPtr, (IntPtr)size, (uint)ProtectionOptions.PageExecuteReadWrite, ref lpflOldProtect)) { throw new Win32Exception(); } return codeBytesPtr; } public static IntPtr AllocExecutionBlock(int size) { //通常應該將方法封裝到公共包裝裡面,從而降低P/Invoke API調用的複雜性,這樣可以增強API的可用性,同時更有利於轉向面向對象的類型結構。 return AllocExecutionBlock(size, GetCurrentProcessHandle()); } public static bool VirtualFreeEx(IntPtr hProcess, IntPtr lpAddress, IntPtr dwSize) { bool result = VirtualFreeEx(hProcess, lpAddress, dwSize, (IntPtr)MemoryFreeType.Decommit); if (!result) { throw new Win32Exception(); } return result; } public static bool VirtualFreeEx(IntPtr lpAddress, IntPtr dwSize) { //無論錯誤處理、struct、還是常量值,優秀的API開發人員都應該提供一個簡化的托管API,降低層的Win32API包裝起來。 return VirtualFreeEx(GetCurrentProcessHandle(), lpAddress, dwSize); } } public class VirtualMemoryPtr:SafeHandle { public readonly IntPtr AllocatedPointer; private readonly IntPtr ProcessHandle; private readonly IntPtr MemorySize; private bool Disposed; public VirtualMemoryPtr(int memorySize) : base(IntPtr.Zero, true) { ProcessHandle = VirtualMemoryManger.GetCurrentProcessHandle(); MemorySize = (IntPtr) memorySize; AllocatedPointer = VirtualMemoryManger.AllocExecutionBlock(memorySize, ProcessHandle); Disposed = false; } public static implicit operator IntPtr(VirtualMemoryPtr virtualAMemoryPointer) { return virtualAMemoryPointer.AllocatedPointer; } protected override bool ReleaseHandle() { if (!Disposed) { Disposed = true; GC.SuppressFinalize(this); VirtualMemoryManger.VirtualFreeEx(ProcessHandle,AllocatedPointer,MemorySize); } return true; } public override bool IsInvalid { get { return Disposed; } } } [Flags] public enum AllocationType { Reserve = 0x2000, Commit = 0x1000, Reset = 0x8000, Physical = 0x400000, TopDown = 0x100000, } [Flags] public enum ProtectionOptions { PageExecuteReadWrite = 0x40, PageExecuteRead = 0x20, Execute = 0x10 } [Flags] public enum MemoryFreeType { Decommit = 0x4000, Release = 0x8000 }View Code