win-wrap 0.3.4

用于Rust的Windows API的高级封装
Documentation
/*
 * Copyright (c) 2024. The RigelA open source project team and
 * its contributors reserve all rights.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * http://www.apache.org/licenses/LICENSE-2.0
 * Unless required by applicable law or agreed to in writing, software distributed under the
 * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and limitations under the License.
 */

use std::ffi::c_void;

pub use windows::Win32::System::Memory::{
    HEAP_CREATE_ALIGN_16, HEAP_CREATE_ENABLE_EXECUTE, HEAP_CREATE_ENABLE_TRACING,
    HEAP_CREATE_HARDENED, HEAP_CREATE_SEGMENT_HEAP, HEAP_DISABLE_COALESCE_ON_FREE, HEAP_FLAGS,
    HEAP_FREE_CHECKING_ENABLED, HEAP_GENERATE_EXCEPTIONS, HEAP_GROWABLE, HEAP_MAXIMUM_TAG,
    HEAP_NONE, HEAP_NO_SERIALIZE, HEAP_PSEUDO_TAG_FLAG, HEAP_REALLOC_IN_PLACE_ONLY, HEAP_TAG_SHIFT,
    HEAP_TAIL_CHECKING_ENABLED, HEAP_ZERO_MEMORY, MEM_COMMIT, MEM_DECOMMIT, MEM_FREE,
    MEM_LARGE_PAGES, MEM_RELEASE, MEM_REPLACE_PLACEHOLDER, MEM_RESERVE, MEM_RESERVE_PLACEHOLDER,
    MEM_RESET, MEM_RESET_UNDO, PAGE_ENCLAVE_DECOMMIT, PAGE_ENCLAVE_MASK, PAGE_ENCLAVE_SS_FIRST,
    PAGE_ENCLAVE_SS_REST, PAGE_ENCLAVE_THREAD_CONTROL, PAGE_ENCLAVE_UNVALIDATED, PAGE_EXECUTE,
    PAGE_EXECUTE_READ, PAGE_EXECUTE_READWRITE, PAGE_EXECUTE_WRITECOPY, PAGE_GRAPHICS_COHERENT,
    PAGE_GRAPHICS_EXECUTE, PAGE_GRAPHICS_EXECUTE_READ, PAGE_GRAPHICS_EXECUTE_READWRITE,
    PAGE_GRAPHICS_NOACCESS, PAGE_GRAPHICS_NOCACHE, PAGE_GRAPHICS_READONLY, PAGE_GRAPHICS_READWRITE,
    PAGE_GUARD, PAGE_NOACCESS, PAGE_NOCACHE, PAGE_PROTECTION_FLAGS, PAGE_READONLY, PAGE_READWRITE,
    PAGE_REVERT_TO_FILE_MAP, PAGE_TARGETS_INVALID, PAGE_TARGETS_NO_UPDATE, PAGE_WRITECOMBINE,
    PAGE_WRITECOPY, SEC_64K_PAGES, SEC_COMMIT, SEC_FILE, SEC_IMAGE_NO_EXECUTE, SEC_LARGE_PAGES,
    SEC_NOCACHE, SEC_PARTITION_OWNER_HANDLE, SEC_PROTECTED_IMAGE, SEC_RESERVE, SEC_WRITECOMBINE,
    VIRTUAL_ALLOCATION_TYPE, VIRTUAL_FREE_TYPE,
};
use windows::Win32::System::{
    Diagnostics::Debug::{ReadProcessMemory, WriteProcessMemory},
    Memory::{
        GetProcessHeap, HeapAlloc, HeapCreate, HeapDestroy, HeapFree, VirtualAllocEx, VirtualFreeEx,
    },
};

use crate::common::{close_handle, Result, HANDLE};

//noinspection SpellCheckingInspection
/**
保留、提交或更改指定进程的虚拟地址空间中内存区域的状态。 函数将它分配的内存初始化为零。
若要指定物理内存的 NUMA 节点,请参阅 virtual_alloc_ex_numa。
每个页面都有一个关联的 页面状态。 virtual_alloc_ex 函数可以执行以下操作:
• 提交保留页的区域
• 保留免费页面区域
• 同时保留和提交可用页面区域
virtual_alloc_ex 无法保留保留页。 它可以提交已提交的页面。 这意味着你可以提交一系列页面,无论它们是否已提交,并且函数不会失败。
可以使用 virtual_alloc_ex 保留页块,然后对 virtual_alloc_ex 进行其他调用,以提交保留块中的单个页面。 这使进程能够保留其虚拟地址空间的范围,而无需使用物理存储,直到需要为止。
如果 address 参数不为 NULL,则该函数使用 address 和 size 参数来计算要分配的页面区域。 整个页面范围的当前状态必须与 allocation_type 参数指定的分配类型兼容。 否则,函数将失败,并且不会分配任何页。 此兼容性要求不排除提交已提交的页面;请参阅前面的列表。
若要执行动态生成的代码,请使用 virtual_alloc_ex 分配内存,并使用 virtual_protect_ex 函数授予 PAGE_EXECUTE 访问权限。
virtual_alloc_ex函数可用于在指定进程的虚拟地址空间中保留地址窗口扩展(AWE)内存区域。然后,可以使用此内存区域根据应用程序的要求将物理页映射到虚拟内存中和映射出虚拟内存。必须在allocation_type参数中设置MEM_PHYSICAL和MEM_RESERVE值。不得设置MEM_COMMIT值。
页面保护必须设置为PAGE_READWRITE。virtual_free_ex 函数可以取消提交已提交页面、释放页面的存储,也可以同时取消提交和释放已提交页面。 它还可以释放保留页,使其成为免费页面。
创建可执行的区域时,调用程序负责在代码设置到位后,通过适当调用 flush_instruction_cache 来确保缓存一致性。 否则,尝试在新可执行区域之外执行代码可能会产生不可预知的结果。
`h_process` 进程的句柄。 函数在此进程的虚拟地址空间中分配内存。句柄必须具有 PROCESS_VM_OPERATION 访问权限。 有关详细信息,请参阅 进程安全和访问权限。
`address` 为要分配的页面区域指定所需起始地址的指针。如果要保留内存,该函数将此地址向下舍入到分配粒度的最接近倍数。如果要提交已保留的内存,该函数会将此地址向下舍入到最近的页边界。若要确定主计算机上的页面大小和分配粒度,请使用get_system_info函数。如果address为NULL,则该函数确定分配区域的位置。如果此地址位于尚未通过调用initialize_enclave进行初始化的enclave内,virtual_alloc_ex会为该地址上的enclave分配一个零页。该页面必须以前未提交,并且不会使用IntelSoftwareGuardExtensions编程模型的EEXTEND指令进行测量。如果中的地址位于你初始化的enclave中,则分配操作将失败并出现ERROR_INVALID_ADDRESS错误。对于不支持动态内存管理的enclave((即SGX1))也是如此。SGX2enclave将允许分配,并且页面必须在分配后被enclave接受。
`size` 要分配的内存区域的大小(以字节为单位)。如果address为NULL,则该函数会将size向上舍入到下一页边界。如果address不为NULL,则该函数将分配从address到address+size范围内包含一个或多个字节的所有页。例如,这意味着跨越页边界的2字节范围会导致函数分配这两个页面。
`allocation_type` 内存分配的类型。 此参数必须包含以下值之一。
- MEM_COMMIT 从指定保留内存页的磁盘) 上的总内存大小和分页文件 (分配内存费用。 函数还保证当调用方稍后最初访问内存时,内容将为零。 除非实际访问虚拟地址,否则不会分配实际物理页。若要在一个步骤中保留和提交页面,请使用 调用 virtual_alloc_ex MEM_COMMIT | MEM_RESERVE。除非已保留整个范围,否则尝试通过指定 MEM_COMMIT 而不 指定MEM_RESERVE 和非 NULL address 来提交特定地址范围。 生成的错误代码 ERROR_INVALID_ADDRESS。尝试提交已提交的页面不会导致函数失败。 这意味着可以提交页面,而无需首先确定每个页面的当前承诺状态。如果 address 指定 enclave 中的地址,则必须MEM_COMMIT allocation_type。
- MEM_RESERVE 保留进程的虚拟地址空间范围,而无需在内存或磁盘上的分页文件中分配任何实际物理存储。通过使用 MEM_COMMIT 再次调用 virtual_alloc_ex 来提交保留页。 若要在一个步骤中保留和提交页面,请使用 调用 virtual_alloc_ex MEM_COMMIT | MEM_RESERVE。其他内存分配函数(如 malloc 和 local_alloc)在释放内存之前无法使用保留内存。
- MEM_RESET 指示 address 和 size 指定的内存范围中的数据不再感兴趣。 不应从分页文件读取或写入页面。 但是,内存块稍后将再次使用,因此不应解除提交。 此值不能与任何其他值一起使用。使用此值并不能保证使用 MEM_RESET 操作的范围将包含零。 如果希望范围包含零,请取消提交内存,然后重新提交。使用 MEM_RESET 时, virtual_alloc_ex 函数将忽略protect 的值。 但是,仍必须将 protect 设置为有效地保护值,例如 PAGE_NOACCESS。如果使用 MEM_RESET 并且内存范围映射到文件,则 virtual_alloc_ex 将返回错误。 仅当共享视图映射到分页文件时,才可接受该视图。
- MEM_RESET_UNDO 应仅对之前成功应用MEM_RESET的地址范围调用MEM_RESET_UNDO。 它指示调用方对 address 和 size 指定的指定内存范围中的数据感兴趣,并尝试反转 MEM_RESET的影响。 如果该函数成功,则表示指定地址范围中的所有数据都保持不变。 如果函数失败,则地址范围中至少有一些数据已替换为零。此值不能与任何其他值一起使用。 如果 对 之前未MEM_RESET的地址范围调用 MEM_RESET_UNDO ,则行为未定义。 指定 MEM_RESET时, virtual_alloc_ex 函数将忽略 protect 的值。 但是,仍必须将 protect 设置为有效地保护值,例如 PAGE_NOACCESS。Windows Server 2008 R2、Windows 7、Windows Server 2008、Windows Vista、Windows Server 2003 和 Windows XP: 在Windows 8和Windows Server 2012之前,不支持MEM_RESET_UNDO标志。
此参数还可以按指示指定以下值。
- MEM_LARGE_PAGES 使用 大页支持分配内存。大小和对齐方式必须是大页最小值的倍数。 若要获取此值,请使用 get_large_page_minimum 函数。如果指定此值,还必须指定 MEM_RESERVE 和 MEM_COMMIT。
- MEM_PHYSICAL 保留可用于映射 地址窗口扩展 (AWE) 页的地址范围。此值必须与 MEM_RESERVE 一起使用,不能与其他值一起使用。
- MEM_TOP_DOWN 在可能的最高地址分配内存。 这比常规分配慢,尤其是在有许多分配时。
`protect` 要分配的页区域的内存保护。 如果正在提交页面,则可以指定任何一个 内存保护常量。如果 address 指定 enclave 中的地址, 则 protect 不能为以下任何值:
• PAGE_NOACCESS
• PAGE_GUARD
• PAGE_NOCACHE
• PAGE_WRITECOMBINE
为 enclave 分配动态内存时, protect 参数必须 PAGE_READWRITE 或 PAGE_EXECUTE_READWRITE。
*/
pub fn virtual_alloc_ex(
    h_process: HANDLE,
    address: Option<*const c_void>,
    size: usize,
    allocation_type: VIRTUAL_ALLOCATION_TYPE,
    protect: PAGE_PROTECTION_FLAGS,
) -> *mut c_void {
    unsafe { VirtualAllocEx(h_process, address, size, allocation_type, protect) }
}

//noinspection SpellCheckingInspection
/**
释放、取消提交或释放和取消提交指定进程的虚拟地址空间中的内存区域。
`h_process` 进程的句柄。 函数释放进程的虚拟地址空间中的内存。句柄必须具有 PROCESS_VM_OPERATION 访问权限。 有关详细信息,请参阅 进程安全和访问权限。
`address` 指向要释放的内存区域的起始地址的指针。如果 free_type 参数 MEM_RELEASE,则 address 必须是保留区域时 virtual_alloc_ex 函数返回的基址。
`size` 要释放的内存区域的大小(以字节为单位)。如果 free_type 参数 MEM_RELEASE,则 size 必须为 0 (零) 。 函数释放在 对 virtual_alloc_ex 的初始分配调用中保留的整个区域。如果 free_type MEM_DECOMMIT,则函数将取消提交包含从 address 参数到 (address+size)范围内的一个或多个字节的所有内存页。 例如,这意味着跨越页边界的 2 字节内存区域会导致两个页面都解除提交。 如果 address 是 virtual_alloc_ex 返回的基址,而 size 为 0 (零) ,则函数将取消提交 virtual_alloc_ex 分配的整个区域。 之后,整个区域将处于保留状态。
`free_type` 释放操作的类型。 此参数须为下列值之一。
- MEM_DECOMMIT 取消提交已提交页面的指定区域。 操作后,页面将处于保留状态。如果尝试取消提交未提交的页面,函数不会失败。 这意味着,无需先确定当前承诺状态,即可取消提交一系列页面。当 address 参数提供 enclave 的基址时,不支持MEM_DECOMMIT值。 对于不支持动态内存管理 ((即 SGX1) )的 enclave 也是如此。 SGX2 enclave 允许MEM_DECOMMIT enclave 中的任意位置。
- MEM_RELEASE释放指定的页面区域或占位符(占位符,释放地址空间并可用于)的其他分配。在执行该操作之后,这些页面将处于可用状态。如果指定此值,size必须为0(零),并且address必须指向保留区域时virtual_alloc函数返回的基址。如果未满足上述任一条件,该函数将失败。如果当前已提交区域中的任何页面,该函数将首先取消提交,然后释放它们。如果尝试释放处于不同状态(一些保留和一些已提交)的页面,函数不会失败。这意味着,无需首先确定当前承诺状态即可发布一系列页面。
使用 MEM_RELEASE 时,此参数还可以指定以下值之一。
- MEM_COALESCE_PLACEHOLDERS 若要合并两个相邻占位符,请指定 MEM_RELEASE | MEM_COALESCE_PLACEHOLDERS。 合并占位符时, address 和 size 必须与要合并的占位符的总体范围完全匹配。
- MEM_PRESERVE_PLACEHOLDER 使用 virtual_alloc2 或virtual2alloc_from_app) 将占位符替换为专用分配后,将分配释放回占位符 (。若要将占位符拆分为两个占位符,请指定 MEM_RELEASE | MEM_PRESERVE_PLACEHOLDER。
*/
pub fn virtual_free(
    h_process: HANDLE,
    address: *mut c_void,
    size: usize,
    free_type: VIRTUAL_FREE_TYPE,
) {
    unsafe { VirtualFreeEx(h_process, address, size, free_type) }.unwrap_or(())
}

/**
read_process_memory 将指定地址范围中的数据从指定进程的地址空间复制到当前进程的指定缓冲区中。 具有PROCESS_VM_READ访问权限的句柄的任何进程都可以调用函数。
要读取的整个区域必须可访问,如果无法访问,则函数将失败。
`h_process` 包含正在读取的内存的进程句柄。 句柄必须具有对进程的PROCESS_VM_READ访问权限。
`base_address` 指向从中读取的指定进程中基址的指针。 在进行任何数据传输之前,系统会验证指定大小的基址和内存中的所有数据是否可供读取访问,如果无法访问,则函数将失败。
`n_size` 要从指定进程读取的字节数。
*/
pub fn read_process_memory(
    h_process: HANDLE,
    base_address: *const c_void,
    buffer: *mut c_void,
    n_size: usize,
) -> (bool, usize) {
    unsafe {
        let mut len = std::mem::zeroed();
        let res =
            ReadProcessMemory(h_process, base_address, buffer, n_size, Some(&mut len)).is_ok();
        (res, len)
    }
}

/**
将数据写入到指定进程中的内存区域。要写入的整个区域必须可访问,否则操作将失败。
返回接收传输到指定进程的字节数。
write_process_memory 将数据从当前进程中的指定缓冲区复制到指定进程的地址范围。 任何具有 PROCESS_VM_WRITE 句柄且PROCESS_VM_OPERATION访问要写入的进程的进程都可以调用 函数。 通常(但并非总是)正在调试包含正在写入的地址空间的进程。
要写入到的整个区域必须可访问,如果无法访问,则函数将失败。
`h_process` 要修改的进程内存的句柄。 句柄必须具有对进程的PROCESS_VM_WRITE和PROCESS_VM_OPERATION访问权限。
`base_address` 指向将数据写入到的指定进程中基址的指针。 在进行数据传输之前,系统会验证指定大小的基址和内存中的所有数据是否可供写入访问,如果无法访问,则函数将失败。
`buffer` 指向缓冲区的指针,该缓冲区包含要写入指定进程的地址空间中的数据。
`n_size` 要写入指定进程的字节数。
*/
pub fn write_process_memory(
    h_process: HANDLE,
    base_address: *const c_void,
    buffer: *const c_void,
    n_size: usize,
) -> usize {
    unsafe {
        let mut written = std::mem::zeroed();
        WriteProcessMemory(h_process, base_address, buffer, n_size, Some(&mut written))
            .unwrap_or(());
        written
    }
}

/**
从堆中分配内存块。 分配的内存不可移动。
如果函数成功,则返回值是指向已分配内存块的指针。
如果函数失败并且您尚未指定 HEAP_GENERATE_EXCEPTIONS,则返回值为 NULL。
如果函数失败并且已指定 HEAP_GENERATE_EXCEPTIONS,则函数可能会生成下表中列出的任一异常。 特定例外取决于堆损坏的性质。 有关详细信息,请参阅 GetExceptionCode。
异常代码 | 说明
STATUS_NO_MEMORY | 由于缺少可用内存或堆损坏,分配尝试失败。
STATUS_ACCESS_VIOLATION | 由于堆损坏或函数参数不正确,分配尝试失败。
如果函数失败,则它不会调用 set_last_error。 应用程序无法调用 get_last_error 以获取扩展错误信息。
如果 heap_alloc 函数成功,它将分配至少请求的内存量。
若要从进程的默认堆分配内存,请将 heap_alloc 与 get_process_heap 函数返回的句柄一起使用。
若要释放 heap_alloc 分配的内存块,请使用 heap_free 函数。
heap_alloc 分配的内存不可移动。 heap_alloc 返回的地址在释放或重新分配内存块之前有效;内存块不需要锁定。 由于系统无法压缩专用堆,因此它可能会碎片化。
heap_alloc 返回的内存对齐方式在 WinNT.h 中MEMORY_ALLOCATION_ALIGNMENT:
```cpp
#if defined(_WIN64) || defined(_M_ALPHA)
#define MEMORY_ALLOCATION_ALIGNMENT 16
#else
#define MEMORY_ALLOCATION_ALIGNMENT 8
#endif
```
以各种分配大小分配大量内存的应用程序可以使用 低碎片堆 来减少堆碎片。
当两个或多个线程尝试从同一堆同时分配或释放块时,序列化可确保相互排斥。 序列化的性能成本很小,但每当多个线程从同一个堆分配和释放内存时,必须使用它。 设置 HEAP_NO_SERIALIZE 值可消除堆上的相互排斥。 如果不进行序列化,使用同一堆句柄的两个或多个线程可能会尝试同时分配或释放内存,这可能会导致堆损坏。 因此,只能在以下情况下安全地使用 HEAP_NO_SERIALIZE 值:
• 进程只有一个线程。
• 进程有多个线程,但只有一个线程调用特定堆的堆函数。
• 进程具有多个线程,应用程序提供自己的机制,用于对特定堆进行相互排斥。
`h_heap` 要从中分配内存的堆的句柄。 此句柄由 heap_create 或 get_process_heap 函数返回。
`flags` 堆分配选项。 指定这些值中的任何一个都将替代使用 heap_create 创建堆时指定的相应值。 此参数可使用以下一个或多个值。
值 | 含义
HEAP_GENERATE_EXCEPTIONS 0x00000004 | 系统将引发异常以指示函数失败(例如内存不足情况),而不是返回 NULL。若要确保为此函数的所有调用生成异常,请在调用 heap_create 时指定HEAP_GENERATE_EXCEPTIONS。 在这种情况下,无需在此函数调用中额外指定 HEAP_GENERATE_EXCEPTIONS 。
HEAP_NO_SERIALIZE 0x00000001 | 序列化访问将不用于此分配。若要确保禁用对此函数的所有调用的序列化访问,请在调用 heap_create 中指定HEAP_NO_SERIALIZE。 在这种情况下,无需在此函数调用中额外指定 HEAP_NO_SERIALIZE 。访问进程的默认堆时,不应指定此值。 系统可能会在应用程序的进程中创建其他线程,例如同时访问进程的默认堆的 CTRL+C 处理程序。
HEAP_ZERO_MEMORY 0x00000008 | 分配的内存将初始化为零。 否则,内存不会初始化为零。
`bytes` 要分配的字节数。如果 h_heap 参数指定的堆是“不可增长的”堆, 则 bytes 必须小于 0x7FFF8。 可以通过使用非零值调用 heap_create 函数来创建不可增长的堆。
*/
pub fn heap_alloc(h_heap: HANDLE, flags: HEAP_FLAGS, bytes: usize) -> *mut c_void {
    unsafe { HeapAlloc(h_heap, flags, bytes) }
}

/**
释放由 heap_alloc 或 heap_re_alloc 函数从堆分配的内存块。
`h_heap` 要释放其内存块的堆的句柄。 此句柄由 heap_create 或 get_process_heap 函数返回。
堆免费选项。 使用 HeapCreate 函数创建堆时,指定以下值将替代 flOptions 参数中指定的相应值。
值 | 含义
HEAP_NO_SERIALIZE 0x00000001 | 不会使用序列化访问。若要确保对此函数的所有调用禁用序列化访问,请在对 heap_create 的调用中指定HEAP_NO_SERIALIZE。 在这种情况下,无需在此函数调用中额外指定 HEAP_NO_SERIALIZE 。访问进程堆时不要指定此值。 系统可能会在应用程序的进程中创建其他线程,例如同时访问进程堆的 CTRL+C 处理程序。
`mem` 指向要释放的内存的指针。 此指针由 heap_alloc 或 heap_re_alloc 函数返回。 此指针可以为 NULL。
*/
pub fn heap_free(h_heap: HANDLE, flags: HEAP_FLAGS, mem: *const c_void) -> bool {
    let mem = if mem.is_null() { None } else { Some(mem) };
    unsafe { HeapFree(h_heap, flags, mem).is_ok() }
}

/**
查询调用进程的默认堆的句柄。 然后,可以在对堆函数的后续调用中使用此句柄。
如果函数成功,则返回值是调用进程的堆的句柄。
如果函数失败,则返回值为 NULL。 要获得更多的错误信息,请调用 get_last_error。
get_process_heap 函数获取调用进程的默认堆的句柄。 进程可以使用此句柄从进程堆分配内存,而无需先使用 heap_create 函数创建专用堆。
Windows Server 2003 和 Windows XP: 若要为进程的默认堆启用低碎片堆,请使用 get_process_heap 返回的句柄调用 heap_set_information 函数。
*/
pub fn get_process_heap() -> HANDLE {
    unsafe { GetProcessHeap().unwrap_or(Default::default()) }
}

/**
创建可由调用进程使用的专用堆对象。 函数在进程的虚拟地址空间中保留空间,并为此块的指定初始部分分配物理存储。
如果函数成功,则返回值是新创建的堆的句柄。
如果函数失败,则返回值为 NULL。 要获得更多的错误信息,请调用 get_last_error。
heap_create 函数创建一个专用堆对象,调用进程可以使用 heap_alloc 函数从该对象分配内存块。 初始大小确定最初为堆分配的已提交页数。 最大大小确定保留页的总数。 这些页面在进程的虚拟地址空间中创建一个块,堆可以增长到其中。 如果 heap_alloc 的请求超过已提交页面的当前大小,则如果物理存储可用,则会自动从此预留空间提交其他页面。
Windows Server 2003 和 Windows XP: 默认情况下,新创建的专用堆是标准堆。 若要启用低碎片堆,请使用专用堆的句柄调用 heap_set_information 函数。
专用堆对象的内存只能由创建它的进程访问。 如果动态链接库 (DLL) 创建专用堆,则会在调用 DLL 的进程地址空间中创建堆,并且只有该进程才能访问该堆。
系统使用专用堆中的内存来存储堆支持结构,因此并非所有指定的堆大小都可供进程使用。 例如,如果 heap_alloc 函数从最大大小为 64K 的堆请求 64 KB (K) ,则请求可能会因系统开销而失败。
如果未在简单(默认)指定HEAP_NO_SERIALIZE,则堆会在调用过程中序列化访问。 当两个或多个线程同时尝试从同一堆分配或释放块时,序列化可确保相互排斥。 序列化的性能成本很小,但每当多个线程从同一个堆分配和释放内存时,必须使用它。 heap_lock 和 heap_unlock 函数可用于阻止和允许访问序列化堆。
设置 HEAP_NO_SERIALIZE 会消除堆上的相互排斥。 如果不进行序列化,使用同一堆句柄的两个或多个线程可能会尝试同时分配或释放内存,这可能会导致堆损坏。 因此, 只能在 以下情况下安全地使用HEAP_NO_SERIALIZE:
• 进程只有一个线程。
• 进程有多个线程,但只有一个线程调用特定堆的堆函数。
• 进程具有多个线程,应用程序提供自己的机制,用于对特定堆进行相互排斥。
如果在使用 HEAP_NO_SERIALIZE 标志创建的堆上调用 heap_lock 和 heap_unlock 函数,则结果未定义。
若要获取进程的默认堆的句柄,请使用 get_process_heap 函数。 若要获取对调用进程处于活动状态的默认堆和专用堆的句柄,请使用 get_process_heaps 函数。
`options` 堆分配选项。 这些选项通过调用堆函数影响对新堆的后续访问。 此参数可以是 0 或以下一个或多个值。
值 | 含义
HEAP_CREATE_ENABLE_EXECUTE 0x00040000 | 如果硬件强制实施 数据执行防护,则从此堆分配的所有内存块都允许代码执行。 在从堆运行代码的应用程序中使用此标志堆。 如果未指定 HEAP_CREATE_ENABLE_EXECUTE ,并且应用程序尝试从受保护的页面运行代码,则应用程序将收到异常,状态代码 STATUS_ACCESS_VIOLATION。
HEAP_GENERATE_EXCEPTIONS 0x00000004 | 系统引发异常以指示失败 (例如,内存不足条件) 调用 heap_alloc 和 heap_re_alloc 而不是返回 NULL。
HEAP_NO_SERIALIZE 0x00000001 | 当堆函数访问此堆时,不使用序列化访问。 此选项适用于所有后续堆函数调用。 或者,可以在单个堆函数调用上指定此选项。无法为使用此选项创建的堆启用低碎片化堆 (LFH) 。不能锁定使用此选项创建的堆。
`initial_size` 堆的初始大小(以字节为单位)。 此值确定为堆提交的初始内存量。 该值向上舍入为系统页面大小的倍数。 该值必须小于 maximum_size。如果此参数为 0,则函数将提交一页。 若要确定主计算机上的页面大小,请使用 get_system_info 函数。
`maximum_size` 堆的最大大小(以字节为单位)。 heap_create 函数将 maximum_size 舍入到系统页大小的倍数,然后在堆的进程虚拟地址空间中保留该大小的块。 如果 heap_alloc 或 heap_re_alloc 函数发出的分配请求超过 initial_size 指定的大小,系统会为堆提交额外的内存页,最大大小为堆的最大大小。如果 maximum_size 不为零,则堆大小是固定的,并且不能增长到超过最大大小。 此外,对于 32 位进程,可从堆中分配的最大内存块略小于 512 KB,而对于 64 位进程,则略低于 1,024 KB。 即使堆的最大大小足以包含块,分配较大块的请求也会失败。如果 maximum_size 为 0,堆大小可能会增大。 堆的大小仅受可用内存的限制。 分配大于固定大小堆限制的内存块的请求不会自动失败;相反,系统会调用 virtual_alloc 函数来获取大型块所需的内存。 需要分配大量内存块的应用程序应将 maximum_size 设置为 0。
*/
pub fn heap_create(
    options: HEAP_FLAGS,
    initial_size: usize,
    maximum_size: usize,
) -> Result<HANDLE> {
    unsafe { HeapCreate(options, initial_size, maximum_size) }
}

/**
销毁指定的堆对象。
heap_destroy 会取消提交并释放私有堆对象的所有页面,并使堆的句柄失效。
进程可以调用 heap_destroy ,而无需先调用 heap_free 函数来释放从堆中分配的内存。
`h_heap` 要销毁的堆的句柄。 此句柄由 heap_create 函数返回。 请勿使用 get_process_heap 函数返回的进程堆的句柄。
*/
pub fn heap_destroy(h_heap: HANDLE) -> bool {
    unsafe { HeapDestroy(h_heap).is_ok() }
}

/// 堆内存操作
pub struct HeapMemory {
    handle: HANDLE,
    allow_destroy: bool,
}

impl HeapMemory {
    /**
    创建新实例。
    `flags` 内存标志。
    `size` 内存大小(字节数)。
    */
    pub fn new(flags: HEAP_FLAGS, initial_size: usize, maximum_size: usize) -> Result<Self> {
        match heap_create(flags, initial_size, maximum_size) {
            Ok(h) => Ok(Self {
                handle: h,
                allow_destroy: true,
            }),
            Err(e) => Err(e),
        }
    }

    /**
    分配内存。
    `size` 要分配的大小。
    */
    pub fn alloc(&self, size: usize) -> *mut c_void {
        heap_alloc(self.handle, HEAP_NONE, size)
    }

    /**
    释放内存。
    `mem` 内存指针。
    */
    pub fn free(&self, mem: *const c_void) -> bool {
        heap_free(self.handle, HEAP_NONE, mem)
    }
}

impl Default for HeapMemory {
    fn default() -> Self {
        Self {
            handle: get_process_heap(),
            allow_destroy: false,
        }
    }
}

impl Drop for HeapMemory {
    fn drop(&mut self) {
        if self.allow_destroy {
            heap_destroy(self.handle);
        }
    }
}

/// 进程内内存操作
pub struct InProcessMemory {
    process_handle: HANDLE,
    heap: HeapMemory,
    ptr: *mut c_void,
    size: usize,
}

impl InProcessMemory {
    /**
    创建新实例。
    `pid` 进程ID。
    `size` 内存大小(字节数)。
    */
    pub fn new(process_handle: HANDLE, size: usize) -> Result<Self> {
        let mem = virtual_alloc_ex(process_handle, None, size, MEM_COMMIT, PAGE_READWRITE);
        match HeapMemory::new(HEAP_NO_SERIALIZE, size, size) {
            Ok(heap) => Ok(Self {
                process_handle,
                heap,
                ptr: mem,
                size,
            }),
            Err(e) => Err(e),
        }
    }

    /**
    获取目标进程内存的可变原始指针。
    */
    pub fn as_ptr_mut(&self) -> *mut c_void {
        self.ptr
    }

    /**
    获取目标进程内存的原始指针。
    */
    pub fn as_ptr(&self) -> *const c_void {
        self.ptr
    }

    /**
    读取内存。
    `convert` 一个闭包函数,在内存读取之后调用,可以做数据转换。
    */
    pub fn read<T: Sized>(&self, convert: impl Fn(*const c_void) -> T) -> Option<T> {
        let mem_local = self.heap.alloc(self.size + 1);
        unsafe { mem_local.write_bytes(b'\0', self.size + 1) };
        read_process_memory(self.process_handle, self.ptr, mem_local, self.size);
        let data = convert(mem_local);
        self.heap.free(mem_local);
        Some(data)
    }

    /**
    写入内存数据(返回实际写入的大小)。
    `ptr` 要写入的数据的指针。
    `size` 要写入的大小(字节数)。
    */
    pub fn write(&self, ptr: *const c_void, size: usize) -> usize {
        write_process_memory(self.process_handle, self.ptr, ptr, size)
    }
}

impl Drop for InProcessMemory {
    fn drop(&mut self) {
        virtual_free(self.process_handle, self.ptr, 0, MEM_RELEASE);
        close_handle(self.process_handle);
    }
}