C# 平台调用与指针操作全解析
1. 平台调用(Platform Invoke)基础
在开发中,我们常常会遇到需要在托管代码(如 C#)中调用非托管代码(如 C++)编写的 Windows API 的情况。平台调用(P/Invoke)就是实现这一目的的重要机制。
1.1 结构体布局
许多 Microsoft Windows 颜色 API 使用
COLORREF
来表示 RGB 颜色。在声明相关结构体时,
StructLayoutAttribute
是关键。默认情况下,托管代码会对类型的内存布局进行优化,这可能导致字段布局不连续。为了确保类型能直接映射,并且可以在托管代码和非托管代码之间逐位复制(blitted),我们需要添加
StructLayoutAttribute
并设置其
LayoutKind.Sequential
枚举值。例如:
[StructLayout(LayoutKind.Sequential)]
struct SomeStruct
{
// 结构体字段
}
由于非托管(C++)的结构体定义与 C# 的定义并不直接匹配,所以在从非托管结构体映射到托管结构体时,开发者需要遵循 C# 的常规准则,考虑类型应表现为值类型还是引用类型,以及其大小是否较小(大约小于 16 字节)。
1.2 错误处理
Win32 API 编程的一个不便之处在于错误报告方式不一致。有些 API 通过返回值(如 0、1、false 等)来指示错误,而有些则通过设置
out
参数来传达错误信息。要获取详细的错误信息,还需要额外调用
GetLastError()
API,然后再调用
FormatMessage()
来获取对应的错误消息。总之,非托管代码中的 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);
// ...
[DllImport("kernel32.dll", SetLastError = true)]
static extern bool VirtualProtectEx(
IntPtr hProcess, IntPtr lpAddress,
IntPtr dwSize, uint flNewProtect,
ref uint lpflOldProtect);
[Flags]
private enum AllocationType : uint
{
// ...
}
[Flags]
private enum ProtectionOptions
{
// ...
}
[Flags]
private enum MemoryFreeType
{
// ...
}
public static IntPtr AllocExecutionBlock(
int size, IntPtr hProcess)
{
IntPtr codeBytesPtr;
codeBytesPtr = VirtualAllocEx(
hProcess, IntPtr.Zero,
(IntPtr)size,
AllocationType.Reserve | AllocationType.Commit,
(uint)ProtectionOptions.PageExecuteReadWrite);
if (codeBytesPtr == IntPtr.Zero)
{
// 处理错误
}
uint lpflOldProtect = 0;
if (!VirtualProtectEx(
hProcess, codeBytesPtr,
(IntPtr)size,
(uint)ProtectionOptions.PageExecuteReadWrite,
ref lpflOldProtect))
{
throw new System.ComponentModel.Win32Exception();
}
return codeBytesPtr;
}
public static IntPtr AllocExecutionBlock(int size)
{
return AllocExecutionBlock(
size, GetCurrentProcessHandle());
}
}
1.3 使用公共包装器简化 API 调用
除了最简单的 API 外,将 P/Invoke 方法封装在公共包装器中是一个不错的做法。这样可以降低 P/Invoke API 调用的复杂性,提高 API 的可用性,并朝着面向对象的类型结构发展。例如,
AllocExecutionBlock()
方法就是一个很好的示例。
1.4 使用 SafeHandle 管理资源
在 P/Invoke 中,经常会涉及到一些资源,如窗口句柄,使用完后需要进行清理。为了避免开发者每次都手动编写清理代码,可以提供一个实现
IDisposable
接口和终结器的类。在 .NET 2.0 中,可以定义一个继承自
System.Runtime.InteropServices.SafeHandle
的类来实现这一点。以下是一个示例:
public class VirtualMemoryPtr :
System.Runtime.InteropServices.SafeHandle
{
public VirtualMemoryPtr(int memorySize) :
base(IntPtr.Zero, true)
{
ProcessHandle =
VirtualMemoryManager.GetCurrentProcessHandle();
MemorySize = (IntPtr)memorySize;
AllocatedPointer =
VirtualMemoryManager.AllocExecutionBlock(
memorySize, ProcessHandle);
Disposed = false;
}
public readonly IntPtr AllocatedPointer;
readonly IntPtr ProcessHandle;
readonly IntPtr MemorySize;
bool Disposed;
public static implicit operator IntPtr(
VirtualMemoryPtr virtualMemoryPointer)
{
return virtualMemoryPointer.AllocatedPointer;
}
// SafeHandle 抽象成员
public override bool IsInvalid
{
get
{
return Disposed;
}
}
// SafeHandle 抽象成员
protected override bool ReleaseHandle()
{
if (!Disposed)
{
Disposed = true;
GC.SuppressFinalize(this);
VirtualMemoryManager.VirtualFreeEx(ProcessHandle,
AllocatedPointer, MemorySize);
}
return true;
}
}
System.Runtime.InteropServices.SafeHandle
包含
IsInvalid
和
ReleaseHandle()
抽象成员。在
ReleaseHandle()
中放置清理代码,
IsInvalid
用于指示清理代码是否已经执行。
1.5 在 C# 1.0 中使用 IDisposable 管理资源
在 C# 1.0 中,
System.Runtime.InteropServices.SafeHandle
不可用,需要自定义实现
IDisposable
接口。以下是一个示例:
public struct VirtualMemoryPtr : IDisposable
{
public VirtualMemoryPtr(int memorySize)
{
ProcessHandle =
VirtualMemoryManager.GetCurrentProcessHandle();
MemorySize = (IntPtr)memorySize;
AllocatedPointer =
VirtualMemoryManager.AllocExecutionBlock(
memorySize, ProcessHandle);
Disposed = false;
}
public readonly IntPtr AllocatedPointer;
readonly IntPtr ProcessHandle;
readonly IntPtr MemorySize;
bool Disposed;
public static implicit operator IntPtr(
VirtualMemoryPtr virtualMemoryPointer)
{
return virtualMemoryPointer.AllocatedPointer;
}
#region IDisposable Members
public void Dispose()
{
if (!Disposed)
{
Disposed = true;
GC.SuppressFinalize(this);
VirtualMemoryManager.VirtualFreeEx(ProcessHandle,
AllocatedPointer, MemorySize);
}
}
#endregion
}
需要注意的是,为了让
VirtualMemoryPtr
具有值类型的语义,需要将其实现为结构体。但这样做的后果是不能有终结器,因为垃圾回收器不管理值类型。这意味着使用该类型的开发者必须记得清理代码,否则没有后备机制。此外,不要将该实例传递或复制到方法外部,这是实现
IDisposable
接口类型的常见准则。
1.6 调用外部函数
声明 P/Invoke 函数后,调用它们就像调用其他类成员一样。关键是要确保导入的 DLL 在路径中,包括可执行文件目录,以便能够成功加载。同时,对于像
flAllocationType
和
flProtect
这样的标志参数,最好提供常量或枚举,将其作为 API 声明的一部分,以提高代码的可读性和可维护性。以下是一个封装 API 的示例:
class VirtualMemoryManager
{
// ...
/// <summary>
/// The type of memory allocation. This parameter must
/// contain one of the following values.
/// </summary>
[Flags]
private enum AllocationType : uint
{
/// <summary>
/// Allocates physical storage in memory or in the
/// paging file on disk for the specified reserved
/// memory pages. The function initializes the memory
/// to zero.
/// </summary>
Commit = 0x1000,
/// <summary>
/// Reserves a range of the process's virtual address
/// space without allocating any actual physical
/// storage in memory or in the paging file on disk.
/// </summary>
Reserve = 0x2000,
/// <summary>
/// Indicates that data in the memory range specified by
/// lpAddress and dwSize is no longer of interest. The
/// pages should not be read from or written to the
/// paging file. However, the memory block will be used
/// again later, so it should not be decommitted. This
/// value cannot be used with any other value.
/// </summary>
Reset = 0x80000,
/// <summary>
/// Allocates physical memory with read-write access.
/// This value is solely for use with Address Windowing
/// Extensions (AWE) memory.
/// </summary>
Physical = 0x400000,
/// <summary>
/// Allocates memory at the highest possible address.
/// </summary>
TopDown = 0x100000,
}
/// <summary>
/// The memory protection for the region of pages to be
/// allocated.
/// </summary>
[Flags]
private enum ProtectionOptions : uint
{
/// <summary>
/// Enables execute access to the committed region of
/// pages. An attempt to read or write to the committed
/// region results in an access violation.
/// </summary>
Execute = 0x10,
/// <summary>
/// Enables execute and read access to the committed
/// region of pages. An attempt to write to the
/// committed region results in an access violation.
/// </summary>
PageExecuteRead = 0x20,
/// <summary>
/// Enables execute, read, and write access to the
/// committed region of pages.
/// </summary>
PageExecuteReadWrite = 0x40,
// ...
}
/// <summary>
/// The type of free operation
/// </summary>
[Flags]
private enum MemoryFreeType : uint
{
/// <summary>
/// Decommits the specified region of committed pages.
/// After the operation, the pages are in the reserved
/// state.
/// </summary>
Decommit = 0x4000,
/// <summary>
/// Releases the specified region of pages. After this
/// operation, the pages are in the free state.
/// </summary>
Release = 0x8000
}
// ...
}
枚举的优点是可以将每个值分组,并且可以将范围限制为这些值。
1.7 简化 API 调用的包装器
优秀的 API 开发者的一个目标是提供一个简化的托管 API 来包装底层的 Win32 API。例如,可以对
VirtualFreeEx()
方法进行重载,提供公共版本以简化调用。以下是示例代码:
class VirtualMemoryManager
{
// ...
[DllImport("kernel32.dll", SetLastError = true)]
static extern bool VirtualFreeEx(
IntPtr hProcess, IntPtr lpAddress,
IntPtr dwSize, IntPtr dwFreeType);
public static bool VirtualFreeEx(
IntPtr hProcess, IntPtr lpAddress,
IntPtr dwSize)
{
bool result = VirtualFreeEx(
hProcess, lpAddress, dwSize,
(IntPtr)MemoryFreeType.Decommit);
if (!result)
{
throw new System.ComponentModel.Win32Exception();
}
return result;
}
public static bool VirtualFreeEx(
IntPtr lpAddress, IntPtr dwSize)
{
return VirtualFreeEx(
GetCurrentProcessHandle(), lpAddress, dwSize);
}
[DllImport("kernel32", SetLastError = true)]
static extern IntPtr VirtualAllocEx(
IntPtr hProcess,
IntPtr lpAddress,
IntPtr dwSize,
AllocationType flAllocationType,
uint flProtect);
// ...
}
1.8 函数指针与委托的映射
在 P/Invoke 中,非托管代码中的函数指针映射到托管代码中的委托。例如,在设置 Microsoft Windows 定时器时,需要提供一个函数指针,定时器到期时会回调该指针。具体来说,需要传递一个与回调签名匹配的委托实例。
2. 编写 P/Invoke 代码的指南
为了更高效地编写 P/Invoke 代码,以下是一些指南:
1.
检查是否有托管类已经暴露了所需的 API
:避免重复造轮子,优先使用已有的托管类。
2.
将 API 外部方法定义为私有或在简单情况下定义为内部方法
:提高代码的封装性。
3.
在外部方法周围提供公共包装器方法
:处理数据类型转换和错误处理,提高 API 的可用性。
4.
重载包装器方法
:通过为外部方法调用插入默认值,减少所需参数的数量。
5.
使用枚举或常量为 API 提供常量值
:作为 API 声明的一部分,提高代码的可读性和可维护性。
6.
对于支持
GetLastError()
的所有 P/Invoke 方法
:确保将
SetLastError
命名属性设置为
true
,以便通过
System.ComponentModel.Win32Exception
报告错误。
7.
将资源(如句柄)封装到类中
:这些类可以继承自
System.Runtime.InteropServices.SafeHandle
或支持
IDisposable
接口,方便资源的管理和清理。
8.
将非托管代码中的函数指针映射到托管代码中的委托实例
:通常需要声明一个与非托管函数指针签名匹配的特定委托类型。
9.
将输入/输出和输出参数映射到
ref
参数
:而不是依赖指针,提高代码的安全性和可读性。
3. 指针和地址操作
在某些情况下,开发者需要直接访问和操作内存以及内存地址,这对于与操作系统交互或实现时间关键型算法是必要的。C# 为此提供了
unsafe
代码结构。
3.1 不安全代码(Unsafe Code)
C# 的一个重要特性是强类型和运行时类型检查。但有时我们需要绕过这些检查,直接操作内存和地址,例如在处理内存映射设备或实现时间关键型算法时。这时,需要将代码的一部分指定为
unsafe
。
unsafe
代码是一个显式的代码块和编译选项。
unsafe
修饰符对生成的 CIL 代码本身没有影响,它只是一个指令,告诉编译器允许在
unsafe
块内进行指针和地址操作。需要注意的是,
unsafe
并不意味着非托管。以下是指定方法为
unsafe
代码的示例:
class Program
{
unsafe static int Main(string[] args)
{
// ...
}
}
也可以使用
unsafe
语句来标记一个代码块允许使用
unsafe
代码:
class Program
{
static int Main(string[] args)
{
unsafe
{
// ...
}
}
}
在
unsafe
块内的代码可以包含指针等不安全构造。
需要注意的是,必须明确告诉编译器支持
unsafe
代码。从命令行编译时,需要使用
/unsafe
开关,例如:
csc.exe /unsafe Program.cs
在 Visual Studio 中,可以通过勾选项目属性窗口“生成”选项卡中的“允许不安全代码”复选框来启用。使用
/unsafe
开关是因为
unsafe
代码可能会导致缓冲区溢出等安全问题,要求使用该开关可以使潜在的安全风险选择更加明确。
3.2 指针声明
在标记代码块为
unsafe
后,就可以声明指针了。例如:
byte* pData;
假设
pData
不为空,它的值指向包含一个或多个连续字节的位置,
pData
的值表示这些字节的内存地址。
*
前面指定的类型是引用类型,即指针指向的类型。在这个例子中,
pData
是指针,
byte
是引用类型。
需要注意的是,指针只是恰好引用内存地址的整数,它们不受垃圾回收的影响。C# 只允许引用类型为非托管类型,即不是引用类型、不是泛型且不包含引用类型的类型。因此,以下声明是无效的:
string* pMessage;
以及:
struct ServiceStatus
{
int State;
string Description; // Description is a reference type
}
ServiceStatus* pStatus;
除了只包含非托管类型的自定义结构体,有效的引用类型还包括枚举、预定义的值类型(如
sbyte
、
byte
、
short
、
ushort
、
int
、
uint
、
long
、
ulong
、
char
、
float
、
double
、
decimal
和
bool
)以及指针类型(如
byte**
)。此外,
void*
指针也是有效的,它表示指向未知类型的指针。
在 C/C++ 中,同一声明中的多个指针声明方式如下:
int *p1, *p2;
而在 C# 中,
*
总是与数据类型放在一起:
int* p1, p2;
3.3 指针赋值
定义指针后,在访问它之前需要为其赋值。和引用类型一样,指针的默认值可以为
null
。指针存储的是一个位置的地址,因此要为其赋值,需要先获取数据的地址。
可以将整数或长整数显式转换为指针,但这种情况很少见,通常需要在运行时确定特定数据值的地址。一般使用地址运算符
&
来获取值类型的地址,但在托管环境中,数据可能会移动,从而使地址无效。例如:
byte* pData = &bytes[0]; // Compile error
错误信息为 “You can only take the address of [an] unfixed expression inside a fixed statement initializer.”。这是因为数组是引用类型,会受到垃圾回收或重新定位的影响。同样,引用可移动类型上的值类型字段也会出现类似问题:
int* a = &"message".Length;
要为某些数据分配地址,需要满足以下条件:
1. 数据必须被分类为变量。
2. 数据必须是非托管类型。
3. 变量需要被分类为固定的,不可移动的。
如果数据是非托管变量类型但不是固定的,可以使用
fixed
语句来固定可移动变量。
3.4 固定数据(Fixing Data)
为了获取可移动数据项的地址,需要固定(或钉住)数据。以下是
fixed
语句的示例:
byte[] bytes = new byte[24];
fixed (byte* pData = &bytes[0]) // pData = bytes also allowed
{
// ...
}
在
fixed
语句的代码块内,分配的数据不会移动。在这个例子中,
bytes
将保持在同一地址,至少直到
fixed
语句结束。
fixed
语句要求在其作用域内声明指针变量,以避免在数据不再固定时访问该变量。但程序员需要确保不会将指针赋值给在
fixed
语句作用域之外仍然存在的另一个变量,例如在 API 调用中。同样,使用
ref
或
out
参数对于在方法调用后不会存在的数据也会有问题。
虽然字符串是无效的引用类型,但在 C# 中,字符串内部是指向字符数组第一个字符的指针,可以使用
char*
声明指向字符的指针。因此,C# 允许在
fixed
语句内声明
char*
类型的指针并将其赋值给字符串,
fixed
语句可以防止字符串在指针生命周期内移动。同样,对于支持隐式转换为另一个指针类型的可移动类型,在
fixed
语句中也可以使用。
可以用缩写形式
bytes
替换冗长的
&bytes[0]
赋值方式:
byte[] bytes = new byte[24];
fixed (byte* pData = bytes)
{
// ...
}
根据执行的频率和时间,
fixed
语句可能会导致堆碎片化,因为垃圾回收器无法压缩固定对象。为了减少这个问题,最佳实践是在执行早期固定大块数据,而不是固定许多小块数据。同时,要尽量减少固定数据的时间,以降低在数据固定期间发生垃圾回收的可能性。在一定程度上,.NET 2.0 由于一些额外的碎片化感知代码,减少了这个问题。
3.5 在栈上分配内存
可以使用
fixed
语句来防止垃圾回收器移动数组数据,另一种方法是在调用栈上分配数组。栈分配的数据不受垃圾回收或伴随的终结器模式的影响。和引用类型一样,使用
stackalloc
分配的数据必须是非托管类型的数组。例如:
byte* bytes = stackalloc byte[42];
由于数据类型是非托管类型的数组,运行时可以为数组分配固定的缓冲区大小,并在指针超出作用域时恢复该缓冲区。具体来说,它分配
sizeof(T) * E
的大小,其中
E
是数组大小,
T
是引用类型。由于
stackalloc
只能用于非托管类型的数组,运行时只需展开栈就可以将缓冲区恢复给系统,避免了遍历
f
- 可达队列和压缩可达数据的复杂性。因此,没有办法显式释放
stackalloc
分配的数据。
综上所述,C# 的平台调用和指针操作提供了强大的功能,但也需要开发者谨慎使用,遵循相关的指南和规则,以确保代码的安全性和性能。
C# 平台调用与指针操作全解析
4. 操作总结与对比
为了更清晰地理解和运用上述知识,下面通过表格对一些关键操作进行总结对比:
|操作类型|具体操作|适用场景|注意事项|
| ---- | ---- | ---- | ---- |
|平台调用结构体布局|使用
StructLayoutAttribute
并设置
LayoutKind.Sequential
枚举值|需要在托管和非托管代码间逐位复制类型时|注意非托管和 C# 结构体定义的差异|
|错误处理|设置
DllImport
属性的
SetLastError
为
true
,使用
System.ComponentModel.Win32Exception
|处理 Win32 API 调用错误时|确保 API 支持
GetLastError()
|
|资源管理|使用
System.Runtime.InteropServices.SafeHandle
或自定义
IDisposable
实现|处理 P/Invoke 涉及的资源清理时|C# 1.0 无
SafeHandle
,结构体实现
IDisposable
无终结器|
|指针声明|在
unsafe
代码块内声明,如
byte* pData;
|需要直接操作内存地址时|引用类型必须是非托管类型|
|指针赋值|使用地址运算符
&
获取地址,结合
fixed
语句|为指针赋值时|数据需为固定的非托管变量|
|栈上内存分配|使用
stackalloc
为非托管类型数组分配内存|需要快速分配和释放内存时|无法显式释放
stackalloc
分配的数据|
5. 示例流程分析
下面通过一个 mermaid 流程图来展示一个完整的使用 P/Invoke 进行虚拟内存分配和管理的流程:
graph TD;
A[开始] --> B[声明 P/Invoke 方法];
B --> C[调用 AllocExecutionBlock 方法];
C --> D{分配是否成功};
D -- 是 --> E[调用 VirtualProtectEx 方法];
E --> F{保护设置是否成功};
F -- 是 --> G[使用分配的内存];
G --> H[使用 SafeHandle 或 IDisposable 清理资源];
H --> I[结束];
D -- 否 --> J[抛出 Win32Exception 错误];
J --> I;
F -- 否 --> K[抛出 Win32Exception 错误];
K --> I;
6. 常见问题及解决方案
在使用 C# 进行平台调用和指针操作时,可能会遇到一些常见问题,以下是这些问题及对应的解决方案:
1.
指针引用类型错误
-
问题描述
:尝试使用引用类型作为指针的引用类型,如
string* pMessage;
。
-
解决方案
:确保指针的引用类型为非托管类型,如预定义的值类型、枚举、只包含非托管类型的自定义结构体等。
2.
地址无效问题
-
问题描述
:在托管环境中,使用
&
运算符获取可移动数据的地址时出现编译错误。
-
解决方案
:使用
fixed
语句固定可移动数据,确保数据在操作期间地址不变。
3.
资源未清理问题
-
问题描述
:在使用 P/Invoke 涉及的资源(如内存、句柄)后,未进行清理,导致资源泄漏。
-
解决方案
:使用
System.Runtime.InteropServices.SafeHandle
或自定义实现
IDisposable
接口的类来管理资源,确保资源在使用后被正确清理。
4.
编译不支持
unsafe
代码
-
问题描述
:编译器不允许使用
unsafe
代码。
-
解决方案
:从命令行编译时使用
/unsafe
开关;在 Visual Studio 中,勾选项目属性窗口“生成”选项卡中的“允许不安全代码”复选框。
7. 实际应用案例
以下是一个简单的实际应用案例,展示如何使用上述知识进行虚拟内存的分配和管理:
using System;
using System.Runtime.InteropServices;
class VirtualMemoryManager
{
[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)]
static extern bool VirtualProtectEx(
IntPtr hProcess, IntPtr lpAddress,
IntPtr dwSize, uint flNewProtect,
ref uint lpflOldProtect);
[DllImport("kernel32.dll", SetLastError = true)]
static extern bool VirtualFreeEx(
IntPtr hProcess, IntPtr lpAddress,
IntPtr dwSize, IntPtr dwFreeType);
[Flags]
private enum AllocationType : uint
{
Commit = 0x1000,
Reserve = 0x2000
}
[Flags]
private enum ProtectionOptions : uint
{
PageExecuteReadWrite = 0x40
}
[Flags]
private enum MemoryFreeType : uint
{
Decommit = 0x4000
}
public static IntPtr AllocExecutionBlock(
int size, IntPtr hProcess)
{
IntPtr codeBytesPtr;
codeBytesPtr = VirtualAllocEx(
hProcess, IntPtr.Zero,
(IntPtr)size,
AllocationType.Reserve | AllocationType.Commit,
(uint)ProtectionOptions.PageExecuteReadWrite);
if (codeBytesPtr == IntPtr.Zero)
{
throw new System.ComponentModel.Win32Exception();
}
uint lpflOldProtect = 0;
if (!VirtualProtectEx(
hProcess, codeBytesPtr,
(IntPtr)size,
(uint)ProtectionOptions.PageExecuteReadWrite,
ref lpflOldProtect))
{
throw new System.ComponentModel.Win32Exception();
}
return codeBytesPtr;
}
public static bool FreeExecutionBlock(
IntPtr hProcess, IntPtr lpAddress,
IntPtr dwSize)
{
bool result = VirtualFreeEx(
hProcess, lpAddress, dwSize,
(IntPtr)MemoryFreeType.Decommit);
if (!result)
{
throw new System.ComponentModel.Win32Exception();
}
return result;
}
}
public class VirtualMemoryPtr :
System.Runtime.InteropServices.SafeHandle
{
public VirtualMemoryPtr(int memorySize) :
base(IntPtr.Zero, true)
{
IntPtr ProcessHandle =
GetCurrentProcessHandle();
IntPtr MemorySize = (IntPtr)memorySize;
AllocatedPointer =
VirtualMemoryManager.AllocExecutionBlock(
memorySize, ProcessHandle);
Disposed = false;
}
public readonly IntPtr AllocatedPointer;
bool Disposed;
public static implicit operator IntPtr(
VirtualMemoryPtr virtualMemoryPointer)
{
return virtualMemoryPointer.AllocatedPointer;
}
public override bool IsInvalid
{
get
{
return Disposed;
}
}
protected override bool ReleaseHandle()
{
if (!Disposed)
{
Disposed = true;
GC.SuppressFinalize(this);
IntPtr ProcessHandle =
GetCurrentProcessHandle();
IntPtr MemorySize = (IntPtr)AllocatedPointer.ToInt32();
VirtualMemoryManager.FreeExecutionBlock(
ProcessHandle, AllocatedPointer, MemorySize);
}
return true;
}
private static IntPtr GetCurrentProcessHandle()
{
// 实际实现中需要获取当前进程句柄
return IntPtr.Zero;
}
}
class Program
{
static unsafe void Main()
{
int memorySize = 1024;
using (VirtualMemoryPtr memoryPtr = new VirtualMemoryPtr(memorySize))
{
if (!memoryPtr.IsInvalid)
{
byte* pData = (byte*)memoryPtr;
// 可以对 pData 指向的内存进行操作
for (int i = 0; i < memorySize; i++)
{
pData[i] = (byte)i;
}
}
} // 离开 using 块时自动清理资源
}
}
在这个案例中,首先定义了
VirtualMemoryManager
类,包含了 P/Invoke 方法用于虚拟内存的分配、保护和释放。然后定义了
VirtualMemoryPtr
类,继承自
SafeHandle
用于管理分配的内存资源。在
Main
方法中,使用
using
语句创建
VirtualMemoryPtr
实例,确保资源在使用后自动清理。同时,在
unsafe
代码块中可以直接操作分配的内存。
7. 总结
C# 的平台调用和指针操作是强大而灵活的工具,能够让开发者在需要时与非托管代码进行交互,直接操作内存。但同时,这些功能也带来了一定的风险,如内存泄漏、缓冲区溢出等安全问题。因此,开发者在使用时必须严格遵循相关的指南和规则,确保代码的安全性和性能。通过合理运用平台调用和指针操作,开发者可以实现更高效、更复杂的功能,满足各种不同的开发需求。
超级会员免费看
13万+

被折叠的 条评论
为什么被折叠?



