|
|
/**************************************************************************\
* * Copyright (c) 1999 Microsoft Corporation * * Module Name: * * GDI+ memory allocation functions * * Abstract: * * This module provides GpMalloc, GpRealloc and GpFree. * * Notes: * * Office provides their own versions of these functions. * * Created: * * 07/08/1999 agodfrey * \**************************************************************************/
#include "precomp.hpp"
namespace GpRuntime { HANDLE GpMemHeap = NULL; };
#if GPMEM_ALLOC_CHK
#if GPMEM_FAULT_INJECTION
#include <time.h>
#include <stdlib.h>
#endif
// Get the definition of Globals::CaptureStackBackTraceFunction
#include "..\common\common.hpp"
// Size of the start and end memory guards (DWORDS)
// Probably should be QWORD aligned (even number).
const unsigned int GPMEM_GUARD_START = 0x10; const unsigned int GPMEM_GUARD_END = 0x10; const unsigned int GPMEM_GS = GPMEM_GUARD_START*sizeof(DWORD); const unsigned int GPMEM_GE = GPMEM_GUARD_END*sizeof(DWORD);
const unsigned char GPMEM_FILL_ALLOC = 0xbf; const unsigned char GPMEM_FILL_GS = 0xac; const unsigned char GPMEM_FILL_GE = 0xfe; const unsigned char GPMEM_FILL_FREE = 0x73;
#define GPMEM_ALLOC_TRACKING 1
#define GPMEM_ALLOC_FILL 1
enum AllocTrackHeaderFlags { MemoryAllocated = 0x00000001, MemoryFreed = 0x00000002, // useful in catching double frees
APIAllocation = 0x00000004 }; // Head of every tracked allocation.
// From <ntrtl.h> -
// #define MAX_STACK_DEPTH 32
// Therefore we define our stack trace size to be 32 entries.
// Number of Stack Frames stash away on every allocation.
// Bear in mind that every allocation will have 4xGPMEM_STACKSIZE bytes
// additional overhead. (8xGPMEM_STACKSIZE on ptr64 machines)
#define GPMEM_STACKSIZE 32
struct AllocTrackHeader { struct AllocTrackHeader *flink; struct AllocTrackHeader *blink; DWORD size; PVOID caller_address[GPMEM_STACKSIZE]; DWORD flags; #if GPMEM_ALLOC_CHK_LIST
char *callerFileName; INT callerLineNumber; #endif
DWORD tag; DWORD padding; // padding to keep 8 byte alignment
};
#define GPMEM_OVERHEAD (GPMEM_GS + GPMEM_GE + sizeof(AllocTrackHeader))
// Head of double linked list of tracked memory allocations.
AllocTrackHeader *gpmemAllocList=NULL;
// An allocation fails if rand() < gpmemDefFailRate (gpmemInitFailRate for
// gdiplus initialization code.
// set to RAND_MAX/2 if you want 50% failure rate, 0 if you want no failures.
//
// The system starts off failing allocations at a rate specified by
// gpmemInitFailRate. Once GpDoneInitializeAllocFailureMode() is called,
// allocations are failed at the rate specified by gpmemDefFailRate().
// This is so that dll initialization code can have a different fail rate
// to regular code.
int gpmemInitFailRate = 0; int gpmemDefFailRate = 0;
// This would give a failure rate of 25%
// int gpmemDefFailRate = (RAND_MAX/4)
BOOL gpmemDoneInitialization = FALSE;
// Some statistics
struct AllocTrackStats { // Totals over the entire run
long CumulativeAllocations; // The number of calls to GpMalloc or GpRealloc
long CumulativeMemorySize; // Cumulative total of allocated memory
long CumulativeReallocs; // The number of calls to GpRealloc
long ForcedFailures; long AllocationFailures;
// Current values
long OutstandingAllocations; // The number of allocation requests
long OutstandingMemorySize; // The amount of memory currently allocated
// Maxima of the 'Outstanding' values
long MaxAllocations; // The maximum of OutstandingAllocations
long MaxMemorySize; // The maximum of OutstandingMemorySize
void Allocated(long size) { size -= GPMEM_OVERHEAD;
CumulativeMemorySize += size; OutstandingMemorySize += size; if (OutstandingMemorySize > MaxMemorySize) { MaxMemorySize = OutstandingMemorySize; } CumulativeAllocations++; OutstandingAllocations++; if (OutstandingAllocations > MaxAllocations) { MaxAllocations = OutstandingAllocations; } }
void Freed(long size) { size -= GPMEM_OVERHEAD;
OutstandingMemorySize -= size; OutstandingAllocations--; } };
AllocTrackStats gpmemAllocTotal = {0};
// Hash Table for tracking memory allocations sorted by callsite.
// This table stores some total memory usage statistics for each
// callsite.
// Turn this on by setting GPMEM_DEBUG_SORT 1
#define GPMEM_DEBUG_SORT 0
#if GPMEM_DEBUG_SORT
struct HashMem { long callsite; long size; long count; };
// It is very important that this hash size be larger than the number of
// possible callsites for GpMalloc.
//
// Set HASHSIZE to some big prime number.
#define HASHSIZE 1069
HashMem HashTable[HASHSIZE];
// Hashing algorithm.
long Hash(long cs) { long tmp = cs % HASHSIZE; long tmploop = tmp; while( (HashTable[tmp].callsite != 0) && (HashTable[tmp].callsite != cs) ) { tmp++; if(tmp>=HASHSIZE) tmp=0; if(tmp==tmploop) return -1; } return tmp; } #endif
#endif
/**************************************************************************\
* * Function Description: * * Do we fail this memory allocation? * * Arguments: [NONE] * Return Value: [NONE] * * History: * * 09/20/1999 asecchia * Created it. * \**************************************************************************/
#if GPMEM_ALLOC_CHK
BOOL GpFailMemoryAllocation() { #if GPMEM_FAULT_INJECTION
int rndnum = rand(); if(gpmemDoneInitialization) { if(rndnum<gpmemDefFailRate) { return TRUE; } } else { if(rndnum<gpmemInitFailRate) { return TRUE; } } #endif
return FALSE; } #endif
/**************************************************************************\
* * Function Description: * * Initializes the random seed. * * Arguments: [NONE] * Return Value: [NONE] * * History: * * 09/20/1999 asecchia * Created it. * \**************************************************************************/
void GpInitializeAllocFailures() { #if GPMEM_ALLOC_CHK
#if GPMEM_FAULT_INJECTION
srand((unsigned)time(NULL)); #endif
#endif
}
/**************************************************************************\
* * Function Description: * * Sets the flag indicating that we're done initialization code and * we're now into regular code. The memory failure mode changes based * on the value of this flag. * * Arguments: [NONE] * Return Value: [NONE] * * History: * * 09/20/1999 asecchia * Created it. * \**************************************************************************/
void GpDoneInitializeAllocFailureMode() { #if GPMEM_ALLOC_CHK
gpmemDoneInitialization=TRUE; #endif
}
void GpStartInitializeAllocFailureMode() { #if GPMEM_ALLOC_CHK
gpmemDoneInitialization=FALSE; #endif
}
#if GPMEM_ALLOC_CHK_LIST
char *skipGdiPlus(char *s) { // Quick hack to return pointer just beyond 'gdiplus'
INT i = 0; while ( s[i] != 0 && ( s[i] != 'g' && s[i] != 'G' || CompareStringA( LOCALE_SYSTEM_DEFAULT, NORM_IGNORECASE, s+i, 7, "gdiplus", 7) != CSTR_EQUAL)) { i++; } if ( CompareStringA(LOCALE_SYSTEM_DEFAULT, NORM_IGNORECASE, s+i, 7, "gdiplus", 7) == CSTR_EQUAL && s[i+7] != 0) { return s + i + 8; // Skip over 'gdiplus/'
} else { return s; // Didn't find gdiplus so return the whole string
} } #endif
/**************************************************************************\
* * Function Description: * * Asserts that there are no memory leaks. Called just before process * termination, the list of allocated memory blocks should be NULL indicating * that all allocated memory was properly disposed. Any memory that relies on * process termination to clean up is leaked and provision should be made * for appropriate cleanup. * * Notes: * * Designed only to be called during shutdown, because it doesn't acquire * GpMallocTrackingCriticalSection. * * This is because, during shutdown, the critsec has not necessarily been * initialized. It's safe during shutdown because shutdown itself is * expected to be single-threaded. * * Todo after we ship: * * NTRAID#NTBUG9-411495-2001/06/06-agodfrey * The refcount is legacy anyway, and the "single-threaded shutdown" thing * may need to be changed. We should initialize all our global critsecs * in DllMain, so that we can rely on them during startup/shutdown. * * Once we've done that, this function can have the "shutdown only" * restriction removed - by making it once again acquire * GpMallocTrackingCriticalSection. * * Arguments: [NONE] * Return Value: [NONE] * * History: * * 09/19/1999 asecchia * Created it. * \**************************************************************************/
void GpAssertShutdownNoMemoryLeaks() { #if GPMEM_ALLOC_CHK
#if GPMEM_ALLOC_CHK_LIST
// Report up to 100 leaked headers
if (gpmemAllocList) { INT i=0; INT j=0; AllocTrackHeader *header = gpmemAllocList; while (header && j < 100) { if (i % 20 == 0) // Title every so often
{ WARNING(("Address- --Size-- API TAG -Caller- -Line- File")); }
// Drop everything up to 'gdiplus' off the filename string
char str[200]; char *tagStr;
tagStr = (char *) &header->tag;
lstrcpynA(str, skipGdiPlus(header->callerFileName), 200);
WARNING(( "%p %8d %-3.3s %c%c%c%c %p %6d %s", header, header->size, header->flags & APIAllocation ? "API" : "", tagStr[3], tagStr[2], tagStr[1], tagStr[0], header->caller_address, header->callerLineNumber, str )); header = header->flink;
i++; j++; } } #endif
ASSERTMSG( gpmemAllocList==NULL, ("Memory leaks detected.\n" "List header (gdiplus!gpmemAllocList) at %p\n" "Use: dt AllocTrackHeader [address] to display the memory block headers.\n" "Use: dds [AllocTrackHeader.caller_address] to display the allocation stack.", gpmemAllocList) );
// Display the report stored in the Hash Table
#if GPMEM_DEBUG_SORT
for(int i=0; i<HASHSIZE; i++) { if(HashTable[i].callsite != 0) { WARNING(("%4d callsite %p size %8d count %8d", i, HashTable[i].callsite, HashTable[i].size, HashTable[i].count)); } } #endif
#endif
}
#if GPMEM_ALLOC_CHK_LIST
void GpTagMalloc(void * mem, GpTag tag, int bApi) { if(mem) { AllocTrackHeader *hdr = (AllocTrackHeader *)((char *) mem - sizeof(AllocTrackHeader) - GPMEM_GS); hdr->tag = (unsigned long) tag; if(bApi) hdr->flags |= APIAllocation; } } #endif
/**************************************************************************\
* * Function Description: * * Allocates a block of memory. * * Arguments: * * [IN] size - number of bytes to allocate * * Return Value: * * A pointer to the new block, or NULL on failure. * * History: * * 09/14/1999 asecchia * Added the checked build memory guard code. * 07/08/1999 agodfrey * Created it. * \**************************************************************************/
/*
Here's the structure of the memory block allocated under GPMEM_ALLOC_CHK
|+AllocTrackHeader Two DWORDS - contains | |+flink | | Pointer to the next memory allocation in the tracked allocation list | |+blink | | Pointer to the previous memory allocation in the tracked allocation link | |+Guard Area | GPMEM_GUARD_START DWORDs filled with the gpmemGuardFill string. | |+Data Area | This is the location we return to the caller. It is pre-initialized to | the repeated DWORD value in gpmemAllocFillBlock (usually 0xbaadf00d) | |+Guard Area: | GPMEM_GUARD_END DWORDs filled with gpmemGuardFill string.
*/ #if GPMEM_ALLOC_CHK_LIST
extern "C" void *GpMallocDebug(size_t size, char *fileName, INT lineNumber) #else
extern "C" void *GpMalloc(size_t size) #endif
{ // on checked builds we add headers to the allocation and hence, really
// large requests will overflow 32bits and succeed a really small
// allocation.
// Also allocations of 2-4Gb usually indicate a sign extension problem in
// the caller when the size is computed. Note that size_t is UNSIGNED.
// Instead of simply checking the high bit with an &, we check to see that
// the size is < 0x7fffffff so that the check works on IA64 too - IA64
// size_t is 64bit. Note that allocations of this size will fail in the
// heap manager anyway. The caller should be using VirtualAlloc for stuff
// that's really this big.
ASSERT(size < 0x7fffffff);
#if GPMEM_ALLOC_CHK
// If we're playing with the tracking headers, we need to be thread safe.
GpMallocTrackingCriticalSection critsecobj;
//
// Memory? _Real_ programmers don't need memory!
//
if(GpFailMemoryAllocation()) { gpmemAllocTotal.AllocationFailures++; gpmemAllocTotal.ForcedFailures++; return NULL; }
//
// Make the allocation request a multiple of a QWORD
//
if(size & (sizeof(DWORD)*2-1)) { size = (size & ~(sizeof(DWORD)*2-1)) + sizeof(DWORD)*2; }
size_t origsize = size;
//
// Allocate space for the FLink and BLink
//
size += sizeof(AllocTrackHeader);
if(GPMEM_GUARD_START) { size += GPMEM_GS; }
if(GPMEM_GUARD_END) { size += GPMEM_GE; }
void *tmpalloc = HeapAlloc(GpMemHeap, GPMEMHEAPFLAGS, size);
if(!tmpalloc) { gpmemAllocTotal.AllocationFailures++; return NULL; }
ASSERTMSG(HeapSize(GpMemHeap, GPMEMHEAPFLAGS, tmpalloc) >= size, ("GpMalloc() allocated %d, but requested %d bytes", HeapSize(GpMemHeap, GPMEMHEAPFLAGS, tmpalloc), size));
// Add the per-callsite allocation statistics
#if GPMEM_DEBUG_SORT
long hidx = Hash(calleraddr); if(hidx>=0) { HashTable[hidx].callsite = calleraddr; HashTable[hidx].size += size-GPMEM_OVERHEAD; HashTable[hidx].count ++; } else { WARNING(("Hash Table too small - increase HASHSIZE")); } #endif
gpmemAllocTotal.Allocated(size); #else
//
// This is the only piece of code that's executed if
// GPMEM_ALLOC_CHK is turned off.
//
#if PROFILE_MEMORY_USAGE
MC_LogAllocation(size); #endif
return HeapAlloc(GpMemHeap, GPMEMHEAPFLAGS, size); #endif
#if GPMEM_ALLOC_CHK
//
// Fill up the entire allocation with the value
// set in GPMEM_FILL_ALLOC
//
if(GPMEM_ALLOC_FILL) { GpMemset((unsigned char *)tmpalloc + sizeof(AllocTrackHeader) + GPMEM_GS, GPMEM_FILL_ALLOC, origsize); }
//
// Fill up the start guard area - if we have one.
//
if(GPMEM_GUARD_START) { unsigned char *p = (unsigned char *)tmpalloc+sizeof(AllocTrackHeader); GpMemset(p, GPMEM_FILL_GS, GPMEM_GS); }
//
// Fill up the end guard area - if we have one.
//
if(GPMEM_GUARD_END) { unsigned char *p = (unsigned char *)tmpalloc+size-GPMEM_GE; GpMemset(p, GPMEM_FILL_GE, GPMEM_GE); }
//
// setup the double linked-list to track all pool allocations.
//
AllocTrackHeader *hdr = (AllocTrackHeader *)tmpalloc; hdr->size = size;
// zero out any unused frame pointers.
GpMemset(hdr->caller_address, 0, sizeof(ULONG_PTR)*GPMEM_STACKSIZE); // Lets stash away the entire stack trace if we have a
// backtrace function.
if(Globals::CaptureStackBackTraceFunction) { ULONG hash; // dummy hash
USHORT frames = Globals::CaptureStackBackTraceFunction( 0, // don't skip any frames
GPMEM_STACKSIZE, // get this many frames
(PVOID*)(hdr->caller_address), // ... into here.
&hash ); } hdr->flags = MemoryAllocated; hdr->tag = 'unkn';
#if GPMEM_ALLOC_CHK_LIST
hdr->callerFileName = fileName; hdr->callerLineNumber = lineNumber; #endif
if(GPMEM_ALLOC_TRACKING) { hdr->blink = NULL; hdr->flink = gpmemAllocList; if(gpmemAllocList) { gpmemAllocList->blink = (AllocTrackHeader *)tmpalloc; } gpmemAllocList = (AllocTrackHeader *)tmpalloc; } else { GpMemset(hdr, 0, sizeof(AllocTrackHeader)); }
//
// Give them a pointer just after the guard bits.
//
return (char *)tmpalloc+sizeof(AllocTrackHeader)+GPMEM_GS; #endif
}
/**************************************************************************\
* * Function Description: * Allocates memory for APIs. Used to track the memory with a separate * identifying flag so that API allocations can be distinguished from * internal allocations. * Used on debug builds. * * Arguments: * [IN] size - size to pass to GpMalloc * * Return Value: * Returns the memory with the appropriately hacked up caller address * * History: * * 4/30/2000 asecchia * Created it. * \**************************************************************************/
#if DBG
#if GPMEM_ALLOC_CHK
#if GPMEM_ALLOC_CHK_LIST
extern "C" void * __stdcall GpMallocAPIDebug(size_t size, char *fileName, INT lineNumber) #else
extern "C" void *GpMallocAPI(size_t size) #endif
{ // If we're playing with the tracking headers, we need to be thread safe.
GpMallocTrackingCriticalSection critsecobj;
#if GPMEM_ALLOC_CHK_LIST
void *p = GpMallocDebug(size, fileName, lineNumber); #else
void *p = GpMalloc(size); #endif
if(p) { AllocTrackHeader *hdr = (AllocTrackHeader *)( (unsigned char *)p-(GPMEM_GS+sizeof(AllocTrackHeader))); hdr->flags |= APIAllocation; } return p; }
#else // !GPMEM_ALLOC_CHK
extern "C" void *GpMallocAPI(size_t size, unsigned int caddr) { return GpMalloc(size); }
#endif // !GPMEM_ALLOC_CHK
#endif // DBG
/**************************************************************************\
* * Function Description: * * Computes the original size of a memory block allocated under GPMEM_ALLOC_CHK * * Arguments: * * [IN] p - current memory block * * Return Value: * * size of the original request for a memory block (i.e. excluding guard * areas, headers, etc). The size returned is the DWORD aligned size - so it * may differ slighly from the original size requested. * * Notes: * * Returns a size of zero if called with NULL * Only compiled under GPMEM_ALLOC_CHK * * History: * * 09/14/1999 asecchia * Created it. * \**************************************************************************/
#if GPMEM_ALLOC_CHK
extern "C" size_t GpSizeBlock(void *p) { if(p) { // Find the beginning of the allocated block header.
p = (char *)p-(GPMEM_GS+sizeof(AllocTrackHeader)); ASSERT( HeapSize(GpMemHeap, GPMEMHEAPFLAGS, p) >= ((AllocTrackHeader *)p)->size ); // Compute the size of the allocated block's data area.
return (((AllocTrackHeader *)p)->size - (GPMEM_GS+GPMEM_GE+sizeof(AllocTrackHeader))); } else { return 0; } } #else
// Non-debug build, just call HeapSize
#define GpSizeBlock(p) HeapSize(GpMemHeap, GPMEMHEAPFLAGS, p)
#endif
/**************************************************************************\
* * Function Description: * * Reallocates a memory block. * * Arguments: * * [IN] memblock - current memory block * [IN] size - new allocation size * * Return Value: * * A pointer to the new block, or NULL on failure. * * Notes: * * If size is 0, frees the block. * If memblock is NULL, allocates a new block. * (If both, does nothing.) * * LocalReAlloc only grows if it can expand the current allocation * - otherwise it fails. * * History: * * 09/14/1999 asecchia * Added the checked build memory guard code. * 07/08/1999 agodfrey * Created it. * \**************************************************************************/
extern "C" void *GpRealloc(void *memblock, size_t size) { #if GPMEM_ALLOC_CHK
gpmemAllocTotal.CumulativeReallocs++; #endif
// Free.
if (!size) { if (memblock) GpFree(memblock); return NULL; } // Alloc.
if (!memblock) { return GpMalloc(size); }
// Realloc - Use GpMalloc/GpMemcpy/GpFree for debug so that the
// extra buffers line up (would require duplicating the code
// from the GpMalloc/GpFree functions otherwise
#if GPMEM_ALLOC_CHK
VOID * p = GpMalloc(size); if (p != NULL) { size_t oldSize = GpSizeBlock(memblock);
// Are we shrinking the block?
if (oldSize > size) { oldSize = size; } GpMemcpy(p, memblock, oldSize); GpFree(memblock); }
#else // !GPMEM_ALLOC_CHK
#if PROFILE_MEMORY_USAGE
MC_LogAllocation(size); #endif
VOID *p = HeapReAlloc(GpMemHeap, GPMEMHEAPFLAGS, memblock, size);
#endif // !GPMEM_ALLOC_CHK
return p; }
/**************************************************************************\
* * Function Description: * * Frees a block of memory. * * Arguments: * * [IN] memblock - block to free * * Notes: * * If memblock is NULL, does nothing. * * History: * * 09/14/1999 asecchia * Added the checked build memory guard code. * 07/08/1999 agodfrey * Created it. * \**************************************************************************/
extern "C" void GpFree(void *memblock) { // Do nothing if the pointer is NULL.
if(memblock) { #if GPMEM_ALLOC_CHK
// If we're playing with the tracking headers, we need to be thread safe.
GpMallocTrackingCriticalSection critsecobj;
memblock = (unsigned char *)memblock-(GPMEM_GS+sizeof(AllocTrackHeader));
// Let's do the header stuff.
AllocTrackHeader *hdr = (AllocTrackHeader *)memblock; DWORD size = hdr->size; gpmemAllocTotal.Freed(size);
ASSERTMSG( (hdr->flags & MemoryAllocated) && !(hdr->flags & MemoryFreed), ("GpFree() already freed memory %p (freed by GpFree())", memblock) );
hdr->flags &= ~MemoryAllocated; hdr->flags |= MemoryFreed;
ASSERTMSG( HeapSize(GpMemHeap, GPMEMHEAPFLAGS, memblock) >= hdr->size, ( "GpFree() already freed memory %p (freed somewhere else?)" " local size=%d, size=%d", memblock, HeapSize(GpMemHeap, GPMEMHEAPFLAGS, memblock), hdr->size ) );
if(GPMEM_ALLOC_TRACKING) { // Useful on checked Win2k builds because they fill guard
// area with 0xFEEEFEEE
ASSERTMSG((hdr->flink == NULL) || ((DWORD)((ULONG_PTR)(hdr->flink->blink) & 0xFFFFFFFF) != 0xFEEEFEEE), ("GpFree() updating forward link to freed page, header %p", memblock));
ASSERTMSG((hdr->blink == NULL) || ((DWORD)((ULONG_PTR)(hdr->blink->flink) & 0xFFFFFFFF) != 0xFEEEFEEE), ("GpFree() updating backward link to freed page, header %p", memblock));
if(hdr->flink) hdr->flink->blink = hdr->blink; if(hdr->blink) hdr->blink->flink = hdr->flink; if(gpmemAllocList==memblock) gpmemAllocList = hdr->flink; } else { ASSERTMSG(hdr->flink==NULL, ("GpFree() corrupt header %p", memblock)); ASSERTMSG(hdr->blink==NULL, ("GpFree() corrupt header %p", memblock)); }
int i; unsigned char *p;
// Check the start guard area
if(GPMEM_GUARD_START) { p = (unsigned char *)memblock+sizeof(AllocTrackHeader); for(i=0; i<GPMEM_GS; i++) { ASSERTMSG(*p==GPMEM_FILL_GS, ("GpFree() pre-guard area corrupt %p", memblock)); p++; } }
// Check the end guard area
if(GPMEM_GUARD_END) { p = (unsigned char *)memblock+size-GPMEM_GE; for(i=0; i<GPMEM_GE; i++) { ASSERTMSG(*p==GPMEM_FILL_GE, ("GpFree() post-guard area corrupt %p", memblock)); p++; } }
// Now lets fill the entire block with something to prevent
// use of free data.
GpMemset(memblock, GPMEM_FILL_FREE, size);
#endif
// HeapFree may fail freeing a NULL pointer on Win98.
BOOL ret = HeapFree(GpMemHeap, GPMEMHEAPFLAGS, memblock);
ASSERTMSG( ret, ( "HeapFree() failed at %p, GetLastError()=%08x", memblock, GetLastError() ) ); } }
extern "C" void * __stdcall zcalloc( void *pvOpaque, unsigned int c, unsigned int cb ) { void *pv = GpMalloc(c * cb); if (pv != NULL) GpMemset(pv, 0, c * cb); return pv; }
extern "C" void __stdcall zcfree(void *pvOpaque, void *pv) { GpFree(pv); }
|