/*******************************************************************************
*
* HEADER: memalloc
*
********************************************************************************
*
* DESCRIPTION: Memory allocation and tracing routines
*
********************************************************************************
*
* Copyright (c) 2002-2024 Marcus Holland-Moritz. All rights reserved.
* This program is free software; you can redistribute it and/or modify
* it under the same terms as Perl itself.
*
*******************************************************************************/

/**
 *  \file memalloc.h
 *  \brief Memory allocation and tracing routines
 *
 *  The functions in this file provide an interface to
 *  the standard malloc / free functions, but in addition
 *  you can selectively enable tracing of your memory
 *  allocation. This may be useful to detect memory leaks
 *  or usage of already freed memory blocks.
 *
 *  A Perl script is supplied to analyze the output of
 *  the memory tracing routines.
 *
 *  To enable the tracing capability, the library must be
 *  compiled with the #DEBUG_MEMALLOC preprocessor flag. Then,
 *  you can selectively enable the tracing for each file or
 *  project by using the SetDebugMemAlloc() routine.
 *
 *  The following code shows an example:
 *
 *  \include Alloc.c
 *
 *  Then, a file like this will be written to stdout:
 *
 *  \verbinclude mem_debug.dat
 *
 *  This output is easy to understand. It tells you that
 *
 *  -# in file \c Alloc.c, line 9, there were 16 bytes allocated at address 0x400031C0,
 *  -# in file \c Alloc.c, line 10, address 0x400031C0 was verified,
 *  -# in file \c Alloc.c, line 11, the memory block at address 0x400031C0 was freed,
 *  -# in file \c Alloc.c, line 12, address 0x400031C0 was verified again.
 *
 *  These files usually become very large if you work a lot with
 *  dynamic memory allocation. So it would be rather hard to step
 *  through that file on your own. For that reason, there's a Perl
 *  script called \c check_alloc.pl that will take \c mem_debug.dat
 *  as input and print all errors discovered and summary statistics:
 *
 *  \verbinclude mem_debug.out
 *
 *  As you can see, the last call to AssertValidPtr() caused an error
 *  because the block that was checked has already been freed. The
 *  other output is only useful if you have lots of dynamic memory
 *  allocation, for example:
 *
 *  \verbinclude memdb_large.out
 *
 *  This will tell you that a total of 32404 memory blocks have been
 *  successfully allocated and freed, a maximum of 13305 memory blocks
 *  were in use simultanously, the peak memory usage was 183675 bytes,
 *  the smallest and largest block that were allocated were 2 and 29
 *  bytes in size, respectively, and there were no memory leaks detected.
 *
 */
#ifndef _UTIL_MEMALLOC_H
#define _UTIL_MEMALLOC_H

#include <stdio.h>

#ifdef UTIL_HAVE_CONFIG_H
# include "config.h"
#endif

#if !(defined(UTIL_MALLOC) && defined(UTIL_CALLOC) && defined(UTIL_REALLOC) && defined(UTIL_FREE))
# include <stdlib.h>
# define UTIL_MALLOC(size)          malloc(size)
# define UTIL_CALLOC(count, size)   calloc(count, size)
# define UTIL_REALLOC(ptr, size)    realloc(ptr, size)
# define UTIL_FREE(ptr)             free(ptr)
#endif

#define DB_MEMALLOC_TRACE    0x00000001
#define DB_MEMALLOC_ASSERT   0x00000002

#ifdef DEBUG_MEMALLOC
void *_memAlloc( size_t size, const char *file, int line );
void *_memCAlloc( size_t nobj, size_t size, const char *file, int line );
void *_memReAlloc( void *p, size_t size, const char *file, int line );
void  _memFree( void *p, const char *file, int line );
void  _assertValidPtr( const void *p, const char *file, int line );
void  _assertValidBlock( const void *p, size_t size, const char *file, int line );
int    SetDebugMemAlloc( void (*dbfunc)(const char *, ...), unsigned long dbflags );
#else
void *_memAlloc( size_t size );
void *_memCAlloc( size_t nobj, size_t size );
void *_memReAlloc( void *p, size_t size );
void  _memFree( void *p );
#endif

/***************************************************************/
/*                       DOCUMENTATION                         */
/***************************************************************/

#ifdef DOXYGEN

/**
 *  Make memory allocation routines abort when out of memory
 *
 *  Set this preprocessor flag if you want the Alloc(), CAlloc()
 *  and ReAlloc() functions as well as the fast macros AllocF(),
 *  CAllocF() and ReAllocF() to abort if the system runs out of
 *  memory.
 */

#define ABORT_IF_NO_MEM

/**
 *  Compile with debugging support
 */

#define DEBUG_MEMALLOC

/**
 *  Compile with tracing / leak detection support
 *
 *  This may slow down memory allocation if lots of blocks
 *  are simultaneously allocted. It will also increase the
 *  memory requirements of your application.
 *
 *  On the plus side, you get run-time memory allocation tracing,
 *  assertion checking, leak detection and memory statistics.
 *  You can control the amount of statistics by setting the
 *  MEMALLOC_STAT_LEVEL environment variable to a value between
 *  0 and 3, with increasing amount of output.
 *
 *  If an assertion fails, the program will usually abort.
 *  You can choose not to abort the program by setting
 *  MEMALLOC_SOFT_ASSERT to a non-zero value in your
 *  environment.
 *
 *  If you want the memory allocator to keep information about
 *  freed blocks, set MEMALLOC_CHECK_FREED to a non-zero value.
 *  This can give more detailed trace output at the cost of
 *  slower execution.
 *
 *  If you like to see hex dumps of non-freed memory blocks,
 *  you can set MEMALLOC_SHOW_DUMPS to a non-zero value.
 *
 *  Only works if DEBUG_MEMALLOC is also defined.
 */

#define TRACE_MEMALLOC

/**
 *  Build with memory allocator that automatically purges
 *  allocated / freed memory blocks.
 *
 *  Only works if DEBUG_MEMALLOC is also defined.
 */

#define AUTOPURGE_MEMALLOC

/**
 *  Build without support for the Alloc(), CAlloc() and
 *  ReAlloc() functions. Memory management is completely
 *  carried out through the use of the fast allocation
 *  macros AllocF(), CAllocF() and ReAllocF().
 */

#define NO_SLOW_MEMALLOC_CALLS

/**
 *  Allocate a memory block
 *
 *  Allocates a memory block of \a size bytes. If the files
 *  were compiled with the #ABORT_IF_NO_MEM preprocessor flag,
 *  the function aborts if no memory can be allocated.
 *
 *  \param size           Size of the memory block in bytes.
 *
 *  \return A pointer to the allocated memory block, or NULL
 *          if memory couldn't be allocated.
 */

void *Alloc( size_t size );

/**
 *  Allocate a memory block and initialize to zero
 *
 *  Allocates a memory block to hold \a nobj times
 *  \a size bytes. If the files were compiled with the
 *  #ABORT_IF_NO_MEM preprocessor flag, the function
 *  aborts if no memory can be allocated.
 *
 *  \param nobj           Number of objects.
 *
 *  \param size           Size of one object in bytes.
 *
 *  \return A pointer to the allocated memory block, or NULL
 *          if memory couldn't be allocated.
 */

void *CAlloc( size_t nobj, size_t size );

/**
 *  Reallocate a memory block
 *
 *  Reallocates a memory block of \a size bytes. If the files
 *  were compiled with the #ABORT_IF_NO_MEM preprocessor flag,
 *  the function aborts if no memory can be allocated.
 *
 *  \param ptr            Pointer to an allocated memory block.
 *
 *  \param size           Size of new memory block in bytes.
 *
 *  \return A pointer to the reallocated memory block, or NULL
 *          if memory couldn't be reallocated.
 */

void *ReAlloc( void *ptr, size_t size );

/**
 *  Fast Alloc Macro
 *
 *  Allocates a memory block of \a size bytes. If the files
 *  were compiled with the #ABORT_IF_NO_MEM preprocessor flag,
 *  the function aborts if no memory can be allocated.
 *
 *  \param cast           Pointer cast.
 *
 *  \param ptr            Pointer to memory block.
 *
 *  \param size           Size of the memory block in bytes.
 */

#define AllocF( cast, ptr, size )

/**
 *  Fast CAlloc Macro
 *
 *  Allocates a memory block to hold \a nobj times
 *  \a size bytes. If the files were compiled with the
 *  #ABORT_IF_NO_MEM preprocessor flag, the function
 *  aborts if no memory can be allocated.
 *
 *  \param cast           Pointer cast.
 *
 *  \param ptr            Pointer to memory block.
 *
 *  \param nobj           Number of objects.
 *
 *  \param size           Size of one object in bytes.
 */

#define CAllocF( cast, ptr, nobj, size )

/**
 *  Fast ReAlloc Macro
 *
 *  Reallocates a memory block of \a size bytes. If the files
 *  were compiled with the #ABORT_IF_NO_MEM preprocessor flag,
 *  the function aborts if no memory can be allocated.
 *
 *  \param cast           Pointer cast.
 *
 *  \param ptr            Pointer to memory block.
 *
 *  \param size           Size of new memory block in bytes.
 */

#define ReAllocF( cast, ptr, size )

/**
 *  Free a memory block
 *
 *  Frees a memory block that has been previously allocated
 *  using the Alloc() function.
 *
 *  \param ptr            Pointer to a previously allocated
 *                        memory block.
 */

void Free( void *ptr );

/**
 *  Trace pointer access.
 *
 *  This may prove useful for checking if \a ptr points to
 *  an existing, previously allocated, not yet freed memory
 *  block.
 *
 *  \param ptr            Pointer to be traced.
 */

void AssertValidPtr( void *ptr );

/**
 *  Trace memory block access.
 *
 *  Allows checking if a certain memory block lies within
 *  a previously allocated memory block.
 *
 *  \param ptr            Pointer to memory block.
 *
 *  \param size           Size of memory block.
 */

void AssertValidBlock( void *ptr, size_t size );

/**
 *  Configure debugging support.
 *
 *  \param dbfunc         Pointer to a printf() like function
 *                        for writing the debug output.
 *
 *  \param dbflags        Binary ORed debugging flags. Currently,
 *                        you can request memory allocation tracing
 *                        with \c DB_MEMALLOC_TRACE and pointer
 *                        assertions with \c DB_MEMALLOC_ASSERT.
 */

int SetDebugMemAlloc( void (*dbfunc)(char *, ...), unsigned long dbflags );

#else /* !DOXYGEN */

/***************************************************************/
/*                    END OF DOCUMENTATION                     */
/***************************************************************/

#ifdef ABORT_IF_NO_MEM
# define abortMEMALLOC( call, size, expr )                                     \
        do {                                                                   \
          size_t tmp_size__ = (size);                                          \
          if( (expr) == NULL && tmp_size__ > 0 ) {                             \
            fprintf(stderr, "%s(%u): out of memory!\n",                        \
                    call, (unsigned) tmp_size__);                              \
            abort();                                                           \
          }                                                                    \
        } while(0)
#else
# define abortMEMALLOC( call, size, expr )  do { (void) (expr); } while(0)
#endif

#ifdef DEBUG_MEMALLOC

# ifndef NO_SLOW_MEMALLOC_CALLS
#  define ReAlloc( ptr, size )           _memReAlloc( ptr, size, __FILE__, __LINE__ )
#  define CAlloc( nobj, size )           _memCAlloc( nobj, size, __FILE__, __LINE__ )
#  define Alloc( size )                  _memAlloc( size, __FILE__, __LINE__ )
# endif

# define Free( ptr )                     _memFree( ptr, __FILE__, __LINE__ )
# define AssertValidPtr( ptr )           _assertValidPtr( ptr, __FILE__, __LINE__ )
# define AssertValidBlock( ptr, size )   _assertValidBlock( ptr, size, __FILE__, __LINE__ )

# define ReAllocF( cast, ptr, size ) \
          do { ptr = (cast) _memReAlloc( ptr, size, __FILE__, __LINE__ ); } while(0)
# define CAllocF( cast, ptr, nobj, size ) \
          do { ptr = (cast) _memCAlloc( nobj, size, __FILE__, __LINE__ ); } while(0)
# define AllocF( cast, ptr, size ) \
          do { ptr = (cast) _memAlloc( size, __FILE__, __LINE__ ); } while(0)

#else /* !DEBUG_MEMALLOC */

# ifndef NO_SLOW_MEMALLOC_CALLS
#  ifdef ABORT_IF_NO_MEM
#   define ReAlloc( ptr, size )          _memReAlloc( ptr, size )
#   define CAlloc( nobj, size )          _memCAlloc( nobj, size )
#   define Alloc( size )                 _memAlloc( size )
#  else
#   define ReAlloc( ptr, size )          UTIL_REALLOC( ptr, size )
#   define CAlloc( nobj, size )          UTIL_CALLOC( nobj, size )
#   define Alloc( size )                 UTIL_MALLOC( size )
#  endif
# endif

# define Free( ptr )                     do { if( ptr ) UTIL_FREE( ptr ); } while(0)
# define AssertValidPtr( ptr )           (void) 0
# define AssertValidBlock( ptr, size )   (void) 0
# define SetDebugMemAlloc( func, flags ) 0

# define ReAllocF( cast, ptr, size ) \
           abortMEMALLOC( "ReAllocF", size, ptr = (cast) UTIL_REALLOC( ptr, size ) )
# define CAllocF( cast, ptr, nobj, size ) \
           abortMEMALLOC( "CAllocF", nobj*size, ptr = (cast) UTIL_CALLOC( nobj, size ) )
# define AllocF( cast, ptr, size ) \
           abortMEMALLOC( "AllocF", size, ptr = (cast) UTIL_MALLOC( size ) )

#endif /* DEBUG_MEMALLOC */

#endif /* DOXYGEN */

#endif