You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
700 lines
19 KiB
700 lines
19 KiB
/*****************************************************************************
|
|
*
|
|
* Assert.c
|
|
*
|
|
* Copyright (c) 1996 Microsoft Corporation. All Rights Reserved.
|
|
*
|
|
* Abstract:
|
|
*
|
|
* Assertions and debug output routines
|
|
* Based on Raymond's assertion code as it looks quite useful
|
|
*
|
|
*
|
|
* Contents:
|
|
*
|
|
* DebugOutPtszV
|
|
* AssertPtszPtszLn
|
|
* ArgsPalPszV
|
|
* EnterDbgflPszPal
|
|
* ExitDbgflPalHresPpv
|
|
*
|
|
*****************************************************************************/
|
|
|
|
#include "pch.h"
|
|
|
|
|
|
#ifdef MAXDEBUG
|
|
|
|
/*****************************************************************************
|
|
*
|
|
* WarnPszV
|
|
*
|
|
* Display a message, suitable for framing.
|
|
*
|
|
*****************************************************************************/
|
|
|
|
#pragma BEGIN_CONST_DATA
|
|
|
|
CHAR c_szPrefix[] = "STI: ";
|
|
|
|
#pragma END_CONST_DATA
|
|
|
|
void EXTERNAL
|
|
WarnPszV(LPCSTR psz, ...)
|
|
{
|
|
va_list ap;
|
|
CHAR sz[1024];
|
|
|
|
lstrcpyA(sz, c_szPrefix);
|
|
va_start(ap, psz);
|
|
wvsprintfA(sz + cA(c_szPrefix) - 1, psz, ap);
|
|
va_end(ap);
|
|
lstrcatA(sz, "\r\n");
|
|
OutputDebugStringA(sz);
|
|
}
|
|
|
|
#endif
|
|
|
|
#ifdef DEBUG
|
|
|
|
/*****************************************************************************
|
|
*
|
|
* Globals
|
|
*
|
|
*****************************************************************************/
|
|
|
|
DBGFL DbgflCur;
|
|
|
|
TCHAR g_tszLogFile[MAX_PATH] = {'\0'};
|
|
|
|
/*****************************************************************************
|
|
*
|
|
* Set current trace parameters
|
|
*
|
|
* This routine is not thread safe
|
|
*
|
|
*****************************************************************************/
|
|
|
|
|
|
VOID InitializeDebuggingSupport(VOID)
|
|
{
|
|
|
|
HKEY hStiSettingsKey;
|
|
DWORD dwErr;
|
|
DWORD dwType;
|
|
|
|
CHAR szLogFile[MAX_PATH];
|
|
UINT cbBuffer ;
|
|
|
|
DBGFL dwFlags = 0;
|
|
|
|
dwErr = OSUtil_RegOpenKeyExW(HKEY_LOCAL_MACHINE,
|
|
REGSTR_PATH_STICONTROL_W,
|
|
0L,KEY_READ,
|
|
&hStiSettingsKey
|
|
);
|
|
|
|
if (NOERROR != dwErr) {
|
|
return;
|
|
}
|
|
|
|
cbBuffer = MAX_PATH;
|
|
ZeroX(szLogFile);
|
|
|
|
dwErr = RegQueryValueExA( hStiSettingsKey,
|
|
REGSTR_VAL_DEBUG_FILE_A,
|
|
NULL,
|
|
&dwType,
|
|
(LPBYTE)szLogFile,
|
|
&cbBuffer );
|
|
|
|
if( ( dwErr == NO_ERROR ) || ( dwErr == ERROR_MORE_DATA ) ) {
|
|
if( ( dwType != REG_SZ ) &&
|
|
( dwType != REG_MULTI_SZ ) &&
|
|
( dwType != REG_EXPAND_SZ ) ) {
|
|
|
|
dwErr = ERROR_FILE_NOT_FOUND;
|
|
}
|
|
else {
|
|
SetDebugLogFileA(szLogFile);
|
|
}
|
|
}
|
|
|
|
dwFlags = ReadRegistryDwordW(hStiSettingsKey,
|
|
REGSTR_VAL_DEBUG_FLAGS_W,
|
|
0L);
|
|
|
|
SetCurrentDebugFlags(dwFlags ) ;
|
|
|
|
RegCloseKey(hStiSettingsKey);
|
|
|
|
return ;
|
|
|
|
}
|
|
|
|
DBGFL SetCurrentDebugFlags(DBGFL NewFlags) {
|
|
|
|
DBGFL OldFlags = DbgflCur;
|
|
DbgflCur = NewFlags;
|
|
return OldFlags;
|
|
}
|
|
|
|
VOID SetDebugLogFileA(CHAR *pszLogFileName)
|
|
{
|
|
if (!pszLogFileName || !*pszLogFileName) {
|
|
*g_tszLogFile = '\0';
|
|
return;
|
|
}
|
|
|
|
lstrcpyA(g_tszLogFile,pszLogFileName);
|
|
return;
|
|
}
|
|
|
|
/*****************************************************************************
|
|
*
|
|
* DebugOutPtsz
|
|
*
|
|
* Writes a message to the debugger and maybe a log file.
|
|
*
|
|
*****************************************************************************/
|
|
|
|
void INTERNAL
|
|
DebugOutPtsz(LPCTSTR ptsz)
|
|
{
|
|
DWORD cbWritten;
|
|
|
|
OutputDebugString(ptsz);
|
|
if (g_tszLogFile[0]) {
|
|
HANDLE h = CreateFile(g_tszLogFile, GENERIC_WRITE,
|
|
FILE_SHARE_READ | FILE_SHARE_WRITE,
|
|
0, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, 0);
|
|
if (h != INVALID_HANDLE_VALUE) {
|
|
#ifdef UNICODE
|
|
CHAR szBuf[1024];
|
|
#endif
|
|
SetFilePointer(h, 0, 0, FILE_END);
|
|
#ifdef UNICODE
|
|
WriteFile(h, szBuf, UToA(szBuf, cA(szBuf), ptsz),&cbWritten,NULL);
|
|
#else
|
|
WriteFile(h, ptsz, cbCtch(lstrlen(ptsz)),&cbWritten,NULL);
|
|
#endif
|
|
CloseHandle(h);
|
|
}
|
|
}
|
|
}
|
|
|
|
/*****************************************************************************
|
|
*
|
|
* DebugOutPtszA
|
|
*
|
|
* DebugOut an ANSI message to the debugger and maybe a log file.
|
|
*
|
|
*****************************************************************************/
|
|
|
|
#ifdef UNICODE
|
|
|
|
void INTERNAL
|
|
DebugOutPtszA(LPCSTR psz)
|
|
{
|
|
OutputDebugStringA(psz);
|
|
if (g_tszLogFile[0]) {
|
|
HANDLE h = CreateFile(g_tszLogFile, GENERIC_WRITE,
|
|
FILE_SHARE_READ | FILE_SHARE_WRITE,
|
|
0, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, 0);
|
|
if (h != INVALID_HANDLE_VALUE) {
|
|
_lwrite((HFILE)h, psz, cbCch(lstrlenA(psz)));
|
|
CloseHandle(h);
|
|
}
|
|
}
|
|
}
|
|
|
|
#else
|
|
|
|
#define DebugOutPtszA DebugOutPtsz
|
|
|
|
#endif
|
|
|
|
/*****************************************************************************
|
|
*
|
|
* DebugOutPtszV
|
|
*
|
|
* DebugOut a message with a trailing crlf.
|
|
*
|
|
*****************************************************************************/
|
|
|
|
void EXTERNAL
|
|
DebugOutPtszV(DBGFL Dbgfl, LPCTSTR ptsz, ...)
|
|
{
|
|
if (Dbgfl == 0 || (Dbgfl & DbgflCur)) {
|
|
va_list ap;
|
|
TCHAR tsz[1024];
|
|
va_start(ap, ptsz);
|
|
wvsprintf(tsz, ptsz, ap);
|
|
va_end(ap);
|
|
lstrcat(tsz, TEXT("\r\n"));
|
|
DebugOutPtsz(tsz);
|
|
}
|
|
}
|
|
|
|
/*****************************************************************************
|
|
*
|
|
* AssertPtszPtszLn
|
|
*
|
|
* Something bad happened.
|
|
*
|
|
*****************************************************************************/
|
|
|
|
int EXTERNAL
|
|
AssertPtszPtszLn(LPCTSTR ptszExpr, LPCTSTR ptszFile, int iLine)
|
|
{
|
|
DebugOutPtszV(DbgFlAlways, TEXT("Assertion failed: `%s' at %s(%d)"),
|
|
ptszExpr, ptszFile, iLine);
|
|
DebugBreak();
|
|
return 0;
|
|
}
|
|
|
|
/*****************************************************************************
|
|
*
|
|
* Procedure call tracing is gross because of the C preprocessor.
|
|
*
|
|
* Oh, if only we had support for m4...
|
|
*
|
|
*****************************************************************************/
|
|
|
|
/*****************************************************************************
|
|
*
|
|
* ArgsPszV
|
|
*
|
|
* Collect arguments to a procedure.
|
|
*
|
|
* psz -> ASCIIZ format string
|
|
* ... = argument list
|
|
*
|
|
* The characters in the format string are listed in EmitPal.
|
|
*
|
|
*****************************************************************************/
|
|
|
|
void EXTERNAL
|
|
ArgsPalPszV(PARGLIST pal, LPCSTR psz, ...)
|
|
{
|
|
va_list ap;
|
|
va_start(ap, psz);
|
|
if (psz) {
|
|
PPV ppv;
|
|
pal->pszFormat = psz;
|
|
for (ppv = pal->rgpv; *psz; psz++) {
|
|
*ppv++ = va_arg(ap, PV);
|
|
}
|
|
} else {
|
|
pal->pszFormat = "";
|
|
}
|
|
}
|
|
|
|
/*****************************************************************************
|
|
*
|
|
* EmitPal
|
|
*
|
|
* OutputDebugString the information, given a pal. No trailing
|
|
* carriage return is emitted.
|
|
*
|
|
* pal -> place where info was saved
|
|
*
|
|
* Format characters:
|
|
*
|
|
* p - 32-bit flat pointer
|
|
* x - 32-bit hex integer
|
|
* s - TCHAR string
|
|
* A - ANSI string
|
|
* W - UNICODE string
|
|
* G - GUID
|
|
* u - unsigned integer
|
|
* C - clipboard format
|
|
*
|
|
*****************************************************************************/
|
|
|
|
void INTERNAL
|
|
EmitPal(PARGLIST pal)
|
|
{
|
|
char sz[MAX_PATH];
|
|
int i;
|
|
DebugOutPtszA(pal->pszProc);
|
|
DebugOutPtsz(TEXT("("));
|
|
for (i = 0; pal->pszFormat[i]; i++) {
|
|
if (i) {
|
|
DebugOutPtsz(TEXT(", "));
|
|
}
|
|
switch (pal->pszFormat[i]) {
|
|
|
|
case 'p': /* 32-bit flat pointer */
|
|
case 'x': /* 32-bit hex */
|
|
wsprintfA(sz, "%08x", pal->rgpv[i]);
|
|
DebugOutPtszA(sz);
|
|
break;
|
|
|
|
case 's': /* TCHAR string */
|
|
if (pal->rgpv[i]) {
|
|
DebugOutPtsz(pal->rgpv[i]);
|
|
}
|
|
break;
|
|
|
|
case 'A': /* ANSI string */
|
|
if (pal->rgpv[i]) {
|
|
DebugOutPtszA(pal->rgpv[i]);
|
|
}
|
|
break;
|
|
|
|
case 'W': /* UNICODE string */
|
|
#ifdef UNICODE
|
|
OutputDebugStringW(pal->rgpv[i]);
|
|
#else
|
|
WideCharToMultiByte(CP_ACP, 0, pal->rgpv[i], -1, sz, cA(sz), 0, 0);
|
|
DebugOutPtszA(sz);
|
|
#endif
|
|
break;
|
|
|
|
#ifndef _WIN64
|
|
//
|
|
// Ignore this option on SunDown
|
|
//
|
|
case 'G': /* GUID */
|
|
wsprintfA(sz, "%08x",
|
|
HIWORD(pal->rgpv[i]) ? *(LPDWORD)pal->rgpv[i]
|
|
: (DWORD)pal->rgpv[i]);
|
|
DebugOutPtszA(sz);
|
|
break;
|
|
|
|
case 'C':
|
|
if (GetClipboardFormatNameA((UINT)pal->rgpv[i], sz, cA(sz))) {
|
|
} else {
|
|
wsprintfA(sz, "[%04x]", pal->rgpv[i]);
|
|
}
|
|
DebugOutPtszA(sz);
|
|
break;
|
|
#endif
|
|
|
|
case 'u': /* 32-bit unsigned decimal */
|
|
wsprintfA(sz, "%u", pal->rgpv[i]);
|
|
DebugOutPtszA(sz);
|
|
break;
|
|
|
|
|
|
default: AssertF(0); /* Invalid */
|
|
}
|
|
}
|
|
DebugOutPtsz(TEXT(")"));
|
|
}
|
|
|
|
/*****************************************************************************
|
|
*
|
|
* EnterDbgflPtsz
|
|
*
|
|
* Mark entry to a procedure. Arguments were already collected by
|
|
* ArgsPszV.
|
|
*
|
|
* Dbgfl -> DebugOuty flags
|
|
* pszProc -> procedure name
|
|
* pal -> place to save the name and get the format/args
|
|
*
|
|
*****************************************************************************/
|
|
|
|
void EXTERNAL
|
|
EnterDbgflPszPal(DBGFL Dbgfl, LPCSTR pszProc, PARGLIST pal)
|
|
{
|
|
pal->pszProc = pszProc;
|
|
if (Dbgfl == 0 || (Dbgfl & DbgflCur)) {
|
|
EmitPal(pal);
|
|
DebugOutPtsz(TEXT("\r\n"));
|
|
}
|
|
}
|
|
|
|
/*****************************************************************************
|
|
*
|
|
* ExitDbgflPalHresPpv
|
|
*
|
|
* Mark exit from a procedure.
|
|
*
|
|
* pal -> argument list
|
|
* hres -> exit result
|
|
* ppv -> optional OUT pointer;
|
|
* ppvDword means that hres is a dword
|
|
* ppvBool means that hres is a boolean
|
|
* ppvVoid means that hres is nothing at all
|
|
*
|
|
*****************************************************************************/
|
|
|
|
void EXTERNAL
|
|
ExitDbgflPalHresPpv(DBGFL Dbgfl, PARGLIST pal, HRESULT hres, PPV ppvObj)
|
|
{
|
|
BOOL fInternalError;
|
|
DWORD le = GetLastError();
|
|
|
|
fInternalError = 0;
|
|
if (ppvObj == ppvVoid) {
|
|
} else if (ppvObj == ppvBool) {
|
|
if (hres == 0) {
|
|
Dbgfl |= DbgFlError;
|
|
}
|
|
} else {
|
|
if (FAILED(hres)) {
|
|
if (fLimpFF(ppvObj && !IsBadWritePtr(ppvObj, cbX(*ppvObj)),
|
|
*ppvObj == 0)) {
|
|
} else {
|
|
fInternalError = 1;
|
|
}
|
|
Dbgfl |= DbgFlError;
|
|
}
|
|
}
|
|
|
|
if (Dbgfl == 0 || (Dbgfl & DbgflCur) || fInternalError) {
|
|
EmitPal(pal);
|
|
DebugOutPtsz(TEXT(" -> "));
|
|
if (ppvObj != ppvVoid) {
|
|
TCHAR tszBuf[32];
|
|
wsprintf(tszBuf, TEXT("%08x"), hres);
|
|
DebugOutPtsz(tszBuf);
|
|
if (HIWORD(PtrToLong(ppvObj))) {
|
|
wsprintf(tszBuf, TEXT(" [%08x]"), *ppvObj);
|
|
DebugOutPtsz(tszBuf);
|
|
} else if (ppvObj == ppvDword) {
|
|
wsprintf(tszBuf, TEXT(" [%08x]"), hres);
|
|
DebugOutPtsz(tszBuf);
|
|
} else if (ppvObj == ppvBool) {
|
|
wsprintf(tszBuf, hres ? TEXT(" OK ") :
|
|
TEXT(" le=[%d]"), le);
|
|
DebugOutPtsz(tszBuf);
|
|
}
|
|
}
|
|
DebugOutPtsz(TEXT("\r\n"));
|
|
AssertF(!fInternalError);
|
|
}
|
|
|
|
/*
|
|
* This redundant test prevents a breakpoint on SetLastError()
|
|
* from being hit constantly.
|
|
*/
|
|
if (le != GetLastError()) {
|
|
SetLastError(le);
|
|
}
|
|
}
|
|
|
|
#endif
|
|
|
|
#ifdef MAXDEBUG
|
|
|
|
/*****************************************************************************
|
|
*
|
|
* @doc INTERNAL
|
|
*
|
|
* @func DWORD | Random |
|
|
*
|
|
* Returns a pseudorandom dword. The value doesn't need to be
|
|
* statistically wonderful.
|
|
*
|
|
* @returns
|
|
* A not very random dword.
|
|
*
|
|
*****************************************************************************/
|
|
|
|
DWORD s_dwRandom = 1; /* Random number seed */
|
|
|
|
DWORD INLINE
|
|
Random(void)
|
|
{
|
|
s_dwRandom = s_dwRandom * 214013 + 2531011;
|
|
return s_dwRandom;
|
|
}
|
|
|
|
|
|
/*****************************************************************************
|
|
*
|
|
* @doc INTERNAL
|
|
*
|
|
* @func void | ScrambleBuf |
|
|
*
|
|
* Fill a buffer with garbage. Used in RDEBUG to make sure
|
|
* the caller is not relying on buffer data.
|
|
*
|
|
* Note: If the buffer is not a multiple of dwords in size,
|
|
* the leftover bytes are not touched.
|
|
*
|
|
* @parm OUT LPVOID | pv |
|
|
*
|
|
* The buffer to be scrambled.
|
|
*
|
|
* @parm UINT | cb |
|
|
*
|
|
* The size of the buffer.
|
|
*
|
|
*****************************************************************************/
|
|
|
|
void EXTERNAL
|
|
ScrambleBuf(LPVOID pv, UINT cb)
|
|
{
|
|
UINT idw;
|
|
UINT cdw = cb / 4;
|
|
LPDWORD pdw = pv;
|
|
for (idw = 0; idw < cdw; idw++) {
|
|
pdw[idw] = Random();
|
|
}
|
|
}
|
|
|
|
/*****************************************************************************
|
|
*
|
|
* @doc INTERNAL
|
|
*
|
|
* @func void | ScrambleBit |
|
|
*
|
|
* Randomly set or clear a bit.
|
|
*
|
|
* @parm OUT LPDWORD | pdw |
|
|
*
|
|
* The dword whose bit is to be set randomly.
|
|
*
|
|
* @parm UINT | flMask |
|
|
*
|
|
* Mask for the bits to scramble.
|
|
*
|
|
*****************************************************************************/
|
|
|
|
void EXTERNAL ScrambleBit(LPDWORD pdw, DWORD flMask)
|
|
{
|
|
*pdw ^= (*pdw ^ Random()) & flMask;
|
|
}
|
|
|
|
/*****************************************************************************
|
|
*
|
|
* @doc INTERNAL
|
|
*
|
|
* @func BOOL | Callback_CompareContexts |
|
|
*
|
|
* Check if two <t CONTEXT> structures are substantially the same
|
|
* to the extent required by the Win32 calling convention.
|
|
*
|
|
* This is necessary because lots of applications pass
|
|
* incorrectly prototyped functions as callbacks. Others will
|
|
* write callback functions that trash registers that are
|
|
* supposed to be nonvolatile. Yuck!
|
|
*
|
|
* NOTE! Platform-dependent code!
|
|
*
|
|
* @parm LPCONTEXT | pctx1 |
|
|
*
|
|
* Context structure before we call the callback.
|
|
*
|
|
* @parm LPCONTEXT | pctx2 |
|
|
*
|
|
* Context structure after we call the callback.
|
|
*
|
|
* @returns
|
|
*
|
|
* Nonzero if the two contexts are substantially the same.
|
|
*
|
|
*****************************************************************************/
|
|
|
|
BOOL INLINE
|
|
Callback_CompareContexts(LPCONTEXT pctx1, LPCONTEXT pctx2)
|
|
{
|
|
#if defined(_X86_)
|
|
return pctx1->Esp == pctx2->Esp; /* Stack pointer */
|
|
#if 0
|
|
/*
|
|
* Can't test these registers because Win95 doesn't preserve
|
|
* them properly. GetThreadContext() stashes what happens to
|
|
* be in the registers when you finally reach the bowels of
|
|
* kernel, at which point who knows what they contain...
|
|
*/
|
|
pctx1->Ebx == pctx2->Ebx && /* Nonvolatile registers */
|
|
pctx1->Esi == pctx2->Esi &&
|
|
pctx1->Edi == pctx2->Edi &&
|
|
pctx1->Ebp == pctx2->Ebp;
|
|
#endif
|
|
|
|
#elif defined(_ALPHA_)
|
|
return pctx1->IntSp == pctx2->IntSp && /* Stack pointer */
|
|
pctx1->IntS0 == pctx2->IntS0 && /* Nonvolatile registers */
|
|
pctx1->IntS1 == pctx2->IntS1 &&
|
|
pctx1->IntS2 == pctx2->IntS2 &&
|
|
pctx1->IntS3 == pctx2->IntS3 &&
|
|
pctx1->IntS4 == pctx2->IntS4 &&
|
|
pctx1->IntS5 == pctx2->IntS5 &&
|
|
pctx1->IntFp == pctx2->IntFp;
|
|
|
|
#elif defined(_MIPS_)
|
|
#pragma message("I hope this is correct for MIPS")
|
|
return pctx1->IntSp == pctx2->IntSp && /* Stack pointer */
|
|
pctx1->IntS0 == pctx2->IntS0 && /* Nonvolatile registers */
|
|
pctx1->IntS1 == pctx2->IntS1 &&
|
|
pctx1->IntS2 == pctx2->IntS2 &&
|
|
pctx1->IntS3 == pctx2->IntS3 &&
|
|
pctx1->IntS4 == pctx2->IntS4 &&
|
|
pctx1->IntS6 == pctx2->IntS6 &&
|
|
pctx1->IntS7 == pctx2->IntS7 &&
|
|
pctx1->IntS8 == pctx2->IntS8;
|
|
|
|
#elif defined(_PPC_)
|
|
#pragma message("I don't know what the PPC calling conventions are")
|
|
|
|
/* Just check the stack register */
|
|
return pctx1->Gpr1 == pctx2->Gpr1;
|
|
|
|
#else
|
|
#pragma message("I don't know what the calling conventions are for this platform")
|
|
return 1;
|
|
#endif
|
|
}
|
|
|
|
|
|
/*****************************************************************************
|
|
*
|
|
* @doc INTERNAL
|
|
*
|
|
* @func BOOL | Callback |
|
|
*
|
|
* Perform a callback the paranoid way, checking that the
|
|
* application used the correct calling convention and preserved
|
|
* all nonvolatile registers.
|
|
*
|
|
* NOTE! Platform-dependent code!
|
|
*
|
|
* @parm STICALLBACKPROC | pfn |
|
|
*
|
|
* Procedure to call back.
|
|
*
|
|
* @parm PV | pv1 |
|
|
*
|
|
* First parameter to callback.
|
|
*
|
|
* @parm PV | pv2 |
|
|
*
|
|
* Second parameter to callback.
|
|
*
|
|
* @returns
|
|
*
|
|
* Whatever the callback returns.
|
|
*
|
|
*****************************************************************************/
|
|
|
|
BOOL EXTERNAL
|
|
Callback(STICALLBACKPROC pfn, PV pv1, PV pv2)
|
|
{
|
|
CONTEXT ctxPre; /* Thread context before call */
|
|
CONTEXT ctxPost; /* Thread context after call */
|
|
volatile BOOL fRc; /* To prevent compiler from enregistering */
|
|
|
|
/* Get state of registers before the callback */
|
|
ctxPre.ContextFlags = CONTEXT_CONTROL | CONTEXT_INTEGER;
|
|
GetThreadContext(GetCurrentThread(), &ctxPre);
|
|
|
|
fRc = pfn(pv1, pv2);
|
|
|
|
ctxPost.ContextFlags = CONTEXT_CONTROL | CONTEXT_INTEGER;
|
|
if (GetThreadContext(GetCurrentThread(), &ctxPost) &&
|
|
!Callback_CompareContexts(&ctxPre, &ctxPost)) {
|
|
RPF("STI: Incorrectly prototyped callback! Crash soon!");
|
|
ValidationException();
|
|
}
|
|
|
|
return fRc;
|
|
}
|
|
|
|
#endif
|