这篇我们看一个”容错“”节省“的实例。一下是一个Win32API的声明(转载请指明出处)
LONG WINAPI RegEnumKeyEx(
__in HKEY hKey,
__in DWORD dwIndex,
__out LPTSTR lpName,
__inout LPDWORD lpcName,
__reserved LPDWORD lpReserved,
__inout LPTSTR lpClass,
__inout_opt LPDWORD lpcClass,
__out_opt PFILETIME lpftLastWriteTime
);
节省
这个函数底层是使用ZwEnumerateKey,使用过该函数的同学应该知道,该函数根据传入的KEY_INFORMATION_CLASS不同而查询该项不同结构体的数据。我们来看两个不同结构体
typedef struct _KEY_BASIC_INFORMATION {
LARGE_INTEGER LastWriteTime;
ULONG TitleIndex;
ULONG NameLength;
WCHAR Name[1];
} KEY_BASIC_INFORMATION, *PKEY_BASIC_INFORMATION;
typedef struct _KEY_NODE_INFORMATION {
LARGE_INTEGER LastWriteTime;
ULONG TitleIndex;
ULONG ClassOffset;
ULONG ClassLength;
ULONG NameLength;
WCHAR Name[1];
} KEY_NODE_INFORMATION, *PKEY_NODE_INFORMATION;
可见到NODE比BASIC多出ClassOffset和ClassLength和藏在Name中的Class信息。RegEnumKeyEx要获取的信息中是可以通过是否为NULL来定的,如果你不想获取Class的信息,可以将lpClass和lpcClass指定为NULL。那么Reactos中如何实现的呢?我们先思考下,如果我们在NtEnumerateKey中一股脑儿使用KEY_NODE_INFORMATION去查询,是很简单,但是是不是有点浪费呢(也许用户不用查询Class信息)?如果使用KEY_BASIC_INFORMATION,则如果用户要查询Class信息,则我们将查询不到。或许你想到了——特殊情况特殊处理,是的Reactos就是如此做的。下面看一段我修改后的代码
lpKeyInfo = HeapAlloc( GetProcessHeap(), 0, BufferSize );
if ( NULL == lpKeyInfo ) {
// 分配失败
lRes = STATUS_MEMORY_NOT_ALLOCATED;
break;
}
ULONG ResultSize = 0;
// 查询不同类型的结构体取决于lpClass是否需要查询
lRes = GETORIFUNC(EnumerateKey)(
hKeyHandle,
dwIndex,
lpClass ? KeyNodeInformation : KeyBasicInformation,
lpKeyInfo,
BufferSize,
&ResultSize );
CHECKRESULT(lRes);
// 定义一个联合体
typedef union {
KEY_NODE_INFORMATION Node;
KEY_BASIC_INFORMATION Basic;
} un;
un* KeyInfo = (un*)lpKeyInfo;
if ( NT_FAILED(lRes) ) {
break;
}
else {
if ( NULL == lpClass ) {
// 如果lpClass不需要查询,则使用KEY_BASIC_INFORMATION结构体中数据
if ( KeyInfo->Basic.NameLength > NameLength ) {
// 承载的空间不够
lRes = STATUS_BUFFER_OVERFLOW;
}
else {
RtlCopyMemory( lpName, KeyInfo->Basic.Name, KeyInfo->Basic.NameLength );
// 返回的长度不包含结尾符
*lpcName = (DWORD)( KeyInfo->Basic.NameLength / sizeof(WCHAR) );
// 设置结尾符
lpName[*lpcName] = (WCHAR)0;
}
}
else {
// 如果lpClass需要查询,则使用KEY_NODE_INFORMATION结构体中数据
if ( KeyInfo->Node.NameLength > NameLength || KeyInfo->Node.ClassLength > ClassLength ) {
// 承载的空间不够
lRes = STATUS_BUFFER_OVERFLOW;
}
else {
// 拷贝数据到内存中
RtlCopyMemory( lpName, KeyInfo->Node.Name, KeyInfo->Node.NameLength );
// 设置返回的大小,不包含结尾符
*lpcName = KeyInfo->Node.NameLength / sizeof(WCHAR);
// 设置结尾符
lpName[*lpcName] = (WCHAR)0;
// 拷贝数据到内存中
RtlCopyMemory( lpClass,
(PVOID)((ULONG_PTR)KeyInfo->Node.Name + KeyInfo->Node.ClassOffset),
KeyInfo->Node.ClassLength );
// 设置返回的大小,不包含结尾符
*lpcClass = (DWORD)(KeyInfo->Node.ClassLength / sizeof(WCHAR));
// 设置结尾符
lpClass[*lpcClass] = (WCHAR)0;
}
}
if ( lRes == STATUS_SUCCESS && NULL != lpftLastWriteTime ) {
if ( lpClass == NULL ) {
// 如果lpClass需要查询,则使用KEY_NODE_INFORMATION结构体中数据
lpftLastWriteTime->dwLowDateTime = KeyInfo->Basic.LastWriteTime.u.LowPart;
lpftLastWriteTime->dwHighDateTime = KeyInfo->Basic.LastWriteTime.u.HighPart;
}
else {
// 如果lpClass需要查询,则使用KEY_NODE_INFORMATION结构体中数据
lpftLastWriteTime->dwLowDateTime = KeyInfo->Node.LastWriteTime.u.LowPart;
lpftLastWriteTime->dwHighDateTime = KeyInfo->Node.LastWriteTime.u.HighPart;
}
}
}
Reactos使用了联合体解决了这个问题。
容错。
我们写的API,往往会接受调用方传入的一些数据。如果这个数据是个很大的且没有固定结构的数据时,那么就要非常注意这个空间的大小了。如RegEnumKeyEx函数就接受了两个用户传入的空间及其大小。
在我们重写的RegEnumKey中对用户传入的数据进行填充前,我们要先准确无误地获取数据,而用户传入的空间和大小我们不能用,因为我们不知道他对不对,于是我们要先分配一个适合大小的空间,调用NtEnumerateKey得到数据后再对用户传入空间大小进行判断,对空间进行填充。但是这个空间大小如何定义呢?有一种办法就是不断试错,通过ResultLength参数得到适合的空间大小。
但是是不是很费呢?是的,Reactos对RegEnumKey的实现则是利用用户传入的空间大小,而没有用其传入的空间,这样一旦空间过小,会快速发现,而不用等数据都查完了才发现用户传入的空间太小。但是现在存在一个问题,如果用户传入的空间大小特别大,实际用不到这么大的数据,那怎么办?难道我们也要听从用户分配一个巨大的内存空间么?不是,我们看看Reactos的做法(我修改后的代码)
LPVOID lpKeyInfo = NULL;
do {
ULONG NameLength = 0;
if ( *lpcName > 0 ) {
NameLength = min( *lpcName - 1, MAX_PATH ) * sizeof(WCHAR);
}
else {
NameLength = 0;
}
ULONG BufferSize = 0;
ULONG ClassLength = 0;
if ( lpClass ) {
if ( *lpcClass > 0 ) {
ClassLength = min( *lpcClass - 1, MAX_PATH ) * sizeof(WCHAR);
}
else {
ClassLength = 0;
}
// +3 再& ~3是为了让大小按4字节对齐且取其上限
// 如果存在lpClass,则要将ClassLength的长度算进去
// 如果存在lpClass,则要使用KEY_NODE_INFORMATION结构体
BufferSize = ( ( sizeof(KEY_NODE_INFORMATION) + NameLength + 3 ) & ~3 ) + ClassLength;
}
else {
// 如果不存在lpClass,则使用KEY_BASIC_INFORMATION结构体
BufferSize = sizeof(KEY_BASIC_INFORMATION) + NameLength;
}
// 在堆上分配一段适合大小的空间
lpKeyInfo = HeapAlloc( GetProcessHeap(), 0, BufferSize );
它在别人传入的空间-1和260之间选了一个最小值。如果调用方传了一个巨大的空间大小,我们也就分配260个WCHAR的大小。可能有人问:那么如果Class和KeyNamed的长度就是长于260呢?好问题!Reactos系统中Class和KeyName的最大长度就是260,何来长于260的名字呢?我在我电脑上刚做了实验,将某键名改成250个1,Regedit就会报错,说名字太长。
这种容错还用在RegQueryValueEx的实现中,以下列出我修改后的部分代码
NTSTATUS WINAPI OriRegQueryValueEx(
HANDLE KeyHandle,
LPCWSTR lpValueName,
LPDWORD lpReserved,
LPDWORD lpType,
LPBYTE lpData,
LPDWORD lpcbData )
{
NTSTATUS lRes = STATUS_INVALID_PARAMETER;
char buffer[256] = {0};
char *buf_ptr = buffer;
do {
KEY_VALUE_PARTIAL_INFORMATION *info = (KEY_VALUE_PARTIAL_INFORMATION *)buffer;
// KEY_VALUE_PARTIAL_INFORMATION结构的必要结构体长度
static const int info_size = offsetof( KEY_VALUE_PARTIAL_INFORMATION, Data );
// 参数判断
if ( ( NULL != lpData && NULL == lpcbData ) || lpReserved ) {
return STATUS_INVALID_PARAMETER;
}
UNICODE_STRING name_str;
RtlInitUnicodeString_( &name_str, lpValueName );
// 取一段比较合理的空间大小,这样避免用户传入过大的空间大小
DWORD total_size = 0;
if ( NULL != lpData ) {
total_size = min( sizeof(buffer), *lpcbData + info_size );
}
else {
total_size = info_size;
if ( NULL != lpcbData ) {
*lpcbData = 0;
}
}
/* this matches Win9x behaviour - NT sets *type to a random value */
if ( lpType ) {
*lpType = REG_NONE;
}
……
因为Value的长度理论上是可以超过260的,于是有以下处理
lRes = GETORIFUNC(QueryValueKey)(
KeyHandle,
&name_str,
KeyValuePartialInformation,
buffer,
total_size,
&total_size );
if ( NT_FAILED(lRes) && lRes != STATUS_BUFFER_OVERFLOW ) {
break;
}
if ( NULL == lpData ) {
lRes = STATUS_SUCCESS;
}
else {
while ( lRes == STATUS_BUFFER_OVERFLOW && total_size - info_size <= *lpcbData ) {
// 空间不足,且需要的空间大小比用户传入的小
// 释放之前分配的内存
if ( buf_ptr != buffer ) {
HeapFree( GetProcessHeap(), 0, buf_ptr );
buf_ptr = NULL;
}
// 重新分配内存
buf_ptr = (char*)HeapAlloc( GetProcessHeap(), 0, total_size );
if ( NULL == buf_ptr ) {
return STATUS_NO_MEMORY;
}
info = (KEY_VALUE_PARTIAL_INFORMATION *)buf_ptr;
lRes = GETORIFUNC(QueryValueKey)(
KeyHandle,
&name_str,
KeyValuePartialInformation,
buf_ptr,
total_size,
&total_size );
}
if ( NT_SUCCESS(lRes) ) {
memcpy( lpData, buf_ptr + info_size, total_size - info_size );
/* if the type is REG_SZ and data is not 0-terminated
* and there is enough space in the buffer NT appends a \0 */
if ( is_string(info->Type) && total_size - info_size <= *lpcbData - sizeof(WCHAR) ) {
WCHAR *ptr = (WCHAR *)( lpData + total_size - info_size );
if (ptr > (WCHAR *)lpData && ptr[-1]) {
*ptr = 0;
}
}
}
else if ( lRes != STATUS_BUFFER_OVERFLOW ) {
break;
}
}
if ( NULL != lpType) {
*lpType = info->Type;
}
if ( NULL != lpcbData) {
*lpcbData = total_size - info_size;
}
} while (0);
在空间不够的情况下,会在堆上分配更多的空间。