/**************************************************************************** * * * INTEL Corporation Prorietary Information * * This listing is supplied under the terms of a license agreement * with INTEL Corporation and may not be copied nor disclosed except * in accordance with the terms of that agreement. * * Copyright (c) 1996 Intel Corporation. * * * Abstract: * * Notes: * ***************************************************************************/ #ifndef __TSTABLE_H #define __TSTABLE_H #include typedef struct _LOCK_ENTRY { HANDLE hLock; int iLockCount; BOOL bCleanup, bDeleted; WORD wNextFree, wUniqueID; } LOCK_ENTRY, *PLOCK_ENTRY; // definition of an invalid ID #define TSTABLE_INVALID_ID (DWORD) 0xFFFFFFFF // return codes that the callback function used in conjunction with EnumerateEntries can return const DWORD CALLBACK_CONTINUE = 1; const DWORD CALLBACK_ABORT = 2; const DWORD CALLBACK_DELETE_ENTRY = 3; const DWORD CALLBACK_DELETE_ENTRY_AND_OBJECT = 4; // used in call to Lock #define TSTABLE_INVALID_UNIQUE_ID (WORD) 0xFFFF #define TSTABLE_INVALID_INDEX (WORD) 0xFFFF // This is a compare function that we aren't using right now. It // will be useful in the future if there is a reason to search // the table typedef INT (*ENTRY_COMPARE) (LPVOID ptr1, LPVOID ptr2); #if defined(DBG) && defined(ENABLE_DUMP_ENTRIES) VOID TSDbgPrint( LPSTR szFormat, ... ) { #define DEBUG_FORMAT_HEADER "TABLE " #define DEBUG_FORMAT_TIMESTAMP "[%02u:%02u:%02u.%03u" #define DEBUG_FORMAT_THREADID ",tid=%x] " va_list Args; SYSTEMTIME SystemTime; char szDebugMessage[512+1]; int nLengthRemaining; int nLength = 0; // retrieve local time GetLocalTime(&SystemTime); // add component header to the debug message nLength += sprintf(&szDebugMessage[nLength], DEBUG_FORMAT_HEADER ); // add timestamp to the debug message nLength += sprintf(&szDebugMessage[nLength], DEBUG_FORMAT_TIMESTAMP, SystemTime.wHour, SystemTime.wMinute, SystemTime.wSecond, SystemTime.wMilliseconds ); // add thread id to the debug message nLength += sprintf(&szDebugMessage[nLength], DEBUG_FORMAT_THREADID, GetCurrentThreadId() ); // point at first argument va_start(Args, szFormat); // determine number of bytes left in buffer nLengthRemaining = sizeof(szDebugMessage) - nLength; // add user specified debug message _vsnprintf(&szDebugMessage[nLength], nLengthRemaining, szFormat, Args ); // release pointer va_end(Args); // output message to specified sink OutputDebugString(szDebugMessage); } #endif template class TSTable { typedef DWORD (*TABLE_CALLBACK) (EntryData* ptr, LPVOID context); public: TSTable (WORD _size); ~TSTable (); BOOL Resize (WORD wNewSize); BOOL CreateAndLock (EntryData* pEntryData, LPDWORD lpdwID); BOOL Validate (DWORD dwID); EntryData *Lock (DWORD dwID, DWORD timeout = INFINITE); BOOL Unlock (DWORD dwID); BOOL Delete (DWORD dwID, BOOL bCleanup = FALSE); EntryData *EnumerateEntries(TABLE_CALLBACK callBackFunc, void* context, BOOL bUnlockTable = FALSE); BOOL IsInitialized () {return bInitialized;} WORD GetSize () {return wNumUsed;} #if defined(DBG) && defined(ENABLE_DUMP_ENTRIES) VOID DumpEntries (); #endif private: // data EntryData** pDataTable; PLOCK_ENTRY pLockTable; CRITICAL_SECTION csTableLock; WORD wSize, wNumUsed, wFirstFree, wLastFree, wUniqueID; BOOL bInitialized; // private methods BOOL LockEntry (WORD wIndex, DWORD timeout = INFINITE); BOOL UnLockEntry(WORD wIndex); void LockTable () { EnterCriticalSection(&csTableLock); }; void UnLockTable() { LeaveCriticalSection(&csTableLock); }; WORD GenerateUniqueID(); DWORD MakeID(WORD wIndex, WORD wUniqueID) { DWORD theID = wUniqueID; theID = (theID << 16) & 0xFFFF0000; theID |= wIndex; return(theID); }; void BreakID(DWORD theID, WORD* pwIndex, WORD* pwUID) { *pwIndex = (WORD) (theID & 0x0000FFFF); *pwUID = (WORD) ((theID >> 16) & 0x0000FFFF); }; }; /* ** TSTable::TSTable * * FILENAME: c:\msdev\projects\firewalls\inc\tstable.h * * PARAMETERS: * * DESCRIPTION: * * RETURNS: * */ template TSTable::TSTable(WORD _size) : wSize(_size), wNumUsed((WORD) 0), wFirstFree((WORD) 0), wLastFree((WORD) (_size - 1)), wUniqueID((WORD) 0), bInitialized(TRUE), pDataTable(NULL), pLockTable(NULL) { WORD wIndex; // Create the table lock InitializeCriticalSection(&csTableLock); // Lock the table LockTable(); // Create the data table pDataTable = new EntryData*[wSize]; if(pDataTable == NULL) { bInitialized = FALSE; return; } // Init the pointers for (wIndex = 0; wIndex < wSize; wIndex++) { pDataTable[wIndex] = NULL; } // Create the lock table #if defined(DBG) && defined(ENABLE_BOGUS_ENTRY) pLockTable = new LOCK_ENTRY[wSize+1]; #else pLockTable = new LOCK_ENTRY[wSize]; #endif if (pLockTable == NULL) { bInitialized = FALSE; return; } // Initialize the lock table entries...each entry begins with // a NULL mutex handle, a zero lock count and it's next free is // the next successive entry. for (wIndex = 0; wIndex < wSize; wIndex++ ) { pLockTable[wIndex].hLock = NULL; pLockTable[wIndex].iLockCount = 0; pLockTable[wIndex].wNextFree = (WORD) (wIndex + 1); } #if defined(DBG) && defined(ENABLE_BOGUS_ENTRY) pLockTable[wSize].hLock = (HANDLE)0xBAADBAAD; pLockTable[wSize].iLockCount = 0xBAADBAAD; pLockTable[wSize].bCleanup = 0xBAADBAAD; pLockTable[wSize].bDeleted = 0xBAADBAAD; pLockTable[wSize].wNextFree = 0xBAAD; pLockTable[wSize].wUniqueID = 0xBAAD; #endif // note: the wNextFree in the last table entry points to an invalid index, however, // this is OK since if the table ever fills, it is automatically resized making what // was an invalid index, the index into the first entry of newly added part of the // enlargened table. Trust me... // Unlock the table UnLockTable(); } /* ** TSTable::~TSTable * * FILENAME: c:\msdev\projects\firewalls\inc\table.h * * PARAMETERS: * * DESCRIPTION: * * RETURNS: * */ template TSTable::~TSTable() { DWORD wIndex; // Lock the table LockTable(); // Delete the data table if (pDataTable != NULL) { delete pDataTable; } // Delete the lock table if (pLockTable != NULL) { // Destroy the mutexes for (wIndex = 0; wIndex < wSize; wIndex++) { if (pLockTable[wIndex].hLock != NULL) { CloseHandle(pLockTable[wIndex].hLock); } } delete pLockTable; } // Unlock the table UnLockTable(); // Destroy the table lock DeleteCriticalSection(&csTableLock); bInitialized = FALSE; } /* ** TSTable::Resize * * FILENAME: c:\msdev\projects\firewalls\inc\table.h * * PARAMETERS: * * DESCRIPTION: * * RETURNS: * */ template BOOL TSTable::Resize(WORD wNewSize) { BOOL bRetCode = TRUE; EntryData** pNewDataTable; PLOCK_ENTRY pNewLockTable; WORD wIndex; // Lock the table LockTable(); // If the table is shrinking, pretend we did it if (wNewSize <= wSize) { goto EXIT; } // Allocate new data and lock tables and make sure that succeeds. pNewDataTable = new EntryData*[wNewSize]; if(pNewDataTable == NULL) { bRetCode = FALSE; goto EXIT; } #if defined(DBG) && defined(ENABLE_BOGUS_ENTRY) pNewLockTable = new LOCK_ENTRY[wNewSize+1]; #else pNewLockTable = new LOCK_ENTRY[wNewSize]; #endif if(pNewLockTable == NULL) { bRetCode = FALSE; goto CLEANUP1; } // Initialize the new section of the lock and data tables for (wIndex = wSize; wIndex < wNewSize; wIndex++) { pNewDataTable[wIndex] = NULL; pNewLockTable[wIndex].hLock = NULL; pNewLockTable[wIndex].iLockCount = 0; pNewLockTable[wIndex].wNextFree = (WORD) (wIndex + 1); } #if defined(DBG) && defined(ENABLE_BOGUS_ENTRY) pNewLockTable[wNewSize].hLock = (HANDLE)0xBAADBAAD; pNewLockTable[wNewSize].iLockCount = 0xBAADBAAD; pNewLockTable[wNewSize].bCleanup = 0xBAADBAAD; pNewLockTable[wNewSize].bDeleted = 0xBAADBAAD; pNewLockTable[wNewSize].wNextFree = 0xBAAD; pNewLockTable[wNewSize].wUniqueID = 0xBAAD; #endif // DBG // Copy the old data table pointers to the new data table memcpy((PCHAR) pNewDataTable, (PCHAR) pDataTable, sizeof(EntryData*) * wSize); // Delete the old data table and fix the pointer delete pDataTable; pDataTable = pNewDataTable; // Copy the old lock table to the new lock table memcpy((PCHAR) pNewLockTable, (PCHAR) pLockTable, sizeof(LOCK_ENTRY) * wSize); // Delete the old lock table and fix the pointer delete pLockTable; pLockTable = pNewLockTable; // Fix the size variable wSize = wNewSize; // Fix the last free index wLastFree = wSize - 1; goto EXIT; CLEANUP1: // Delete the new data table delete pNewDataTable; EXIT: // Unlock the table UnLockTable(); return bRetCode; } /* ** TSTable::CreateAndLock * * FILENAME: c:\msdev\projects\firewalls\inc\table.h * * PARAMETERS: * * DESCRIPTION: * * RETURNS: * */ template BOOL TSTable::CreateAndLock(EntryData* pEntryData, LPDWORD lpdwID) { BOOL bRetCode = FALSE; WORD wIndex; // If the pointer passed in is bad, then don't even try to do anything for them if (pEntryData == NULL || lpdwID == NULL) { goto EXIT; } // Lock the table LockTable(); // If the table is full, then resize it. if (wNumUsed == wSize) { if (Resize((WORD) (wSize + 20)) == FALSE) { goto EXIT; } } // Get the first free entry wIndex = wFirstFree; // Create the mutex for the object if ((pLockTable[wIndex].hLock = CreateMutexA(NULL, FALSE, NULL)) == NULL) { goto EXIT; } // Lock the entry (no need checking the return code as the entire // table is locked) - since this is a new entry, that means that nobody // could have locked the entry already. LockEntry(wIndex, 0); // Copy pointer to the data table pDataTable[wIndex] = pEntryData; // Init the corresponding lock table entry pLockTable[wIndex].bDeleted = FALSE; pLockTable[wIndex].iLockCount = 1; pLockTable[wIndex].wUniqueID = GenerateUniqueID(); // Set the id for the caller *lpdwID = MakeID(wIndex, pLockTable[wIndex].wUniqueID); // Bump up the count of number used wNumUsed++; // Fix the next free index wFirstFree = pLockTable[wIndex].wNextFree; // Fix the last free index if (wIndex == wLastFree) { wLastFree = wFirstFree; } // Signal success bRetCode = TRUE; EXIT: // Unlock the table UnLockTable(); return bRetCode; } /* ** TSTable::Lock * * FILENAME: c:\msdev\projects\firewalls\inc\table.h * * PARAMETERS: * * DESCRIPTION: * * RETURNS: * */ template EntryData* TSTable::Lock(DWORD dwID, DWORD timeout) { EntryData* pEntryData = NULL; WORD wIndex, wUID; BreakID(dwID, &wIndex, &wUID); // Lock the table LockTable(); // Verify the index is within bounds if (wIndex >= wSize) { goto EXIT; } // Verify that the entry is actually valid (ie the lock in non-NULL, // the object status is valid, and the unique ID matches). if (pLockTable[wIndex].hLock == NULL || pLockTable[wIndex].bDeleted == TRUE || pLockTable[wIndex].wUniqueID != wUID) { goto EXIT; } // If the timeout is INFINITE, then try to lock the entry using a more // "thread friendly" method. If a timeout is specified, then don't do // the spin lock since it could be implemented at a higher level. if(timeout == INFINITE) { // simulate infinity with a pseudo "spin lock" // This is more "thread friendly" in that it unlocks the table allowing some // other thread that is trying to unlock the same entry to be able to lock the // table. while(LockEntry(wIndex, 0) == FALSE) { UnLockTable(); // give up the rest of this thread quantum, allowing others to run and potentially // unlock the entry Sleep(0); LockTable(); // If the entry has been replaced, deleted or marked for deletion then // bag it (give up) if((pLockTable[wIndex].wUniqueID != wUID) || (pLockTable[wIndex].hLock == NULL) || (pLockTable[wIndex].bDeleted == TRUE)) { goto EXIT; } } // we got the lock pEntryData = pDataTable[wIndex]; } // Otherwise, do a normal lock else { if (LockEntry(wIndex, timeout) == TRUE) { pEntryData = pDataTable[wIndex]; } } EXIT: // Unlock the table UnLockTable(); return pEntryData; } /* ** TSTable::Unlock * * FILENAME: c:\msdev\projects\firewalls\inc\table.h * * PARAMETERS: * * DESCRIPTION: * * RETURNS: * */ template BOOL TSTable::Unlock(DWORD dwID) { BOOL bRetCode = TRUE; WORD wIndex, wUID; BreakID(dwID, &wIndex, &wUID); // Lock the table LockTable(); // Verify the id is within bounds if (wIndex >= wSize) { bRetCode = FALSE; goto EXIT; } // verify that the UID matches if (pLockTable[wIndex].wUniqueID != wUID) { bRetCode = FALSE; goto EXIT; } // Verify that the lock is actually valid and that the entry has not been // deleted if (pLockTable[wIndex].hLock == NULL) { goto EXIT; } // Make sure that that thread has the lock on the entry if ((bRetCode = LockEntry(wIndex, 0)) == TRUE) { // if this table entry is marked for delete and the lock count is less than 2 // (since the thread could have called delete after unlocking the entry...although // this is a no-no) then clean up the table entry if (pLockTable[wIndex].bDeleted == TRUE && pLockTable[wIndex].iLockCount <= 2) { // If the caller specifed cleanup on delete, then get rid of memory if (pLockTable[wIndex].bCleanup == TRUE) { delete pDataTable[wIndex]; } // Set the pointer to NULL pDataTable[wIndex] = NULL; // See if table is full if (wNumUsed == wSize) { // Fix the entry so that it's next free index points to end of // table. No need to update last entry since it was virtual. pLockTable[wIndex].wNextFree = wLastFree; wFirstFree = wIndex; wLastFree = wIndex; } else { // Fix the entry so that it's next free index is what is currently // the next free pointed to by the current last free entry. // Then update the last free entry's next pointer, and finally, // update the last free index to this entry pLockTable[wIndex].wNextFree = pLockTable[wLastFree].wNextFree; pLockTable[wLastFree].wNextFree = wIndex; wLastFree = wIndex; } // Decrement the count of used entries wNumUsed--; } // Do two unlocks on the entry ... one for the original lock and another for // the lock we obtained during the test UnLockEntry(wIndex); UnLockEntry(wIndex); // Since the entire table is locked, then we can get away with this. If // the code is ever changed so that the entire table is not locked during // these operations, then this will cause a race condition. // If we got rid of the data, then close the handle to the mutex and // set the handle to NULL if (pDataTable[wIndex] == NULL) { CloseHandle(pLockTable[wIndex].hLock); pLockTable[wIndex].hLock = NULL; } } EXIT: // Unlock the table UnLockTable(); return bRetCode; } /* ** TSTable::Delete * * FILENAME: c:\msdev\projects\firewalls\inc\table.h * * PARAMETERS: * * DESCRIPTION: * * RETURNS: * */ template BOOL TSTable::Delete(DWORD dwID, BOOL bCleanup) { BOOL bRetCode = TRUE; WORD wIndex, wUID; BreakID(dwID, &wIndex, &wUID); // Lock the table LockTable(); // Verify that the ID is within bounds if (wIndex >= wSize) { bRetCode = FALSE; goto EXIT; } // verify that the UID matches if (pLockTable[wIndex].wUniqueID != wUID) { bRetCode = FALSE; goto EXIT; } // Verify that the entry is valid if (pDataTable[wIndex] == NULL) { bRetCode = FALSE; goto EXIT; } // Try to lock the entry (ie check to see if we had the entry locked) if (LockEntry(wIndex, 0) == TRUE) { // mark it for deletion, set the cleanp flag and then unlock it pLockTable[wIndex].bDeleted = TRUE; pLockTable[wIndex].bCleanup = bCleanup; UnLockEntry(wIndex); // Note: this function does not call ::Unlock() on behalf of the user. // Thus, the entry is only marked as deleted at this point and can no // longer be locked by any threads (including the one that marked it for delete). // The thread that marked the entry as deleted must call ::Unlock() to actually // free up the entry. } EXIT: // Unlock the table UnLockTable(); return bRetCode; } /* ** TSTable::Lock * * FILENAME: c:\msdev\projects\firewalls\inc\table.h * * PARAMETERS: * * DESCRIPTION: Validates that an object still exists. Can be called * regardless if caller has entry locked or not. * * RETURNS: * */ template BOOL TSTable::Validate(DWORD dwID) { BOOL bRetCode = TRUE; WORD wIndex, wUID; BreakID(dwID, &wIndex, &wUID); // Lock the table LockTable(); // Verify the index is within bounds if (wIndex >= wSize) { bRetCode = FALSE; goto EXIT; } // Verify that the entry is actually valid (ie the lock in non-NULL, // the object status is valid, the unique ID matches, and the data ptr is not null). if (pLockTable[wIndex].hLock == NULL || pLockTable[wIndex].bDeleted == TRUE || pLockTable[wIndex].wUniqueID != wUID || pDataTable[wIndex] == NULL) { bRetCode = FALSE; goto EXIT; } EXIT: // Unlock the table UnLockTable(); return bRetCode; } /* ** TSTable::EnumerateEntries * * FILENAME: c:\msdev\projects\firewalls\inc\table.h * * PARAMETERS: * * DESCRIPTION: * * RETURNS: * */ template EntryData* TSTable::EnumerateEntries(TABLE_CALLBACK callbackFunc, LPVOID context, BOOL bUnlockTable) { DWORD dwAction; WORD wIndex = wSize; EntryData* pEntryData = NULL; DWORD dwEntryID; // Make sure they passed a good function if (callbackFunc == NULL) { goto EXIT; } // Lock the table LockTable(); // Run through the data table and pass the data to the callback function for (wIndex = 0; wIndex < wSize; wIndex++) { // Verify that there is actually data in the entry and that the entry has not // been marked for deletion. if (pDataTable[wIndex] == NULL || pLockTable[wIndex].bDeleted == TRUE) { continue; } // Try to lock the entry...if we cannot, then we don't have the lock and // we will only report entries that we have locked (or are unlocked) if (LockEntry(wIndex, 0) == FALSE) { continue; } // build and remember the "full" entry ID so we can use it to unlock the entry dwEntryID = MakeID(wIndex, pLockTable[wIndex].wUniqueID); // Save the pointer to the object. pEntryData = pDataTable[wIndex]; // note: only unlock the table during the callback if we are explicitly asked to (the // default is not to unlock the table). if(bUnlockTable == TRUE) UnLockTable(); // Call their function dwAction = callbackFunc(pDataTable[wIndex], context); if(bUnlockTable == TRUE) LockTable(); // If the action says to delete the entry, then do so...if we are also to delete // the object, pass in a TRUE. if (dwAction == CALLBACK_DELETE_ENTRY || dwAction == CALLBACK_DELETE_ENTRY_AND_OBJECT) { Delete(dwEntryID, (dwAction == CALLBACK_DELETE_ENTRY ? FALSE : TRUE)); } // If the action says abort, then break the loop...notice that means that // the entry is still locked else if (dwAction == CALLBACK_ABORT) { goto EXIT; } // Unlock the entry...notice we don't use UnLockEntry. The reason is that // if the entry has been marked as deleted, then we need to have // it destroyed and UnLockEntry doesn't do that. Unlock(dwEntryID); } EXIT: // Unlock the table UnLockTable(); // Return NULL if we processed the entire table...if we were told to abort, // return a pointer to the entry we stopped on. return (wIndex == wSize ? NULL : pEntryData); } // helper functions - these assume table is locked and index is good /* ** TSTable::LockEntry * * FILENAME: c:\msdev\projects\firewalls\inc\table.h * * PARAMETERS: * * DESCRIPTION: * * RETURNS: * */ template BOOL TSTable::LockEntry(WORD wIndex, DWORD timeout) { BOOL bRetCode = TRUE; DWORD dwRetCode; // Try to lock the entry. If it succeeds, we'll bump up the lock count. If // the wait ended because another thread abandoned the mutex, then set the count // to one. dwRetCode = WaitForSingleObject(pLockTable[wIndex].hLock, timeout); if (dwRetCode == WAIT_OBJECT_0) { pLockTable[wIndex].iLockCount++; } else if (dwRetCode == WAIT_ABANDONED) { pLockTable[wIndex].iLockCount = 1; } else { bRetCode = FALSE; } return bRetCode; } /* ** TSTable::UnLockEntry * * FILENAME: c:\msdev\projects\firewalls\inc\table.h * * PARAMETERS: * * DESCRIPTION: * * RETURNS: * */ template BOOL TSTable::UnLockEntry(WORD wIndex) { BOOL bRetCode; // Release the mutex...if that succeeds, reduce the count if((bRetCode = ReleaseMutex(pLockTable[wIndex].hLock)) == TRUE) { pLockTable[wIndex].iLockCount--; } return bRetCode; } /* ** TSTable::GenerateUniqueID * * FILENAME: c:\msdev\projects\firewalls\inc\table.h * * PARAMETERS: * * DESCRIPTION: table should be locked before calling this function. * * RETURNS: * */ template WORD TSTable::GenerateUniqueID() { // table must be locked if(++wUniqueID == TSTABLE_INVALID_UNIQUE_ID) wUniqueID++; return(wUniqueID); } #if defined(DBG) && defined(ENABLE_DUMP_ENTRIES) /* ** TSTable::DumpEntries * * FILENAME: c:\msdev\projects\firewalls\inc\table.h * * PARAMETERS: * * DESCRIPTION: * * RETURNS: * */ template VOID TSTable::DumpEntries() { WORD wIndex; WORD wNumFree; // Lock the table LockTable(); #ifdef ENABLE_DUMP_ENTRIES_VERBOSE for (wIndex = 0; wIndex < wSize; wIndex++) { if (pLockTable[wIndex].hLock == NULL) { TSDbgPrint("[Entry %d] - FREE" "\n\thLock = %x" "\n\tiLockCount = %d" "\n\tbCleanup = %s" "\n\tbDeleted = %s" "\n\twNextFree = %d" "\n\tpEntryData = 0x%08lx\n", wIndex, pLockTable[wIndex].hLock, pLockTable[wIndex].iLockCount, pLockTable[wIndex].bCleanup ? "TRUE" : "FALSE", pLockTable[wIndex].bDeleted ? "TRUE" : "FALSE", pLockTable[wIndex].wNextFree, pDataTable[wIndex] ); } else if (LockEntry(wIndex, 0) == TRUE) { TSDbgPrint("[Entry %d] - USED" "\n\thLock = %x" "\n\tiLockCount = %d" "\n\tbCleanup = %s" "\n\tbDeleted = %s" "\n\twNextFree = %d" "\n\tpEntryData = 0x%08lx\n", wIndex, pLockTable[wIndex].hLock, pLockTable[wIndex].iLockCount, pLockTable[wIndex].bCleanup ? "TRUE" : "FALSE", pLockTable[wIndex].bDeleted ? "TRUE" : "FALSE", pLockTable[wIndex].wNextFree, pDataTable[wIndex] ); UnLockEntry(wIndex); } else { TSDbgPrint("[Entry %d] - LOCKED\n",wIndex); } } #endif TSDbgPrint("[Free List] - " "wFirstFree=%d," "wLastFree=%d\n", wFirstFree, wLastFree ); wIndex = wFirstFree; wNumFree = (wFirstFree < wSize) ? 1 : 0; while (wIndex != wLastFree) { TSDbgPrint("\t%d -> %d\n",wIndex,pLockTable[wIndex].wNextFree); wIndex = pLockTable[wIndex].wNextFree; wNumFree++; } TSDbgPrint("[Summary] - " "wSize=%d," "wNumUsed=%d," "wNumFree=%d %s\n\n", wSize, wNumUsed, wNumFree, ((wSize - wNumUsed) != wNumFree) ? "ERROR" : "OKAY" ); // Unlock the table UnLockTable(); } #endif #endif