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 }