|
|
//+----------------------------------------------------------------------------
//
// Copyright (C) 1992, Microsoft Corporation
//
// File: registry.c
//
// Contents: Module to read in registry parameters.
//
// This module is intended as a simple, reusable, interface to the
// registry. Among its features are:
//
// o Kernel Callable.
//
// o Intuitive (it is to me!) - To read the value for /a/b/c/d,
// you say KRegGetValue("/a/b/c/d"), instead of calling at
// least 3 different Nt APIs. Any and all strings returned are
// guaranteed to be NULL terminated.
//
// o Allocates memory on behalf of caller - so we don't waste any
// by allocating max amounts.
//
// o Completely non-reentrant - 'cause it maintains state across
// calls. (It would be simple to make it multi-threaded tho)
//
//
// Classes:
//
// Functions: KRegSetRoot
// KRegCloseRoot
// KRegGetValue
// KRegGetNumValuesAndSubKeys
// KRegEnumValueSet
// KRegEnumSubKeySet
//
//
// History: 18 Sep 92 Milans created
//
//-----------------------------------------------------------------------------
#include "registry.h"
#include "regsups.h"
//
// If we ever needed to go multithreaded, we should return HKEY_ROOT to caller
// instead of keeping it here.
//
static HKEY HKEY_ROOT = NULL; // Handle to root key
#define KREG_SIZE_THRESHOLD 5 // For allocation optimization
//
// Some commonly used error format strings.
//
#if DBG == 1
static char *szErrorOpen = "Error opening %ws section.\n"; static char *szErrorRead = "Error reading %ws section.\n";
#endif
#ifdef ALLOC_PRAGMA
#pragma alloc_text( PAGE, KRegInit )
#pragma alloc_text( PAGE, KRegCloseRoot )
#pragma alloc_text( PAGE, KRegSetRoot )
#pragma alloc_text( PAGE, KRegCreateKey )
#pragma alloc_text( PAGE, KRegGetValue )
#pragma alloc_text( PAGE, KRegSetValue )
#pragma alloc_text( PAGE, KRegDeleteValue )
#pragma alloc_text( PAGE, KRegGetNumValuesAndSubKeys )
#pragma alloc_text( PAGE, KRegEnumValueSet )
#pragma alloc_text( PAGE, KRegEnumSubKeySet )
#pragma alloc_text( PAGE, KRegFreeArray )
#endif // ALLOC_PRAGMA
//+----------------------------------------------------------------------------
//
// Function: KREG_CHECK_HARD_STATUS, macro
//
// Synopsis: If Status is not STATUS_SUCCESS, print out debug msg and return.
//
// Arguments:
//
// Returns:
//
//-----------------------------------------------------------------------------
#define KREG_CHECK_HARD_STATUS(Status, M1, M2) \
if (!NT_SUCCESS(Status)) { \ kreg_debug_out(M1, M2); \ return(Status); \ }
//+----------------------------------------------------------------------------
//
// Function: KRegInit
//
// Synopsis:
//
// Arguments: None
//
// Returns: STATUS_SUCCESS
//
//-----------------------------------------------------------------------------
NTSTATUS KRegInit(void) { return(STATUS_SUCCESS); }
//+----------------------------------------------------------------------------void
//
// Function: KRegCloseRoot
//
// Synopsis: Close the Root opened by KRegSetRoot.
//
// Arguments: None
//
// Returns: Nothing
//
//-----------------------------------------------------------------------------
void KRegCloseRoot() { if (HKEY_ROOT) { NtClose(HKEY_ROOT); HKEY_ROOT = NULL; } }
//+----------------------------------------------------------------------------
//
// Function: KRegSetRoot
//
// Synopsis: Sets a particular key as the "root" key for subsequent calls
// to registry routines.
//
// Arguments: wszRootName - Name of root key
//
// Returns: Result of opening the key.
//
//-----------------------------------------------------------------------------
NTSTATUS KRegSetRoot( IN PWSTR wszRootName ) { NTSTATUS Status;
if (HKEY_ROOT != NULL) { NtClose(HKEY_ROOT); }
Status = NtOpenKey( &HKEY_ROOT, // Handle to DFS root key
KEY_READ | KEY_WRITE, // Only need to read
KRegpAttributes( // Name of Key
NULL, wszRootName ) );
return(Status); }
//+----------------------------------------------------------------------------
//
// Function: KRegDeleteKey
//
// Synopsis: Deletes a key from the registry.
//
// Arguments: [wszKey] -- name of key relative to the current root.
//
// Returns: Status from deleting the key.
//
//-----------------------------------------------------------------------------
NTSTATUS KRegDeleteKey( IN PWSTR wszKey) { NTSTATUS Status; APWSTR awszSubKeys; ULONG cSubKeys, i; HKEY hkey;
//
// NtDeleteKey won't delete key's which have subkeys. So, we first
// enumerate all the subkeys and delete them, before deleting the key.
//
Status = KRegEnumSubKeySet(wszKey, &cSubKeys, &awszSubKeys);
if (!NT_SUCCESS(Status)) { return( Status ); }
for (i = 0; i < cSubKeys && NT_SUCCESS(Status); i++) {
Status = NtOpenKey( &hkey, KEY_ALL_ACCESS, KRegpAttributes(HKEY_ROOT, awszSubKeys[i]) );
if (NT_SUCCESS(Status)) { Status = NtDeleteKey( hkey ); NtClose(hkey); }
}
//
// If we were able to delete all the subkeys, we can delete the
// key itself.
//
if (NT_SUCCESS(Status)) { Status = NtOpenKey( &hkey, KEY_ALL_ACCESS, KRegpAttributes(HKEY_ROOT, wszKey) );
if (Status == STATUS_OBJECT_NAME_NOT_FOUND) { Status = STATUS_SUCCESS; } else if (NT_SUCCESS(Status)) { Status = NtDeleteKey( hkey ); NtClose(hkey); } }
KRegFreeArray( cSubKeys, (APBYTE) awszSubKeys );
return(Status); }
//+----------------------------------------------------------------------------
//
// Function: KRegCreateKey
//
// Synopsis: Creates a new key in the registry under the subkey given
// in wszSubKey. wszSubKey may be NULL, in which case the
// new key is created directly under the current root.
//
// Arguments: [wszSubKey] -- Name of subkey relative to root key under
// which new key will be created.
// [wszNewKey] -- Name of new key to be created.
//
// Returns:
//
//-----------------------------------------------------------------------------
NTSTATUS KRegCreateKey( IN PWSTR wszSubKey, IN PWSTR wszNewKey) { HKEY hkeySubKey, hkeyNewKey; NTSTATUS Status; UNICODE_STRING ustrValueName;
if (wszSubKey != NULL) { Status = NtOpenKey( &hkeySubKey, KEY_WRITE, KRegpAttributes(HKEY_ROOT, wszSubKey) );
KREG_CHECK_HARD_STATUS(Status, szErrorOpen, wszSubKey); } else { hkeySubKey = HKEY_ROOT; }
Status = NtCreateKey( &hkeyNewKey, KEY_WRITE, KRegpAttributes(hkeySubKey, wszNewKey), 0L, // TitleIndex
NULL, // Class
REG_OPTION_NON_VOLATILE, // Create option
NULL); // Create disposition
if (wszSubKey != NULL) { NtClose(hkeySubKey); }
if (!NT_SUCCESS(Status)) { kreg_debug_out(szErrorOpen, wszNewKey); NtClose(hkeyNewKey); return(Status); }
NtClose(hkeyNewKey);
return( Status ); }
//+----------------------------------------------------------------------------
//
// Function: KRegGetValue
//
// Synopsis: Given a Value Name and a handle, will allocate memory for the
// handle and fill it with the Value Data for the value name.
//
// Arguments: [wszSubKey] - Name of subkey relative to root key
// [wszValueName] - name of value data
// [ppValueData] - pointer to pointer to byte data.
//
// Returns: STATUS_SUCCESS, STATUS_NO_MEMORY, Status from NtReg api.
//
// Notes: It will allocate memory for the data, and return a pointer to
// it in ppValueData. Caller must free it.
//
//-----------------------------------------------------------------------------
NTSTATUS KRegGetValue( IN PWSTR wszSubKey, IN PWSTR wszValueName, OUT PBYTE *ppValueData ) { HKEY hkeySubKey; ULONG dwUnused, cbMaxDataSize; ULONG cbActualSize; PBYTE pbData = NULL; NTSTATUS Status;
Status = NtOpenKey( &hkeySubKey, KEY_READ, KRegpAttributes(HKEY_ROOT, wszSubKey) );
KREG_CHECK_HARD_STATUS(Status, szErrorOpen, wszSubKey);
Status = KRegpGetKeyInfo( hkeySubKey, &dwUnused, // Number of Subkeys
&dwUnused, // Max size of subkey length
&dwUnused, // # of Values
&dwUnused, // Max size of value Name
&cbMaxDataSize // Max size of value Data
);
if (!NT_SUCCESS(Status)) { kreg_debug_out(szErrorRead, wszSubKey); NtClose(hkeySubKey); return(Status); }
//
// Just in case the value is of type REG_SZ, provide for inserting a
// UNICODE_NULL at the end.
//
cbMaxDataSize += sizeof(UNICODE_NULL);
pbData = kreg_alloc(cbMaxDataSize); if (pbData == NULL) { Status = STATUS_NO_MEMORY; goto Cleanup; } cbActualSize = cbMaxDataSize;
Status = KRegpGetValueByName( hkeySubKey, wszValueName, pbData, &cbActualSize);
if (NT_SUCCESS(Status)) { if ((cbMaxDataSize - cbActualSize) < KREG_SIZE_THRESHOLD) {
//
// Optimization - no need to double allocate if actual size and max
// size are pretty close.
//
*ppValueData = pbData; pbData = NULL; // "deallocate" pbData
} else {
//
// Big enough difference between actual and max size, warrants
// allocating a smaller buffer.
//
*ppValueData = (PBYTE) kreg_alloc(cbActualSize); if (*ppValueData == NULL) {
//
// Well, we couldn't "reallocate" a smaller chunk, we'll just
// live on the edge and return the original buffer.
//
*ppValueData = pbData; pbData = NULL; } else { RtlMoveMemory(*ppValueData, pbData, cbActualSize); }
} }
Cleanup:
NtClose(hkeySubKey); if (pbData != NULL) { kreg_free(pbData); }
return(Status); }
//+----------------------------------------------------------------------------
//
// Function: KRegSetValue
//
// Synopsis: Given a Key Name, Value Name, and type, size and data, this
// routine will update the registry's value to the type and data.
// The valuename will be created if it doesn't exist. The key
// must already exist.
//
// Arguments: [wszSubKey] - Name of subkey relative to root key
// [wszValueName] - name of value data
// [ulType] - as in REG_DWORD, REG_SZ, REG_MULTI_SZ, etc.
// [cbSize] - size in bytes of data. If type == REG_SZ, data need
// !not! include the terminating NULL. One will be
// appended as needed.
// [pValueData] - pointer to pointer to byte data.
//
// Returns: STATUS_SUCCESS, STATUS_NO_MEMORY, Status from NtReg api.
//
// Notes: It will allocate memory for the data, and return a pointer to
// it in ppValueData. Caller must free it.
//
//-----------------------------------------------------------------------------
NTSTATUS KRegSetValue( IN PWSTR wszSubKey, IN PWSTR wszValueName, IN ULONG ulType, IN ULONG cbSize, IN PBYTE pValueData ) { HKEY hkeySubKey; NTSTATUS Status; UNICODE_STRING ustrValueName; PWSTR pwszValueData;
Status = NtOpenKey( &hkeySubKey, KEY_WRITE, KRegpAttributes(HKEY_ROOT, wszSubKey) );
KREG_CHECK_HARD_STATUS(Status, szErrorOpen, wszSubKey);
RtlInitUnicodeString(&ustrValueName, wszValueName);
if (ulType == REG_SZ) {
pwszValueData = (PWSTR) kreg_alloc(cbSize+sizeof(UNICODE_NULL));
if (pwszValueData == NULL) { NtClose(hkeySubKey); return(STATUS_INSUFFICIENT_RESOURCES); }
RtlMoveMemory((PVOID) pwszValueData, (PVOID) pValueData, cbSize); pwszValueData[cbSize/sizeof(WCHAR)] = UNICODE_NULL; cbSize += sizeof(UNICODE_NULL); } else { pwszValueData = (PWSTR) pValueData; }
Status = NtSetValueKey( hkeySubKey, &ustrValueName, 0L, // TitleIndex
ulType, pwszValueData, cbSize);
if (ulType == REG_SZ) { kreg_free(pwszValueData); }
if (NT_SUCCESS(Status)) { NtFlushKey(hkeySubKey); }
NtClose(hkeySubKey); return(Status); }
//+----------------------------------------------------------------------------
//
// Function: KRegDeleteValue
//
// Synopsis: Deletes a value name
//
// Arguments: [wszSubKey] -- Name of subkey under which wszValueName exists.
// [wszValueName] -- Name of Value to delete.
//
// Returns:
//
//-----------------------------------------------------------------------------
NTSTATUS KRegDeleteValue( IN PWSTR wszSubKey, IN PWSTR wszValueName) { HKEY hkeySubKey; NTSTATUS Status; UNICODE_STRING ustrValueName;
Status = NtOpenKey( &hkeySubKey, KEY_WRITE, KRegpAttributes(HKEY_ROOT, wszSubKey) );
KREG_CHECK_HARD_STATUS(Status, szErrorOpen, wszSubKey); RtlInitUnicodeString(&ustrValueName, wszValueName);
Status = NtDeleteValueKey( hkeySubKey, &ustrValueName);
if (NT_SUCCESS(Status)) { NtFlushKey(hkeySubKey); }
NtClose(hkeySubKey); return(Status); }
//+----------------------------------------------------------------------------
//
// Function: KRegGetNumValuesAndSubKeys
//
// Synopsis: Given a Subkey, return how many subkeys and values it has.
//
// Arguments: [wszSubKey] - for which info is required.
// [plNumValues] - receives number of values this subkey has.
// [plNumSubKeys] - receives number of subkeys under this subkey.
//
// Returns: STATUS_SUCCESS, or Status from NtRegAPI.
//
//-----------------------------------------------------------------------------
NTSTATUS KRegGetNumValuesAndSubKeys( IN PWSTR wszSubKey, OUT PULONG pcNumValues, OUT PULONG pcNumSubKeys ) { HKEY hKeySubKey; ULONG dwUnused; NTSTATUS Status;
Status = NtOpenKey( &hKeySubKey, KEY_READ, KRegpAttributes(HKEY_ROOT, wszSubKey) );
KREG_CHECK_HARD_STATUS(Status, szErrorOpen, wszSubKey);
Status = KRegpGetKeyInfo( hKeySubKey, pcNumSubKeys, // Number of Subkeys
&dwUnused, // Max size of subkey length
pcNumValues, // # of Values
&dwUnused, // Max size of value Name
&dwUnused // Max size of value Data
);
if (!NT_SUCCESS(Status)) { kreg_debug_out(szErrorRead, wszSubKey); NtClose(hKeySubKey); return(Status); }
NtClose(hKeySubKey); return(Status); }
//+----------------------------------------------------------------------------
//
// Function: KRegEnumValueSet
//
// Synopsis: Given a subkey, return the names and data for all its values.
//
// Arguments: [wszSubKey] - Name of subkey to read.
// [pcMaxElements] - On return, contains number of names and
// data actually read.
// [pawszValueNames] - Pointer to array of PWSTRS. This routine
// will allocate memory for the array and the
// strings.
// [papbValueData] - pointer to array of PBYTES. This routine
// will allocate the array and the memory for the
// data, stuffing the pointers to data into this
// array.
// [paValueStatus] - Status of reading the corresponding Value. The
// array will be allocated here.
//
// Returns: STATUS_SUCCESS, STATUS_NO_MEMORY, or Status from NtRegAPI.
//
// Notes: Returned Value Names are always NULL terminated.
// If the Value Data is of type REG_SZ, it, too, is NULL terminated
// Caller frees the last three OUT params (and the contents of
// awszValueNames and apbValueData!) only on SUCCESSFUL return.
//
//-----------------------------------------------------------------------------
NTSTATUS KRegEnumValueSet( IN PWSTR wszSubKey, OUT PULONG pcMaxElements, OUT APWSTR *pawszValueNames, OUT APBYTE *papbValueData, OUT ANTSTATUS *paValueStatus ) { ULONG i = 0; HKEY hKeySubKey; ULONG dwUnused, cbMaxNameSize, cbMaxDataSize; PWSTR wszName = NULL; PBYTE pbData = NULL; NTSTATUS Status;
APWSTR awszValueNames; APBYTE apbValueData; ANTSTATUS aValueStatus; ULONG cMaxElements;
awszValueNames = *pawszValueNames = NULL; apbValueData = *papbValueData = NULL; aValueStatus = *paValueStatus = NULL;
Status = NtOpenKey( &hKeySubKey, KEY_READ, KRegpAttributes(HKEY_ROOT, wszSubKey) );
KREG_CHECK_HARD_STATUS(Status, szErrorOpen, wszSubKey);
Status = KRegpGetKeyInfo( hKeySubKey, &dwUnused, // Number of Subkeys
&dwUnused, // Max size of subkey length
&cMaxElements, // # of Values
&cbMaxNameSize, // Max size of value Name
&cbMaxDataSize // Max size of value Data
);
if (!NT_SUCCESS(Status)) { kreg_debug_out(szErrorRead, wszSubKey); NtClose(hKeySubKey); return(Status); }
//
// Allocate memory for the arrays.
//
awszValueNames = *pawszValueNames = kreg_alloc(cMaxElements * sizeof(PWSTR)); apbValueData = *papbValueData = kreg_alloc(cMaxElements * sizeof(PBYTE)); aValueStatus = *paValueStatus = kreg_alloc(cMaxElements * sizeof(NTSTATUS));
if (awszValueNames == NULL || apbValueData == NULL || aValueStatus == NULL) { Status = STATUS_NO_MEMORY; goto Cleanup;
}
//
// Initialize the arrays
//
RtlZeroMemory(awszValueNames, cMaxElements * sizeof(PWSTR)); RtlZeroMemory(apbValueData, cMaxElements * sizeof(PBYTE)); RtlZeroMemory(aValueStatus, cMaxElements * sizeof(NTSTATUS));
//
// For name, we need an extra spot for the terminating NULL
//
wszName = kreg_alloc(cbMaxNameSize + sizeof(WCHAR)); pbData = kreg_alloc(cbMaxDataSize);
if (pbData == NULL || wszName == NULL) { Status = STATUS_NO_MEMORY; goto Cleanup; }
for (i = 0; i < cMaxElements; i++) { ULONG cbActualNameSize, cbActualDataSize;
cbActualNameSize = cbMaxNameSize; cbActualDataSize = cbMaxDataSize;
Status = KRegpEnumKeyValues( hKeySubKey, i, wszName, &cbActualNameSize, pbData, &cbActualDataSize ); if (Status == STATUS_NO_MORE_ENTRIES) { Status = STATUS_SUCCESS; break; } else if (!NT_SUCCESS(Status)) { aValueStatus[i] = Status; continue; } else { aValueStatus[i] = Status; }
cbActualNameSize += sizeof(WCHAR); // For terminating NULL
awszValueNames[i] = (PWSTR) kreg_alloc(cbActualNameSize); apbValueData[i] = (PBYTE) kreg_alloc(cbActualDataSize);
if (apbValueData[i] == NULL || awszValueNames[i] == NULL) { Status = aValueStatus[i] = STATUS_NO_MEMORY; goto Cleanup; } RtlMoveMemory(awszValueNames[i], wszName, cbActualNameSize); RtlMoveMemory(apbValueData[i], pbData, cbActualDataSize); }
Cleanup:
NtClose(hKeySubKey); if (wszName != NULL) { kreg_free(wszName); } if (pbData != NULL) { kreg_free(pbData); }
if (!NT_SUCCESS(Status)) { *pcMaxElements = 0;
KRegFreeArray(i+1, (APBYTE) awszValueNames); KRegFreeArray(i+1, apbValueData);
if (aValueStatus != NULL) { kreg_free(aValueStatus); }
} else { // Status success
*pcMaxElements = i; }
return(Status); }
//+----------------------------------------------------------------------------
//
// Function: KRegEnumSubKeySet
//
// Synopsis: This one's slightly trick. Given a Subkey, it returns an array
// of its subkey-names. The following is true - say the root is
// /a/b/c. Say wszSubKey is d. Say d has subkeys s1, s2, and s3.
// Then the array of subkey-names returned will contain the names
// d/s1, d/s2, d/s3.
//
// Arguments: [wszSubKey] - the Subkey relative to the root.
// [pcMaxElements] - On return, # subkeys actually being returned.
// [awszValueNames] - Unitialized array of PWSTR (room for at least
// *plMaxElements). This routine will allocate
// memory for the subkey names, and fill in this
// array with pointers to them.
//
// Returns:
//
// Notes: Caller must free at most *plMaxElements members of
// awszSubKeyNames. See synopsis above for form of subkey names.
//
//-----------------------------------------------------------------------------
NTSTATUS KRegEnumSubKeySet( IN PWSTR wszSubKey, IN OUT PULONG pcMaxElements, OUT APWSTR *pawszSubKeyNames ) { ULONG i, j = 0; APWSTR awszSubKeyNames = NULL; PWSTR wszBuffer = NULL; HKEY hKeySubKey; ULONG dwUnused, cbMaxNameSize, cwszSubKey; NTSTATUS Status;
cwszSubKey = wcslen(wszSubKey);
*pawszSubKeyNames = NULL;
Status = NtOpenKey( &hKeySubKey, KEY_READ, KRegpAttributes(HKEY_ROOT, wszSubKey) );
KREG_CHECK_HARD_STATUS(Status, szErrorOpen, wszSubKey);
Status = KRegpGetKeyInfo( hKeySubKey, pcMaxElements, // Number of Subkeys
&cbMaxNameSize, // Max size of subkey name
&dwUnused, // # of Values
&dwUnused, // Max size of value Name
&dwUnused // Max size of value Data
);
if (pcMaxElements == 0) { NtClose(hKeySubKey); *pawszSubKeyNames = NULL; return(STATUS_SUCCESS); }
if (!NT_SUCCESS(Status)) { kreg_debug_out(szErrorRead, wszSubKey); goto Cleanup; }
*pawszSubKeyNames = kreg_alloc( (*pcMaxElements + 1) * sizeof(PWSTR) ); awszSubKeyNames = *pawszSubKeyNames; wszBuffer = kreg_alloc(cbMaxNameSize + sizeof(WCHAR));
if (wszBuffer == NULL || awszSubKeyNames == NULL) { Status = STATUS_NO_MEMORY; goto Cleanup; } else { RtlZeroMemory(awszSubKeyNames, (*pcMaxElements + 1) * sizeof(PWSTR)); }
for (i = j = 0; j < *pcMaxElements; i++) { ULONG cbActualNameSize, cNameIndex;
cbActualNameSize = cbMaxNameSize;
Status = KRegpEnumSubKeys( hKeySubKey, i, wszBuffer, &cbActualNameSize ); if (Status == STATUS_NO_MORE_ENTRIES) { Status = STATUS_SUCCESS; break; } else if (!NT_SUCCESS(Status)) { continue; }
cbActualNameSize += sizeof(WCHAR); // for terminating NULL
awszSubKeyNames[j] = (PWSTR) kreg_alloc( cwszSubKey * sizeof(WCHAR) + sizeof(WCHAR) + // for backslash
cbActualNameSize ); if (awszSubKeyNames[j] == NULL) { Status = STATUS_NO_MEMORY; goto Cleanup; } else { RtlZeroMemory( awszSubKeyNames[j], cwszSubKey * sizeof(WCHAR) + sizeof(WCHAR) + // for backslash
cbActualNameSize); if (wszSubKey[0] != UNICODE_NULL) { wcscpy(awszSubKeyNames[j], wszSubKey); wcscat(awszSubKeyNames[j], UNICODE_PATH_SEP_STR); cNameIndex = cwszSubKey + 1; } else { cNameIndex = 0; }
RtlMoveMemory( &awszSubKeyNames[j][cNameIndex], wszBuffer, cbActualNameSize); j++; } }
Cleanup: NtClose(hKeySubKey); if (wszBuffer != NULL) { kreg_free(wszBuffer); }
if (!NT_SUCCESS(Status)) { *pcMaxElements = 0;
KRegFreeArray(j, (APBYTE) awszSubKeyNames);
} else { // status success
*pcMaxElements = j; }
return(Status); }
//+----------------------------------------------------------------------------
//
// Function: KRegFreeArray
//
// Synopsis: Given an array of pointers, frees the pointers and the array.
//
// Arguments: [cElements] - Number of elements to free. Some elements can be
// NULL.
// [pa] - pointer to the array.
//
// Returns: Nothing
//
//-----------------------------------------------------------------------------
VOID KRegFreeArray( IN ULONG cElements, IN APBYTE pa) { ULONG i;
if (pa != NULL) { for (i = 0; i < cElements; i++) { if (pa[i] != NULL) { kreg_free(pa[i]); } }
kreg_free(pa); } }
|