1
0
forked from 0ad/0ad
0ad/source/lib/debug.h
2022-01-31 06:53:30 +00:00

553 lines
19 KiB
C++

/* Copyright (C) 2022 Wildfire Games.
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be included
* in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
/*
* platform-independent debug support code.
*/
#ifndef INCLUDED_DEBUG
#define INCLUDED_DEBUG
// this module provides platform-independent debug facilities, useful for
// diagnosing and reporting program errors.
// - a symbol engine provides access to compiler-generated debug information and
// can also give a stack trace including local variables;
// - our more powerful assert() replacement gives a stack trace so
// that the underlying problem becomes apparent;
// - the output routines make for platform-independent logging and
// crashlogs with "last-known activity" reporting.
#include "lib/alignment.h"
#include "lib/code_annotation.h"
#include "lib/code_generation.h"
#include "lib/status.h"
#include "lib/types.h"
/**
* trigger a breakpoint when reached/"called".
* if defined as a macro, the debugger can break directly into the
* target function instead of one frame below it as with a conventional
* call-based implementation.
**/
#if MSC_VERSION
# define debug_break __debugbreak // intrinsic "function"
#else
extern void debug_break();
#endif
//-----------------------------------------------------------------------------
// output
//-----------------------------------------------------------------------------
/**
* write a formatted string to the debug channel, subject to filtering
* (see below). implemented via debug_puts - see performance note there.
*
* @param fmt Format string and varargs; see printf.
**/
void debug_printf(const char* fmt, ...) PRINTF_ARGS(1);
/**
* translates and displays the given strings in a dialog.
* this is typically only used when debug_DisplayError has failed or
* is unavailable because that function is much more capable.
* implemented via sys_display_msg; see documentation there.
**/
void debug_DisplayMessage(const wchar_t* caption, const wchar_t* msg);
/// flags to customize debug_DisplayError behavior
enum DebugDisplayErrorFlags
{
/**
* disallow the Continue button. used e.g. if an exception is fatal.
**/
DE_NO_CONTINUE = 1,
/**
* enable the Suppress button. set automatically by debug_DisplayError if
* it receives a non-NULL suppress pointer. a flag is necessary because
* the sys_display_error interface doesn't get that pointer.
* rationale for automatic setting: this may prevent someone from
* forgetting to specify it, and disabling Suppress despite having
* passed a non-NULL pointer doesn't make much sense.
**/
DE_ALLOW_SUPPRESS = 2,
/**
* do not trigger a breakpoint inside debug_DisplayError; caller
* will take care of this if ER_BREAK is returned. this is so that the
* debugger can jump directly into the offending function.
**/
DE_MANUAL_BREAK = 4,
/**
* display just the given message; do not add any information about the
* call stack, do not write crashlogs, etc.
*/
DE_NO_DEBUG_INFO = 8
};
/**
* a bool that is reasonably certain to be set atomically.
* we cannot assume support for OpenMP (requires GCC 4.2) or C++0x,
* so we'll have to resort to intptr_t, cpu_CAS and COMPILER_FENCE.
**/
typedef volatile intptr_t atomic_bool;
/**
* value for suppress flag once set by debug_DisplayError.
* rationale: this value is fairly distinctive and helps when
* debugging the symbol engine.
* use 0 as the initial value to avoid allocating .rdata space.
**/
static const atomic_bool DEBUG_SUPPRESS = 0xAB;
/**
* choices offered by the error dialog that are returned
* by debug_DisplayError.
**/
enum ErrorReaction
{
/**
* ignore, continue as if nothing happened.
* note: value doesn't start at 0 because that is interpreted as a
* DialogBoxParam failure.
**/
ER_CONTINUE = 1,
/**
* trigger breakpoint, i.e. enter debugger.
* only returned if DE_MANUAL_BREAK was passed; otherwise,
* debug_DisplayError will trigger a breakpoint itself.
**/
ER_BREAK
};
/**
* all choices offered by the error dialog. those not defined in
* ErrorReaction are acted upon by debug_DisplayError and
* never returned to callers.
* (this separation avoids enumerator-not-handled warnings)
**/
enum ErrorReactionInternal
{
ERI_CONTINUE = ER_CONTINUE,
ERI_BREAK = ER_BREAK,
/**
* ignore and do not report again.
* note: non-persistent; only applicable during this program run.
**/
ERI_SUPPRESS,
/**
* exit the program immediately.
**/
ERI_EXIT,
/**
* special return value for the display_error app hook stub to indicate
* that it has done nothing and that the normal sys_display_error
* implementation should be called instead.
**/
ERI_NOT_IMPLEMENTED
};
/**
* display an error dialog with a message and stack trace.
*
* @param description text to show.
* @param flags: see DebugDisplayErrorFlags.
* @param context, lastFuncToSkip: see debug_DumpStack.
* @param file, line, func: location of the error (typically passed as
* WIDEN(__FILE__), __LINE__, __func__ from a macro)
* @param suppress pointer to a caller-allocated flag that can be used to
* suppress this error. if NULL, this functionality is skipped and the
* "Suppress" dialog button will be disabled.
* note: this flag is read and written exclusively here; caller only
* provides the storage. values: see DEBUG_SUPPRESS above.
* @return ErrorReaction (user's choice: continue running or stop?)
**/
ErrorReaction debug_DisplayError(const wchar_t* description, size_t flags, void* context, const wchar_t* lastFuncToSkip, const wchar_t* file, int line, const char* func, atomic_bool* suppress);
// simplified version for just displaying an error message
#define DEBUG_DISPLAY_ERROR_IMPL(description, flags)\
do\
{\
CACHE_ALIGNED(u8) context[DEBUG_CONTEXT_SIZE];\
(void)debug_CaptureContext(context);\
(void)debug_DisplayError(description, flags, context, L"debug_DisplayError", WIDEN(__FILE__), __LINE__, __func__, 0);\
}\
while(0)
#define DEBUG_DISPLAY_ERROR(description) DEBUG_DISPLAY_ERROR_IMPL(description, 0)
// disallow continue for the error.
#define DEBUG_DISPLAY_FATAL_ERROR(description) DEBUG_DISPLAY_ERROR_IMPL(description, DE_NO_CONTINUE)
//
// filtering
//
/**
* debug output is very useful, but "too much of a good thing can kill you".
* we don't want to require different LOGn() macros that are enabled
* depending on "debug level", because changing that entails lengthy
* compiles and it's too coarse-grained. instead, we require all
* strings to start with "tag_string|" (exact case and no quotes;
* the alphanumeric-only \<tag_string\> identifies output type).
* they are then subject to filtering: only if the tag has been
* "added" via debug_filter_add is the appendant string displayed.
*
* this approach is easiest to implement and is fine because we control
* all logging code. LIMODS falls from consideration since it's not
* portable and too complex.
*
* notes:
* - filter changes only affect subsequent debug_*printf calls;
* output that didn't pass the filter is permanently discarded.
* - strings not starting with a tag are always displayed.
* - debug_filter_* can be called at any time and from the debugger,
* but are not reentrant.
*
* in future, allow output with the given tag to proceed.
* no effect if already added.
**/
void debug_filter_add(const char* tag);
/**
* in future, discard output with the given tag.
* no effect if not currently added.
**/
void debug_filter_remove(const char* tag);
/**
* clear all filter state; equivalent to debug_filter_remove for
* each tag that was debug_filter_add-ed.
**/
void debug_filter_clear();
/**
* indicate if the given text would be printed.
* useful for a series of debug_printfs - avoids needing to add a tag to
* each of their format strings.
**/
bool debug_filter_allows(const char* text);
/**
* call debug_puts if debug_filter_allows allows the string.
**/
void debug_puts_filtered(const char* text);
/**
* write an error description and all logs into crashlog.txt
* (in unicode format).
*
* @param text description of the error (including stack trace);
* typically generated by debug_BuildErrorMessage.
*
* @return Status; ERR::REENTERED if reentered via recursion or
* multithreading (not allowed since an infinite loop may result).
**/
Status debug_WriteCrashlog(const char* text);
//-----------------------------------------------------------------------------
// assertions
//-----------------------------------------------------------------------------
/**
* ensure the expression \<expr\> evaluates to non-zero. used to validate
* invariants in the program during development and thus gives a
* very helpful warning if something isn't going as expected.
* sprinkle these liberally throughout your code!
*
* to pass more information to users at runtime, you can write
* ENSURE(expression && "descriptive string").
**/
#define ENSURE(expr)\
do\
{\
static atomic_bool suppress__;\
if(!(expr))\
{\
switch(debug_OnAssertionFailure(WIDEN(#expr), &suppress__, WIDEN(__FILE__), __LINE__, __func__))\
{\
case ER_CONTINUE:\
break;\
case ER_BREAK:\
default:\
debug_break();\
break;\
}\
}\
}\
while(0)
/**
* same as ENSURE in debug mode, does nothing in release mode.
* (we don't override the `assert' macro because users may
* inadvertently include \<assert.h\> afterwards)
* (we do not provide an MFC-style VERIFY macro because the distinction
* between ENSURE and VERIFY is unclear. to always run code but only
* check for success in debug builds without raising unused-variable warnings,
* use ASSERT + UNUSED2.)
**/
#define ASSERT(expr) ENSURE(expr)
#ifdef NDEBUG
# undef ASSERT
# define ASSERT(expr)
#endif
/**
* display the error dialog with the given text. this is less error-prone than
* ENSURE(0 && "text"). note that "conditional expression is constant" warnings
* are disabled anyway.
*
* if being able to suppress the warning is desirable (e.g. for self-tests),
* then use DEBUG_WARN_ERR instead.
**/
#define debug_warn(expr) ENSURE(0 && (expr))
/**
* display the error dialog with text corresponding to the given error code.
* used by WARN_RETURN_STATUS_IF_ERR et al., which wrap function calls and automatically
* raise warnings and return to the caller.
**/
#define DEBUG_WARN_ERR(status)\
do\
{\
static atomic_bool suppress__;\
switch(debug_OnError(status, &suppress__, WIDEN(__FILE__), __LINE__, __func__))\
{\
case ER_CONTINUE:\
break;\
case ER_BREAK:\
default:\
debug_break();\
break;\
}\
}\
while(0)
/**
* called when a ENSURE/ASSERT fails;
* notifies the user via debug_DisplayError.
*
* @param assert_expr the expression that failed; typically passed as
* \#expr in the assert macro.
* @param suppress see debug_DisplayError.
* @param file, line source file name and line number of the spot that failed
* @param func name of the function containing it
* @return ErrorReaction (user's choice: continue running or stop?)
**/
ErrorReaction debug_OnAssertionFailure(const wchar_t* assert_expr, atomic_bool* suppress, const wchar_t* file, int line, const char* func) ANALYZER_NORETURN;
/**
* called when a DEBUG_WARN_ERR indicates an error occurred;
* notifies the user via debug_DisplayError.
*
* @param err Status value indicating the error that occurred
* @param suppress see debug_DisplayError.
* @param file, line source file name and line number of the spot that failed
* @param func name of the function containing it
* @return ErrorReaction (user's choice: continue running or stop?)
**/
ErrorReaction debug_OnError(Status err, atomic_bool* suppress, const wchar_t* file, int line, const char* func) ANALYZER_NORETURN;
/**
* suppress (prevent from showing) the error dialog from subsequent
* debug_OnError for the given Status.
*
* rationale: for edge cases in some functions, warnings are raised in
* addition to returning an error code. self-tests deliberately trigger
* these cases and check for the latter but shouldn't cause the former.
* we therefore need to squelch them.
*
* @param err the Status to skip.
*
* note: only one concurrent skip request is allowed; call
* debug_StopSkippingErrors before the next debug_SkipErrors.
*/
void debug_SkipErrors(Status err);
/**
* @return how many errors were skipped since the call to debug_SkipErrors()
**/
size_t debug_StopSkippingErrors();
//-----------------------------------------------------------------------------
// symbol access
//-----------------------------------------------------------------------------
namespace ERR
{
const Status SYM_NO_STACK_FRAMES_FOUND = -100400;
const Status SYM_UNRETRIEVABLE_STATIC = -100401;
const Status SYM_UNRETRIEVABLE = -100402;
const Status SYM_TYPE_INFO_UNAVAILABLE = -100403;
const Status SYM_INTERNAL_ERROR = -100404;
const Status SYM_UNSUPPORTED = -100405;
const Status SYM_CHILD_NOT_FOUND = -100406;
// this limit is to prevent infinite recursion.
const Status SYM_NESTING_LIMIT = -100407;
// this limit is to prevent large symbols (e.g. arrays or linked lists)
// from taking up all available output space.
const Status SYM_SINGLE_SYMBOL_LIMIT = -100408;
}
namespace INFO
{
// one of the dump_sym* functions decided not to output anything at
// all (e.g. for member functions in UDTs - we don't want those).
// therefore, skip any post-symbol formatting (e.g. ) as well.
const Status SYM_SUPPRESS_OUTPUT = +100409;
}
/**
* Maximum number of characters (including null terminator) written to
* user's buffers by debug_ResolveSymbol.
**/
static const size_t DEBUG_SYMBOL_CHARS = 1000;
static const size_t DEBUG_FILE_CHARS = 100;
/**
* read and return symbol information for the given address.
*
* NOTE: the PDB implementation is rather slow (~500 us).
*
* @param ptr_of_interest address of symbol (e.g. function, variable)
* @param sym_name optional out; holds at least DEBUG_SYMBOL_CHARS;
* receives symbol name returned via debug info.
* @param file optional out; holds at least DEBUG_FILE_CHARS;
* receives base name only (no path; see rationale in wdbg_sym) of
* source file containing the symbol.
* @param line optional out; receives source file line number of symbol.
*
* note: all of the output parameters are optional; we pass back as much
* information as is available and desired.
* @return Status; INFO::OK iff any information was successfully
* retrieved and stored.
**/
Status debug_ResolveSymbol(void* ptr_of_interest, wchar_t* sym_name, wchar_t* file, int* line);
static const size_t DEBUG_CONTEXT_SIZE = 2048; // Win32 CONTEXT is currently 1232 bytes
/**
* @param context must point to an instance of the platform-specific type
* (e.g. CONTEXT) or CACHE_ALIGNED storage of DEBUG_CONTEXT_SIZE bytes.
**/
Status debug_CaptureContext(void* context);
/**
* write a complete stack trace (including values of local variables) into
* the specified buffer.
*
* @param buf Target buffer.
* @param maxChars Max chars of buffer (should be several thousand).
* @param context Platform-specific representation of execution state
* (e.g. Win32 CONTEXT). either specify an SEH exception's
* context record or use debug_CaptureContext to retrieve the current state.
* Rationale: intermediates such as debug_DisplayError change the
* context, so it should be captured as soon as possible.
* @param lastFuncToSkip Is used for omitting error-reporting functions like
* debug_OnAssertionFailure from the stack trace. It is either 0 (skip nothing) or
* a substring of a function's name (this allows platform-independent
* matching of stdcall-decorated names).
* Rationale: this is safer than specifying a fixed number of frames,
* which can be incorrect due to inlining.
* @return Status; ERR::REENTERED if reentered via recursion or
* multithreading (not allowed since static data is used).
**/
Status debug_DumpStack(wchar_t* buf, size_t maxChars, void* context, const wchar_t* lastFuncToSkip);
//-----------------------------------------------------------------------------
// helper functions (used by implementation)
//-----------------------------------------------------------------------------
/**
* [system-dependent] write a string to the debug channel.
* this can be quite slow (~1 ms)! On Windows, it uses OutputDebugString
* (entails context switch), otherwise stdout+fflush (waits for IO).
**/
void debug_puts(const char* text);
/**
* return the caller of a certain function on the call stack.
*
* this function is useful for recording (partial) stack traces for
* memory allocation tracking, etc.
*
* @param context, lastFuncToSkip - see debug_DumpStack
* @return address of the caller
**/
void* debug_GetCaller(void* context, const wchar_t* lastFuncToSkip);
/**
* check if a pointer appears to be totally invalid.
*
* this check is not authoritative (the pointer may be "valid" but incorrect)
* but can be used to filter out obviously wrong values in a portable manner.
*
* @param p pointer
* @return 1 if totally bogus, otherwise 0.
**/
int debug_IsPointerBogus(const void* p);
/// does the given pointer appear to point to code?
bool debug_IsCodePointer(void* p);
/// does the given pointer appear to point to the stack?
bool debug_IsStackPointer(void* p);
/**
* inform the debugger of the current thread's name.
*
* (threads are easier to keep apart when they are identified by
* name rather than TID.)
**/
void debug_SetThreadName(const char* name);
/**
* build a string describing the given error.
*
* this is a helper function used by debug_DumpStack and is made available
* so that the self-test doesn't have to display the error dialog.
*
* @param description: general description of the problem.
* @param fn_only filename (no path) of source file that triggered the error.
* @param line, func: exact position of the error.
* @param context, lastFuncToSkip: see debug_DumpStack.
**/
const wchar_t* debug_BuildErrorMessage(const wchar_t* description, const wchar_t* fn_only, int line, const char* func, void* context, const wchar_t* lastFuncToSkip);
#endif // #ifndef INCLUDED_DEBUG