reactos/win32ss/gdi/ntgdi/gdidbg.c
Timo Kreuzer cfd1647914
[REACTOS] Fix misc 64 bit issues (#783)
* [WIN32K] Fix handle calculation in DbgGdiHTIntegrityCheck
* [NOTEPAD] Fix MSVC warnings
* [PSDK] Simplify *PROC definitions in windef.h
* [VIDEOPRT] Don't try to use NtVdmControl on x64
* [FREELDR] Fix some macros
* [CRT] Make qsort 64 bit compatible
* [NTOS] Use #ifndef _WIN64 instead of #ifdef _M_IX86 around C_ASSERTs
* [FAST486] Fix 64 bit warnings and change DWORD to ULONG, so it can be used in kernel mode
* [APPHELP_APITEST] Fix 64 bit issue
2019-01-05 10:50:11 +01:00

804 lines
20 KiB
C

/*
* PROJECT: ReactOS win32 kernel mode subsystem
* LICENSE: GPL - See COPYING in the top level directory
* FILE: win32ss/gdi/ntgdi/gdidbg.c
* PURPOSE: Special debugging functions for GDI
* PROGRAMMERS: Timo Kreuzer
*/
/** INCLUDES ******************************************************************/
#if DBG
#include <win32k.h>
#define NDEBUG
#include <debug.h>
extern ULONG gulFirstFree;
extern ULONG gulFirstUnused;
extern PENTRY gpentHmgr;
ULONG gulLogUnique = 0;
/* Note: the following values need to be sorted */
DBG_CHANNEL DbgChannels[DbgChCount] = {
{L"EngBlt", DbgChEngBlt},
{L"EngBrush", DbgChEngBrush},
{L"EngClip", DbgChEngClip},
{L"EngCursor", DbgChEngCursor},
{L"EngDev", DbgChEngDev},
{L"EngErr", DbgChEngErr},
{L"EngEvent", DbgChEngEvent},
{L"EngGrad", DbgChEngGrad},
{L"EngLDev", DbgChEngLDev},
{L"EngLine", DbgChEngLine},
{L"EngMapping", DbgChEngMapping},
{L"EngPDev", DbgChEngPDev},
{L"EngSurface", DbgChEngSurface},
{L"EngWnd", DbgChEngWnd},
{L"EngXlate", DbgChEngXlate},
{L"GdiBitmap", DbgChGdiBitmap},
{L"GdiBlt", DbgChGdiBlt},
{L"GdiBrush", DbgChGdiBrush},
{L"GdiClipRgn", DbgChGdiClipRgn},
{L"GdiCoord", DbgChGdiCoord},
{L"GdiDC", DbgChGdiDC},
{L"GdiDCAttr", DbgChGdiDCAttr},
{L"GdiDCState", DbgChGdiDCState},
{L"GdiDev", DbgChGdiDev},
{L"GdiDib", DbgChGdiDib},
{L"GdiFont", DbgChGdiFont},
{L"GdiLine", DbgChGdiLine},
{L"GdiObj", DbgChGdiObj},
{L"GdiPalette", DbgChGdiPalette},
{L"GdiPath", DbgChGdiPath},
{L"GdiPen", DbgChGdiPen},
{L"GdiPool", DbgChGdiPool},
{L"GdiRgn", DbgChGdiRgn},
{L"GdiText", DbgChGdiText},
{L"GdiXFormObj", DbgChGdiXFormObj},
{L"UserAccel", DbgChUserAccel},
{L"UserCallback", DbgChUserCallback},
{L"UserCallProc", DbgChUserCallProc},
{L"UserCaret", DbgChUserCaret},
{L"UserClass", DbgChUserClass},
{L"UserClipbrd", DbgChUserClipbrd},
{L"UserCsr", DbgChUserCsr},
{L"UserDce", DbgChUserDce},
{L"UserDefwnd", DbgChUserDefwnd},
{L"UserDesktop", DbgChUserDesktop},
{L"UserDisplay",DbgChUserDisplay},
{L"UserEvent", DbgChUserEvent},
{L"UserFocus", DbgChUserFocus},
{L"UserHook", DbgChUserHook},
{L"UserHotkey", DbgChUserHotkey},
{L"UserIcon", DbgChUserIcon},
{L"UserInput", DbgChUserInput},
{L"UserKbd", DbgChUserKbd},
{L"UserKbdLayout", DbgChUserKbdLayout},
{L"UserMenu", DbgChUserMenu},
{L"UserMetric", DbgChUserMetric},
{L"UserMisc", DbgChUserMisc},
{L"UserMonitor", DbgChUserMonitor},
{L"UserMsg", DbgChUserMsg},
{L"UserMsgQ", DbgChUserMsgQ},
{L"UserObj", DbgChUserObj},
{L"UserPainting", DbgChUserPainting},
{L"UserProcess", DbgChUserProcess},
{L"UserProp", DbgChUserProp},
{L"UserScrollbar", DbgChUserScrollbar},
{L"UserShutdown", DbgChUserShutdown},
{L"UserSysparams", DbgChUserSysparams},
{L"UserTimer", DbgChUserTimer},
{L"UserThread", DbgChUserThread},
{L"UserWinpos", DbgChUserWinpos},
{L"UserWinsta", DbgChUserWinsta},
{L"UserWnd", DbgChUserWnd}
};
ULONG
NTAPI
DbgCaptureStackBackTace(
_Out_writes_(cFramesToCapture) PVOID* ppvFrames,
_In_ ULONG cFramesToSkip,
_In_ ULONG cFramesToCapture)
{
ULONG cFrameCount;
PVOID apvTemp[30];
NT_ASSERT(cFramesToCapture <= _countof(apvTemp));
/* Zero it out */
RtlZeroMemory(ppvFrames, cFramesToCapture * sizeof(PVOID));
/* Capture kernel stack */
cFrameCount = RtlWalkFrameChain(apvTemp, _countof(apvTemp), 0);
/* If we should skip more than we have, we are done */
if (cFramesToSkip > cFrameCount)
return 0;
/* Copy, but skip frames */
cFrameCount -= cFramesToSkip;
cFrameCount = min(cFrameCount, cFramesToCapture);
RtlCopyMemory(ppvFrames, &apvTemp[cFramesToSkip], cFrameCount * sizeof(PVOID));
/* Check if there is still space left */
if (cFrameCount < cFramesToCapture)
{
/* Capture user stack */
cFrameCount += RtlWalkFrameChain(&ppvFrames[cFrameCount],
cFramesToCapture - cFrameCount,
1);
}
return cFrameCount;
}
#if DBG_ENABLE_GDIOBJ_BACKTRACES
static
BOOL
CompareBacktraces(
USHORT idx1,
USHORT idx2)
{
POBJ pobj1, pobj2;
ULONG iLevel;
/* Get the objects */
pobj1 = gpentHmgr[idx1].einfo.pobj;
pobj2 = gpentHmgr[idx2].einfo.pobj;
/* Loop all stack levels */
for (iLevel = 0; iLevel < GDI_OBJECT_STACK_LEVELS; iLevel++)
{
/* If one level doesn't match we are done */
if (pobj1->apvBackTrace[iLevel] != pobj2->apvBackTrace[iLevel])
{
return FALSE;
}
}
return TRUE;
}
typedef struct
{
USHORT idx;
USHORT iCount;
} GDI_DBG_HANDLE_BT;
VOID
NTAPI
DbgDumpGdiHandleTableWithBT(void)
{
static BOOL bLeakReported = FALSE;
ULONG idx, j;
BOOL bAlreadyPresent;
GDI_DBG_HANDLE_BT aBacktraceTable[GDI_DBG_MAX_BTS];
USHORT iCount;
KIRQL OldIrql;
POBJ pobj;
ULONG iLevel, ulObj;
/* Only report once */
if (bLeakReported)
{
DPRINT1("GDI handle abusers already reported!\n");
return;
}
bLeakReported = TRUE;
DPRINT1("Reporting GDI handle abusers:\n");
/* Zero out the table */
RtlZeroMemory(aBacktraceTable, sizeof(aBacktraceTable));
/* We've got serious business to do */
KeRaiseIrql(DISPATCH_LEVEL, &OldIrql);
/* Step through GDI handle table and find out who our culprit is... */
for (idx = RESERVE_ENTRIES_COUNT; idx < GDI_HANDLE_COUNT; idx++)
{
/* If the handle is free, continue */
if (gpentHmgr[idx].einfo.pobj == 0) continue;
/* Check if this backtrace is already covered */
bAlreadyPresent = FALSE;
for (j = RESERVE_ENTRIES_COUNT; j < idx; j++)
{
if (CompareBacktraces(idx, j))
{
bAlreadyPresent = TRUE;
break;
}
}
if (bAlreadyPresent) continue;
/* We don't have this BT yet, count how often it is present */
iCount = 1;
for (j = idx + 1; j < GDI_HANDLE_COUNT; j++)
{
if (CompareBacktraces(idx, j))
{
iCount++;
}
}
/* Now add this backtrace */
for (j = 0; j < GDI_DBG_MAX_BTS; j++)
{
/* Insert it below the next smaller count */
if (aBacktraceTable[j].iCount < iCount)
{
/* Check if there are entries above */
if (j < GDI_DBG_MAX_BTS - 1)
{
/* Move the following entries up by 1 */
RtlMoveMemory(&aBacktraceTable[j],
&aBacktraceTable[j + 1],
GDI_DBG_MAX_BTS - j - 1);
}
/* Set this entry */
aBacktraceTable[j].idx = idx;
aBacktraceTable[j].iCount = iCount;
/* We are done here */
break;
}
}
}
/* Print the worst offenders... */
DbgPrint("Count Handle Backtrace\n");
DbgPrint("------------------------------------------------\n");
for (j = 0; j < GDI_DBG_MAX_BTS; j++)
{
idx = aBacktraceTable[j].idx;
if (idx == 0)
break;
ulObj = ((ULONG)gpentHmgr[idx].FullUnique << 16) | idx;
pobj = gpentHmgr[idx].einfo.pobj;
DbgPrint("%5d %08lx ", aBacktraceTable[j].iCount, ulObj);
for (iLevel = 0; iLevel < GDI_OBJECT_STACK_LEVELS; iLevel++)
{
DbgPrint("%p,", pobj->apvBackTrace[iLevel]);
}
DbgPrint("\n");
}
__debugbreak();
KeLowerIrql(OldIrql);
}
#endif /* DBG_ENABLE_GDIOBJ_BACKTRACES */
#if DBG
BOOL
NTAPI
DbgGdiHTIntegrityCheck(VOID)
{
ULONG i, nDeleted = 0, nFree = 0, nUsed = 0;
PGDI_TABLE_ENTRY pEntry;
BOOL r = 1;
KeEnterCriticalRegion();
/* FIXME: Check reserved entries */
/* Now go through the deleted objects */
i = gulFirstFree & 0xffff;
while (i)
{
pEntry = &GdiHandleTable->Entries[i];
if (i >= GDI_HANDLE_COUNT)
{
DPRINT1("nDeleted=%lu\n", nDeleted);
ASSERT(FALSE);
}
nDeleted++;
/* Check the entry */
if ((pEntry->Type & GDI_ENTRY_BASETYPE_MASK) != 0)
{
r = 0;
DPRINT1("Deleted Entry has a type != 0\n");
}
if ((ULONG_PTR)pEntry->KernelData >= GDI_HANDLE_COUNT)
{
r = 0;
DPRINT1("Deleted entries KernelPointer too big\n");
}
if (pEntry->UserData != NULL)
{
r = 0;
DPRINT1("Deleted entry has UserData != 0\n");
}
if (pEntry->ProcessId != 0)
{
r = 0;
DPRINT1("Deleted entry has ProcessId != 0\n");
}
i = (ULONG_PTR)pEntry->KernelData & 0xffff;
};
for (i = gulFirstUnused;
i < GDI_HANDLE_COUNT;
i++)
{
pEntry = &GdiHandleTable->Entries[i];
if ((pEntry->Type) != 0)
{
r = 0;
DPRINT1("Free Entry has a type != 0\n");
}
if ((ULONG_PTR)pEntry->KernelData != 0)
{
r = 0;
DPRINT1("Free entries KernelPointer != 0\n");
}
if (pEntry->UserData != NULL)
{
r = 0;
DPRINT1("Free entry has UserData != 0\n");
}
if (pEntry->ProcessId != 0)
{
r = 0;
DPRINT1("Free entry has ProcessId != 0\n");
}
nFree++;
}
for (i = RESERVE_ENTRIES_COUNT; i < GDI_HANDLE_COUNT; i++)
{
HGDIOBJ Handle;
ULONG Type;
pEntry = &GdiHandleTable->Entries[i];
Type = pEntry->Type;
Handle = (HGDIOBJ)(ULONG_PTR)((Type << GDI_ENTRY_UPPER_SHIFT) + i);
if (Type & GDI_ENTRY_BASETYPE_MASK)
{
if (pEntry->KernelData == NULL)
{
r = 0;
DPRINT1("Used entry has KernelData == 0\n");
}
else if (pEntry->KernelData <= MmHighestUserAddress)
{
r = 0;
DPRINT1("Used entry invalid KernelData\n");
}
else if (((POBJ)(pEntry->KernelData))->hHmgr != Handle)
{
r = 0;
DPRINT1("Used entry %lu, has invalid hHmg %p (expected: %p)\n",
i, ((POBJ)(pEntry->KernelData))->hHmgr, Handle);
}
nUsed++;
}
}
if (RESERVE_ENTRIES_COUNT + nDeleted + nFree + nUsed != GDI_HANDLE_COUNT)
{
r = 0;
DPRINT1("Number of all entries incorrect: RESERVE_ENTRIES_COUNT = %lu, nDeleted = %lu, nFree = %lu, nUsed = %lu\n",
RESERVE_ENTRIES_COUNT, nDeleted, nFree, nUsed);
}
KeLeaveCriticalRegion();
return r;
}
#endif /* DBG */
#if DBG_ENABLE_EVENT_LOGGING
VOID
NTAPI
DbgLogEvent(PSLIST_HEADER pslh, LOG_EVENT_TYPE nEventType, LPARAM lParam)
{
PLOGENTRY pLogEntry;
/* Log a maximum of 100 events */
if (QueryDepthSList(pslh) >= 1000) return;
/* Allocate a logentry */
pLogEntry = EngAllocMem(0, sizeof(LOGENTRY), 'golG');
if (!pLogEntry) return;
/* Set type */
pLogEntry->nEventType = nEventType;
pLogEntry->ulUnique = InterlockedIncrement((LONG*)&gulLogUnique);
pLogEntry->dwProcessId = HandleToUlong(PsGetCurrentProcessId());
pLogEntry->dwThreadId = HandleToUlong(PsGetCurrentThreadId());
pLogEntry->lParam = lParam;
/* Capture a backtrace */
DbgCaptureStackBackTace(pLogEntry->apvBackTrace, 1, 20);
switch (nEventType)
{
case EVENT_ALLOCATE:
case EVENT_CREATE_HANDLE:
case EVENT_REFERENCE:
case EVENT_DEREFERENCE:
case EVENT_LOCK:
case EVENT_UNLOCK:
case EVENT_DELETE:
case EVENT_FREE:
case EVENT_SET_OWNER:
default:
break;
}
/* Push it on the list */
InterlockedPushEntrySList(pslh, &pLogEntry->sleLink);
}
#define REL_ADDR(va) ((ULONG_PTR)va - (ULONG_PTR)&__ImageBase)
VOID
NTAPI
DbgPrintEvent(PLOGENTRY pLogEntry)
{
PSTR pstr;
switch (pLogEntry->nEventType)
{
case EVENT_ALLOCATE: pstr = "Allocate"; break;
case EVENT_CREATE_HANDLE: pstr = "CreatHdl"; break;
case EVENT_REFERENCE: pstr = "Ref"; break;
case EVENT_DEREFERENCE: pstr = "Deref"; break;
case EVENT_LOCK: pstr = "Lock"; break;
case EVENT_UNLOCK: pstr = "Unlock"; break;
case EVENT_DELETE: pstr = "Delete"; break;
case EVENT_FREE: pstr = "Free"; break;
case EVENT_SET_OWNER: pstr = "SetOwner"; break;
default: pstr = "Unknown"; break;
}
DbgPrint("[%lu] %03x:%03x %.8s val=%p <%lx,%lx,%lx,%lx>\n",
pLogEntry->ulUnique,
pLogEntry->dwProcessId,
pLogEntry->dwThreadId,
pstr,
(PVOID)pLogEntry->lParam,
REL_ADDR(pLogEntry->apvBackTrace[2]),
REL_ADDR(pLogEntry->apvBackTrace[3]),
REL_ADDR(pLogEntry->apvBackTrace[4]),
REL_ADDR(pLogEntry->apvBackTrace[5]));
}
VOID
NTAPI
DbgDumpEventList(PSLIST_HEADER pslh)
{
PSLIST_ENTRY psle;
PLOGENTRY pLogEntry;
while ((psle = InterlockedPopEntrySList(pslh)))
{
pLogEntry = CONTAINING_RECORD(psle, LOGENTRY, sleLink);
DbgPrintEvent(pLogEntry);
}
}
VOID
NTAPI
DbgCleanupEventList(PSLIST_HEADER pslh)
{
PSLIST_ENTRY psle;
PLOGENTRY pLogEntry;
while ((psle = InterlockedPopEntrySList(pslh)))
{
pLogEntry = CONTAINING_RECORD(psle, LOGENTRY, sleLink);
EngFreeMem(pLogEntry);
}
}
#endif /* DBG_ENABLE_EVENT_LOGGING */
#if 1 || DBG_ENABLE_SERVICE_HOOKS
VOID
NTAPI
DbgDumpLockedGdiHandles(VOID)
{
ULONG i;
for (i = RESERVE_ENTRIES_COUNT; i < GDI_HANDLE_COUNT; i++)
{
PENTRY pentry = &gpentHmgr[i];
if (pentry->Objt)
{
POBJ pobj = pentry->einfo.pobj;
if (pobj->cExclusiveLock > 0)
{
DPRINT1("Locked object: %lx, type = %lx. allocated from:\n",
i, pentry->Objt);
DBG_DUMP_EVENT_LIST(&pobj->slhLog);
}
}
}
}
void
NTAPI
GdiDbgPreServiceHook(ULONG ulSyscallId, PULONG_PTR pulArguments)
{
PTHREADINFO pti = (PTHREADINFO)PsGetCurrentThreadWin32Thread();
if (pti && pti->cExclusiveLocks != 0)
{
DbgPrint("FATAL: Win32DbgPreServiceHook(0x%lx): There are %lu exclusive locks!\n",
ulSyscallId, pti->cExclusiveLocks);
DbgDumpLockedGdiHandles();
ASSERT(FALSE);
}
}
ULONG_PTR
NTAPI
GdiDbgPostServiceHook(ULONG ulSyscallId, ULONG_PTR ulResult)
{
PTHREADINFO pti = (PTHREADINFO)PsGetCurrentThreadWin32Thread();
if (pti && pti->cExclusiveLocks != 0)
{
DbgPrint("FATAL: Win32DbgPostServiceHook(0x%lx): There are %lu exclusive locks!\n",
ulSyscallId, pti->cExclusiveLocks);
DbgDumpLockedGdiHandles();
ASSERT(FALSE);
}
return ulResult;
}
#endif /* DBG_ENABLE_SERVICE_HOOKS */
NTSTATUS NTAPI
QueryEnvironmentVariable(PUNICODE_STRING Name,
PUNICODE_STRING Value)
{
NTSTATUS Status;
PWSTR wcs;
UNICODE_STRING var;
PWSTR val;
PPEB Peb;
PWSTR Environment;
/* Ugly HACK for ReactOS system threads */
if(!NtCurrentTeb())
{
return(STATUS_VARIABLE_NOT_FOUND);
}
Peb = NtCurrentPeb();
if (Peb == NULL)
{
return(STATUS_VARIABLE_NOT_FOUND);
}
Environment = Peb->ProcessParameters->Environment;
if (Environment == NULL)
{
return(STATUS_VARIABLE_NOT_FOUND);
}
Value->Length = 0;
wcs = Environment;
while (*wcs)
{
var.Buffer = wcs++;
wcs = wcschr(wcs, L'=');
if (wcs == NULL)
{
wcs = var.Buffer + wcslen(var.Buffer);
}
if (*wcs)
{
var.Length = var.MaximumLength = (wcs - var.Buffer) * sizeof(WCHAR);
val = ++wcs;
wcs += wcslen(wcs);
if (RtlEqualUnicodeString(&var, Name, TRUE))
{
Value->Length = (wcs - val) * sizeof(WCHAR);
if (Value->Length <= Value->MaximumLength)
{
memcpy(Value->Buffer, val,
min(Value->Length + sizeof(WCHAR), Value->MaximumLength));
Status = STATUS_SUCCESS;
}
else
{
Status = STATUS_BUFFER_TOO_SMALL;
}
return(Status);
}
}
wcs++;
}
return(STATUS_VARIABLE_NOT_FOUND);
}
static int __cdecl
DbgCompareChannels(const void * a, const void * b)
{
return wcscmp((WCHAR*)a, ((DBG_CHANNEL*)b)->Name);
}
static BOOL
DbgAddDebugChannel(PPROCESSINFO ppi, WCHAR* channel, WCHAR* level, WCHAR op)
{
DBG_CHANNEL *ChannelEntry;
UINT iLevel, iChannel;
/* Special treatment for the "all" channel */
if (wcscmp(channel, L"all") == 0)
{
for (iChannel = 0; iChannel < DbgChCount; iChannel++)
{
DbgAddDebugChannel(ppi, DbgChannels[iChannel].Name, level, op);
}
return TRUE;
}
ChannelEntry = (DBG_CHANNEL*)bsearch(channel,
DbgChannels,
DbgChCount,
sizeof(DBG_CHANNEL),
DbgCompareChannels);
if(ChannelEntry == NULL)
{
return FALSE;
}
iChannel = ChannelEntry->Id;
ASSERT(iChannel < DbgChCount);
if(level == NULL || *level == L'\0' ||wcslen(level) == 0 )
iLevel = MAX_LEVEL;
else if(wcsncmp(level, L"err", 3) == 0)
iLevel = ERR_LEVEL;
else if(wcsncmp(level, L"fixme", 5) == 0)
iLevel = FIXME_LEVEL;
else if(wcsncmp(level, L"warn", 4) == 0)
iLevel = WARN_LEVEL;
else if (wcsncmp(level, L"trace", 4) == 0)
iLevel = TRACE_LEVEL;
else
return FALSE;
if(op==L'+')
{
DBG_ENABLE_CHANNEL(ppi, iChannel, iLevel);
}
else
{
DBG_DISABLE_CHANNEL(ppi, iChannel, iLevel);
}
return TRUE;
}
static BOOL
DbgParseDebugChannels(PPROCESSINFO ppi, PUNICODE_STRING Value)
{
WCHAR *str, *separator, *c, op;
str = Value->Buffer;
do
{
separator = wcschr(str, L',');
if(separator != NULL)
*separator = L'\0';
c = wcschr(str, L'+');
if(c == NULL)
c = wcschr(str, L'-');
if(c != NULL)
{
op = *c;
*c = L'\0';
c++;
DbgAddDebugChannel(ppi, c, str, op);
}
str = separator + 1;
}while(separator != NULL);
return TRUE;
}
BOOL DbgInitDebugChannels(VOID)
{
WCHAR valBuffer[100];
UNICODE_STRING Value;
UNICODE_STRING Name = RTL_CONSTANT_STRING(L"DEBUGCHANNEL");
NTSTATUS Status;
PPROCESSINFO ppi;
BOOL ret;
/* Initialize all channels to ERROR */
ppi = PsGetCurrentProcessWin32Process();
RtlFillMemory( ppi->DbgChannelLevel,
sizeof(ppi->DbgChannelLevel),
ERR_LEVEL);
/* Find DEBUGCHANNEL env var */
Value.Buffer = valBuffer;
Value.Length = 0;
Value.MaximumLength = sizeof(valBuffer);
Status = QueryEnvironmentVariable(&Name, &Value);
/* It does not exist */
if(Status == STATUS_VARIABLE_NOT_FOUND)
{
/* There is nothing more to do */
return TRUE;
}
/* If the buffer in the stack is not enough allocate it */
if(Status == STATUS_BUFFER_TOO_SMALL)
{
Value.Buffer = ExAllocatePool(PagedPool, Value.MaximumLength);
if(Value.Buffer == NULL)
{
return FALSE;
}
/* Get the env var again */
Status = QueryEnvironmentVariable(&Name, &Value);
}
/* Check for error */
if(!NT_SUCCESS(Status))
{
if(Value.Buffer != valBuffer)
{
ExFreePool(Value.Buffer);
}
return FALSE;
}
/* Parse the variable */
ret = DbgParseDebugChannels(ppi, &Value);
/* Clean up */
if(Value.Buffer != valBuffer)
{
ExFreePool(Value.Buffer);
}
return ret;
}
#endif // DBG
/* EOF */