56、C 平台调用与指针操作全解析

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# 的平台调用和指针操作是强大而灵活的工具,能够让开发者在需要时与非托管代码进行交互,直接操作内存。但同时,这些功能也带来了一定的风险,如内存泄漏、缓冲区溢出等安全问题。因此,开发者在使用时必须严格遵循相关的指南和规则,确保代码的安全性和性能。通过合理运用平台调用和指针操作,开发者可以实现更高效、更复杂的功能,满足各种不同的开发需求。

内容概要:本文是一份针对2025年中国企业品牌传播环境撰写的《网媒体发稿白皮书》,聚焦企业媒体发稿的策略制定、渠道选择效果评估难题。通过分析当前企业面临的资源分散、内容同质、效果难量化等核心痛点,系统性地介绍了新闻媒体、央媒、地方官媒和自媒体四大渠道的特点适用场景,并深度融合“传声港”AI驱动的新媒体平台能力,提出“策略+工具+落地”的一体化解决方案。白皮书详细阐述了传声港在资源整合、AI智能匹配、舆情监测、合规审核及链路效果追踪方面的技术优势,构建了涵盖曝光、互动、转化品牌影响力的多维评估体系,并通过快消、科技、零售等行业的实战案例验证其有效性。最后,提出了按企业发展阶段和营销节点定制的媒体组合策略,强调本土化传播政府关系协同的重要性,助力企业实现品牌声量实际转化的双重增长。; 适合人群:企业市场部负责人、品牌方管理者、公关传播从业者及从事数字营销的相关人员,尤其适用于初创期至成熟期不同发展阶段的企业决策者。; 使用场景及目标:①帮助企业科学制定媒体发稿策略,优化预算分配;②解决渠道对接繁琐、投放不精准、效果不可衡量等问题;③指导企业在重大营销节点(如春节、双11)开展高效传播;④提升品牌权威性、区域渗透力危机应对能力; 阅读建议:建议结合自身企业所处阶段和发展目标,参考文中提供的“传声港服务组合”“预算分配建议”进行策略匹配,同时重视AI工具在投放、监测优化中的实际应用,定期复盘数据以实现持续迭代。
先展示下效果 https://pan.quark.cn/s/987bb7a43dd9 VeighNa - By Traders, For Traders, AI-Powered. Want to read this in english ? Go here VeighNa是一套基于Python的开源量化交易系统开发框架,在开源社区持续不断的贡献下一步步成长为多功能量化交易平台,自发布以来已经积累了众多来自金融机构或相关领域的用户,包括私募基金、证券公司、期货公司等。 在使用VeighNa进行二次开发(策略、模块等)的过程中有任何疑问,请查看VeighNa项目文档,如果无法解决请前往官方社区论坛的【提问求助】板块寻求帮助,也欢迎在【经验分享】板块分享你的使用心得! 想要获取更多关于VeighNa的资讯信息? 请扫描下方二维码添加小助手加入【VeighNa社区交流微信群】: AI-Powered VeighNa发布十周年之际正式推出4.0版本,重磅新增面向AI量化策略的vnpy.alpha模块,为专业量化交易员提供一站式多因子机器学习(ML)策略开发、投研和实盘交易解决方案: :bar_chart: dataset:因子特征工程 * 专为ML算法训练优化设计,支持高效批量特征计算处理 * 内置丰富的因子特征表达式计算引擎,实现快速一键生成训练数据 * Alpha 158:源于微软Qlib项目的股票市场特征集合,涵盖K线形态、价格趋势、时序波动等多维度量化因子 :bulb: model:预测模型训练 * 提供标准化的ML模型开发模板,大幅简化模型构建训练流程 * 统一API接口设计,支持无缝切换不同算法进行性能对比测试 * 集成多种主流机器学习算法: * Lass...
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值