概述
C#虽然功能强大,但在某些时刻必须也要对内存和指针进行操作。 C#中一共有三种方式:
1) 通过平台调用(P/Invoke)来调用非托管DLL所公开的API。
2) 通过不安全的代码,它允许我们访问内存地址和指针。
3) 通过COMInterop(COM 互操作),但此处不进行描述。
平台调用
1) 外部函数的声明
确定了要调用的目标函数后,P/Invoke的第一步便是用托管代码声明函数。和一个类的普通方法一样,必须在一个类的上下文中声明目标API,但要添加 extern修饰符。
class PlatformInvokeTest
{
[DllImport("msvcrt.dll")]
public static extern int puts(string c);
[DllImport("msvcrt.dll")]
internal static extern int _flushall();
}
2) 参数的数据类型
定义了函数声明后,下一步便是标识或创建与外部函数中的非托管数据类型对应的托管数据类型。例如:外部函数的定义为
LPVOID VirtualAllocEx(HANDLE hProcess,
LPVOID lpAddress,
SIZE_T dwSize,
DWORD flAllocationType,
DWORD flProtect);
class PlatformInvokeTest
{
[DllImport("kernel32.dll", SetLastError = true)]
internal static extern IntPtr VirtualAllocEx(IntPtr hProcess,
IntPtr lpAddress,
IntPtr dwSize,
AllocationType flAllocationType,
uint flProtect)
}
这里定义的托管函数与非托管函数的参数是一一对应的。
关于 IntPtr
托管代码的特征是像int这样的基本数据类型不会因处理器而改变大小。但在非托管代码中,内存指针会随处理器而变化。因此不能将HANDLE 或 LPVOID映射到 int型上,而应把他们映射到System.IntPtr上,其大小将依据处理器的内存布局而变化。
3) 使用 ref 而不是指针
很多情况下,非托管代码会为传引用参数使用指针。在这些情况下,P/Invoke不要求你在托管代码中将数据类型映射到一个指针。应该将参数映射为 ref。例如:
class PlatformInvokeTest
{
[DllImport("kernel32.dll", SetLastError = true)]
internal static extern IntPtr VirtualAllocEx(IntPtr hProcess,
IntPtr lpAddress,
IntPtr dwSize,
AllocationType flAllocationType,
uint flProtect
ref unit lpflOldProtect);
}
4) 错误处理
Win32 API 编程的一个不便之处在于,错误报告的方式不一致,有时使用返回值,有时以参数输出的方式,有时使用GetLastError()函数。非托管代码中Win32的错误报告很少通过异常来生成。P/Invoke 为此提供了相应的处理方式。如果需要启用这一方式,DLLImport 特性的SetLastError 参数要设为 true。这样就可以实例化 System.ComponentModel.Win32Exception()。在P/Invoke调用后,会自动用Win32数据来初始化它。
例如:
class VirtualMemoryManager
{
[DLLImport("kernel32.dll", SetLastError = true)]
private static extern IntPtr VirtualAllocEx(IntPtr hProcess,
IntPtr lpAddress,
IntPtr dwSize,
AllocationType flAllocationType,
uint flProtect);
private static IntPtr AllocExecuteionBlock(int size, IntPtr hProcess)
{
IntPtr codeBytesPtr;
codeBytesPtr = VirtualAllocEx(hProcess, IntPtr.Zero, (IntPtr)size,
AllocationType.Reserve,
(uint)ProtectionOptions.PageExecuteReadWrite);
if (codeBytesPtr == IntPtr.Zero)
{
throw new System.ComponentModel.Win32Exception();
}
return codeBytesPtr;
}
}
5) 使用SafeHandle
很多时候P/Invoke会涉及资源清理和释放。
这时需要定义一个类派生自 System.Runtime.InteropServices.SafeHandle并且实现其中的ReleaseHandle() 方法, 在其中释放相关的资源。
指针和地址 (不安全代码)
有些情况下开发人员希望直接访问和操作内存,以及直接使用指针来定位内存。如果我们希望直接操纵内存,我们只需将一个代码区域指定为 unsafe(不安全)即可。
使用 unsafe 指定一个方法
class Program
{
unsafe static int Main()
{
// ...
}
}
使用 unsafe 指定一个代码块
class Program
{
static int Main()
{
unsafe
{
// ...
}
}
}
指针的声明和编写不安全的代码
代码实例:
using System;
namespace unsafe_test1
{
class UnsafeTest
{
// Unsafe method: takes pointer to int:
unsafe static void SquarePtrParam(int* p)
{
*p *= *p;
}
unsafe static void Main()
{
int i = 5;
// Unsafe method: uses address-of operator (&):
SquarePtrParam(&i);
Console.WriteLine(i);
Console.ReadKey();
}
}
}
一些限制:
指针不能指向托管类型,例如 string, 类等。为了将一些数据的地址赋给一个指针,要求如下:
- 数据必须属于一变量
- 数据必须是非托管类型
- 变量需要用 fixed 固定,不能移动
byte[] bytes = new byte[24];
fixed (byte* pData = & bytes[0])
{
// ...
}