When a repeated operation leaves more objects alive, start with two questions: did the allocation
pass through EurekaLog's memory filter, and was leak registration enabled when it happened?
EMemLeaks can name those blocks, record their allocation stacks, count them and report survivors.
It cannot decide which object in your design should have freed them.
Here memory leak means an unreleased allocation through Delphi's memory manager: GetMem,
AllocMem, TObject.Create, strings or dynamic arrays. Direct Windows allocations and handles
belong to resource-leak monitoring in EResLeaks (chapter 19), and only supported, monitored
API paths are covered. Neither feature measures the whole process footprint.
Two facts about the unit are worth having before any of its routines make sense. First, EMemLeaks
(and EResLeaks) are always included in a EurekaLog-enabled application, even when no leak
detection is configured: detection can be switched on at run time, and the detection code has to run
before anything else does. The unit's page turns that into a rule for the project's uses clause -
EMemLeaks has to be the first unit to be initialized, with no live allocations before it - and lists
the units it tolerates ahead of itself: a memory-manager implementation, or a unit whose initialization
allocates nothing or releases everything it allocated. A project option lets you add your own units to
that list; statically linked DLLs are the trap, because they initialize before the executable does.
Second, leak detection runs during shutdown. A survivor is still allocated at the detector's
checkpoint; later RTL finalization can still release it, and incorrect finalization ordering can
also cause failures (chapter 22, section 22.5). The filter's other work - checking a block when the
memory manager is called for it, and an explicit CheckHeap - happens during execution instead.
The leaks found at exit go into a single
report, and that report differs from an ordinary one in two documented ways: it carries no CPU and
Assembler sections, and no standard event handlers are called while it is produced.
The unit is also usable without EurekaLog: the documentation notes that you can leave EurekaLog disabled for the project and still enable leaks detection. The feature is its own mechanism, installed as a memory filter around the memory manager - and that filter's state is what decides what every other routine in this chapter can tell you. The unit's page asks for one thing in return: keep the extended memory manager option enabled even when every other memory option is off, because the same filter is what lets EurekaLog associate an exception object with its information without hooks, and exception tracing is built on that.
What follows is what the unit offers from code: the state queries, the counters, the allocation
wrappers, the naming and grouping calls, the routines that mark a leak as known, and the error kinds the
checks report. IsMemLeaksInstalled, IsMemLeaksEnabled, CheckHeap and DumpAllocationsToFile are
also described in chapter 6, section 6.6.
12.1 Is the filter there, and is registration on?
Both questions have their routine in this unit: the installed question is a property of the running build, while the enabled one is global state you can change.
IsMemLeaksInstalled
function IsMemLeaksInstalled: Boolean; // EMemLeaks
True when EurekaLog's memory filter is installed - in which case the filter is used and its overhead is present; False when this filter is absent and adds no memory-filter overhead. For your own bookkeeping, this value is initialized on startup and never changes. Decide once whether the memory numbers below mean anything; do not poll for a later change that cannot happen.
IsMemLeaksEnabled
function IsMemLeaksEnabled: Boolean; // EMemLeaks
True when leak registration is on. Two conditions on the page are easy to be surprised by: the routine always returns False if EurekaLog is not installed, and - the page's own tip - it also returns False when called from inside EurekaLog's own code.
What the flag buys is stated plainly in both directions. With registration enabled, EurekaLog collects the call stack of every allocated memory block and stores additional information with it - name, grouping and the rest. With registration disabled it collects neither, and "overhead will be significantly reduced". The documented use is to switch registration off around a stretch of code that allocates heavily and is known not to leak.
This is global state, changed by the unit's routines in section 12.2. SetMemLeaksRegistering
is another setter, but no public signature is specified for it.
12.2 Turning registration off and on
DisableMemLeaksRegistering and EnableMemLeaksRegistering
procedure DisableMemLeaksRegistering; // EMemLeaks
procedure EnableMemLeaksRegistering; // EMemLeaks
The pair switches leak registration off and back on, and the state they set is exactly the one
IsMemLeaksEnabled reports. Nothing more happens: disabling stops the collection of call stacks and of
the extra information, enabling resumes it.
Three warnings come with them.
- They are not recursive. Two calls to
DisableMemLeaksRegisteringfollowed by one call toEnableMemLeaksRegisteringleave registration enabled. Preserve the incoming state if a controlled scope can be nested. A saved Boolean does not make this global switch thread-local or synchronize other callers; do not use it around one worker while unrelated workers allocate. - Disabling does not stop the report about a leak that is already registered.
ActivateMemoryLeakCheckcontrols leak reporting separately; no public signature is specified for that routine. - Disabling at run time does not remove the filter. The vendor's guidance for production builds notes that disabling checks at run time "would not remove EurekaLog's memory filter shim", and that it is better to have memory checks disabled from the start, in the project settings. What you save is the per-allocation bookkeeping, not the shim.
If what you want is to silence one known leak rather than all of them, use
EurekaRegisterExpectedMemoryLeak (section 12.6).
For a bounded, single-threaded diagnostic task, the following complete helper preserves an outer
scope's disabled state. Put it in a unit whose implementation uses EMemLeaks. AWork must be a
plain, non-nested procedure, must not change registration itself, and must not hand work to another
thread. Do not use this to conceal allocations whose lifetime you have not established.
type
TAllocationWork = procedure;
procedure RunWithoutLeakRegistration(const AWork: TAllocationWork);
var
WasEnabled: Boolean;
begin
WasEnabled := IsMemLeaksEnabled;
if WasEnabled then
DisableMemLeaksRegistering;
try
AWork;
finally
if WasEnabled then
EnableMemLeaksRegistering;
end;
end;
This prevents new registration in the scope; it neither frees its allocations nor suppresses leaks registered earlier. Prefer registering one identified expected block when that is the actual task.
12.3 Counters: how many blocks, how many bytes
Five functions, all declared by EMemLeaks, all returning Int64, all reading the filter's own
accounting rather than the operating system's view of the process. The pages present each one as the
non-deprecated counterpart of an RTL counter of a similar name - AllocMemCount for the two count
functions, AllocMemSize for the three size functions - and say that they always return actual
information where those do not.
AllocMemCountEx
function AllocMemCountEx: Int64; // EMemLeaks
The number of memory blocks allocated through EurekaLog's memory filter that are currently alive: incremented on allocation, decremented on release. It returns 0 when the filter is not installed, when no allocations were made, or when all of them have been released. Read it at a defined workload boundary, after the work and its intended cleanup. Remaining blocks may be valid caches or globals; the count alone does not identify leaks.
AllocMemCountMax
function AllocMemCountMax: Int64; // EMemLeaks
The maximum the counter above ever reached - the high-water mark of the block count. It returns 0
if the filter is not installed or no allocations were made, and its value is "always equals or greater
than AllocMemCountEx". This is the counter for peak usage questions.
AllocMemSizeEx
function AllocMemSizeEx: Int64; // EMemLeaks
Total size in bytes of the currently live blocks, as requested by your code. The page is explicit
that this one mimics the RTL's AllocMemSize and reports the sum of requested sizes without the
additional overhead imposed by EurekaLog's filter. Use it when you want the number your own
allocations add up to; its size is the total, and the count above is the number of blocks.
AllocMemSizeEx2
function AllocMemSizeEx2: Int64; // EMemLeaks
The same total including the overhead of EurekaLog's memory and leak checks. The page states the
contrast as a note on itself: this one does not mimic AllocMemSize.
AllocMemSizeMax
function AllocMemSizeMax: Int64; // EMemLeaks
The maximum that AllocMemSizeEx reached - the peak of requested bytes. Returns 0 if the filter is
not installed or no allocations were made, and is always equal to or greater than AllocMemSizeEx.
Together with AllocMemCountMax it answers "what was the worst moment of this process".
Two things all five share. They count only blocks that went through the filter, so they are blind to any
allocation that bypasses it - EMemLeaks implements no memory manager of its own, only a filter in
front of the one already installed, and IsMemLeaksInstalled is the precondition, fixed at startup.
And a rising AllocMemCountEx tells you blocks are alive, not whether they were supposed to be.
For growth investigations, compare the same boundary in successive runs: warm up the application, perform a fixed number of operations, close their documents or sessions, and let application-owned cleanup finish. Record requested bytes, bytes including filter overhead, and live-block count. High-water marks will not fall after cleanup; use current counters to ask whether memory came back. Concurrent allocation also means separately read counters are not an atomic snapshot.
DumpAllocationsToFile (chapter 6, section 6.6) adds allocation stacks and short memory dumps, not a
list of proven leaks. It includes only blocks allocated through the filter with registration enabled.
Use it only in a disposable local diagnostic run, with a fully qualified path in an existing,
writable folder; the file is recreated. It blocks memory operations and can leave the heap invalid:
the API recommends terminating immediately afterwards. To compare one operation with a hundred,
use two fresh runs, each ending with one dump, rather than continuing work after the first dump.
Treat the file as sensitive because the short dumps can contain application data.
12.4 Allocating through the filter yourself
These are the routines the memory filter uses, and the pages say the same thing about all of them:
do not call them manually - they run automatically when the corresponding RTL routine is called.
One of them, EurekaTryGetMem, is the documented exception.
EurekaGetMem, EurekaAllocMem, EurekaReallocMem, EurekaFreeMem
function EurekaGetMem(Size: PtrInt): Pointer; // EMemLeaks
function EurekaAllocMem(Size: TIntForRealloc): Pointer; // EMemLeaks
function EurekaReallocMem(P: Pointer; Size: PtrInt): Pointer; // EMemLeaks
function EurekaFreeMem(P: Pointer): Integer; // EMemLeaks
EurekaGetMem allocates a block of Size bytes on the heap and returns its address; the bytes are
not zeroed. It stands behind GetMem. EurekaAllocMem does the same and zero-initializes every
byte; it stands behind AllocMem, and its own note is the useful comparison - if the memory does not
need to be zeroed, EurekaGetMem skips that step.
EurekaReallocMem resizes a block obtained from EurekaGetMem, EurekaTryGetMem or EurekaAllocMem.
Size may be larger or smaller than the original; the returned pointer may or may not equal P, the
old content is preserved, and memory added by growing is not zero-initialized. If the block cannot
be expanded in place, the routine frees it and copies the contents to the new block.
EurekaFreeMem releases a block obtained from one of those three allocation calls. It is declared as a
function returning Integer, and the page states that its return value is always 0.
The warnings that come with the allocation side are the same on all three allocating pages, and they matter more than the signatures:
- Allocation failure is treated as fatal. EurekaLog calls the
MemLeaksErrorevent handler for the problem and terminates the application; the pages add that these functions "usually never return nil nor raise exceptions", and point at the global variableMemLeaksErrorsToIgnorefor error kinds you want tolerable. Their parameter text still says anleOutOfMemoryerror is raised when there is not enough memory: the two statements describe the same failure from two sides, and the practical reading is the one the Important note gives - unless you use theTryvariant, do not expect to continue after a failed allocation. - Do not call them directly - with one exception.
EurekaTryGetMemis the documented way to allocate through the filter and keep the failure.
function EurekaTryGetMem(Size: PtrUInt): Pointer; // EMemLeaks
EurekaTryGetMem allocates Size bytes through the filter and returns the pointer - the memory is not
zero-initialized - or nil when a memory error occurs. Unlike EurekaGetMem, it can be called
manually. Use it when the caller needs to receive an allocation failure rather than have the
filter handle it.
MemLeaksError is the global handler variable, of type TMemLeaksError, and
MemLeaksErrorsToIgnore is the set of error kinds that are tolerated. No complete public
declarations are specified for these extension points.
12.5 Naming blocks, and grouping children with their parents
A leak report is a list of blocks identified by call stack, size, type and a short memory dump. In a loop - reading a document and creating one entry per chapter, say - every block looks the same. The naming calls exist for exactly that, and they feed both the leak report and the allocations dump.
MemLeaksName, MemLeaksNameDynArray, MemLeaksNameStr
procedure MemLeaksName(const ABlock: Pointer; const AName: String); // EMemLeaks
procedure MemLeaksNameDynArray(const ABlock: Pointer; const AName: String); // EMemLeaks
procedure MemLeaksNameStr(const ABlock: Pointer; const AName: String); // EMemLeaks
MemLeaksName assigns a custom name to a memory block allocated through the filter while leak
registration is enabled; the name is displayed with the block in leak reports, "which should allow you
to identify memory block". The page's tip is a limit to keep in mind: names have a limited length -
they are stored in the block's header in a TCustomName field, which the type page gives as an array
of 60 AnsiChar, and only owners of the Enterprise version with sources can enlarge that field and
recompile.
The important rule is about the pointer: pass a pointer to the beginning of the memory block. The
page states the exception - dynamic arrays and strings do not point to the beginning of their allocation
(their headers sit at negative offsets) - and gives them their own variants. MemLeaksNameDynArray
takes a dynamic array cast to pointer, MemLeaksNameStr a string cast to pointer.
MemLeaksGetName
function MemLeaksGetName(const ABlock: Pointer): String; // EMemLeaks
Reads back the name previously set on a block, or returns an empty string when no name was set. The same pointer requirement as the setter applies, with the same caveat for arrays and strings.
MemLeaksOwn and MemLeaksSetParentBlock
procedure MemLeaksOwn(const AParentBlock, AChildBlock: Pointer; const AName: String = ''); // EMemLeaks
procedure MemLeaksSetParentBlock(const AParentBlock, AChildBlock: Pointer); // EMemLeaks
These declare that one block belongs to another. MemLeaksSetParentBlock always sets the parent
link; MemLeaksOwn combines that with naming - the optional AName names the child, and an empty
string (the default) means "do not change name" - and it does not overwrite a parent link that is
already set. This lets you claim ownership of reference-counted data that was handed to you as
an argument (a string passed to a constructor and stored in a field, for instance), where a later
MemLeaksSetParentBlock call would replace whatever link was there.
MemLeaksOwn(nil, Pointer(Stream)) is documented separately as removing a parent-child
relationship to give ownership to external code. Its relationship to the no-overwrite rule is
not specified; do not infer a broader overwrite contract from that special call form.
What a parent link buys is visible in the dump written by DumpAllocationsToFile (chapter 6, section 6.6). While that routine's grouping is on - it is on by default - a child block is not listed
separately: the dump shows the root parent and adds the child's size to it. The documented reasoning is
the point of the feature - if the parent leaked, its children leaked with it, and the children are not
where the bug is. A child whose parent did not leak is still reported. The routine's page adds that
memory allocated inside an object's constructor is treated as a child of that object automatically, by
a heuristic over call stacks - the second of the dump's two grouping switches, and one that is ignored
when grouping itself is off.
Variants exist for the types that do not point at the start of their allocation. MemLeaksOwn has the
DynArrayBoth, DynArrayChild, DynArrayParent and Str forms; MemLeaksSetParentBlock has the same
four with its own prefix. All keep the two-pointer shape and the AName default where naming applies,
and the Child / Parent / Both suffix says which argument carries the special type. The Str forms
are the exception to that reading: there the special type is always the child argument, and the parent
stays an ordinary block pointer.
For MemLeaksOwnDynArrayParent, the child argument is still the child block pointer.
The MemLeaksSetParentBlockDynArray* variants take dynamic arrays in the positions indicated
by their suffixes, not strings.
12.6 Telling EurekaLog about leaks you have decided not to fix
An expected-leak registration is an exclusion, not a release and not a transfer of ownership.
First identify the allocation, its real owner and its intended lifetime. Fix a missing destructor or
finally block in your own code instead of registering the symptom. Narrow exclusions can be useful
for an understood third-party allocation or a version-specific shutdown artifact; record the reason
and the build to which it applies, and review it after upgrades.
EurekaRegisterExpectedMemoryLeak and its variants
function EurekaRegisterExpectedMemoryLeak(P: Pointer): Boolean; // EMemLeaks
function EurekaRegisterExpectedMemoryLeakDynArray(P: Pointer): Boolean; // EMemLeaks
function EurekaRegisterExpectedMemoryLeakStr(P: Pointer): Boolean; // EMemLeaks
Register the block at P for exclusion from the memory leak report. The return value says whether the
registration happened: False "usually indicate that this block was already registered". The routine
works on already allocated blocks, so the page calls it the tool for leaks in third-party code;
for your own code you can choose between it and DisableMemLeaksRegistering.
Pass a pointer to the beginning of the allocation. Dynamic arrays and strings do not point
there, so use the DynArray and Str variants for them. The Str variant accepts ANSI or
Unicode strings, but not WideString.
Register as late as possible, preferably during shutdown while the owner and block are still
valid: a variable freed and reallocated during normal work can change address. Do not keep stale
pointers to register later, and do not free third-party data merely because it appears in a report.
The Str and DynArray variants do not turn managed data into manually owned buffers.
Exception filters cannot be used for this. They have already been finalized when the shutdown leak report is built. Use the expected-leak API, not an ordinary exception filter. This describes EurekaLog's reporting order, not a promise that every RTL finalizer has already run.
Replacing the handler that adds the leaks
Instead of listing the blocks you accept, you can intercept every leak as the check adds it.
The global procedure variable MemLeaksAdd, declared by ETypes, holds the default handler.
Install a replacement from the unit's finalization section, preserving the previous handler:
function FilterLeaks(const AMemBlock: Pointer; const ABlockType: TBlockType;
const LeakDescription: String;
const LeakSingleSize, LeakTotalSize, LeakCount: PtrUInt;
const LeakCallStack: TCallStackArray): Boolean;
Returning False means the leak was not added. Return the previous handler's result for
everything you do not want to exclude. Preserve that handler before replacing it: it turns
a leaked block into a report entry. The handler variable uses TMemLeaksAdd, for which no
complete public type contract is specified. EResLeaks has the same arrangement under the
name ResLeaksAdd.
EurekaUnregisterExpectedMemoryLeak and its variants
function EurekaUnregisterExpectedMemoryLeak(P: Pointer): Boolean; // EMemLeaks
function EurekaUnregisterExpectedMemoryLeakDynArray(P: Pointer): Boolean; // EMemLeaks
function EurekaUnregisterExpectedMemoryLeakStr(P: Pointer): Boolean; // EMemLeaks
The reverse operation - it removes a successfully registered block from the list of exclusions. P is
the pointer that was passed to the matching registration call. True means the leak was unregistered;
False means it was not, and the page lists the ordinary reasons: it had already been unregistered, no
such leak was registered, or the registration call itself returned False. The pages note it is
"usually not needed", but it is what you use when a block's expected-leak status holds only for part of
the program's life.
12.7 The error kinds: TMemoryError
When a check does find something wrong, what it found is described by TMemoryError, a type declared by
ETypes, not by EMemLeaks: it is the type the memory-error handler is called with, and its
identifiers are the vocabulary of a memory problem report. The declaration order - the table below keeps
it - is:
| Identifier | What it means |
|---|---|
leNone |
No memory error is detected. |
leOverflow |
A buffer overflow was detected - some code damaged or overwrote EurekaLog's internal header. |
leMultiFree |
Double free of a memory block or an object. |
leObjectInUse |
An attempt to delete an object that is still in use. |
leInvalidBlock |
A pointer to an invalid block was passed to FreeMem or ReallocMem - a block without a valid EurekaLog header. It can be a valid but corrupted block (overflow or underflow), a block that was already released (double free), a random or rogue pointer, or a block allocated by another memory manager. |
leCorruption |
The passed block is still valid, but EurekaLog's internal linked list is corrupted. |
leHang |
The memory manager is unresponsive - hung, deadlocked and so on. The page names damaged internal structures of the memory manager or a terminated thread as possible causes. |
leUseAfterFree |
An attempt to use an already released object or interface was detected - a virtual method was called on a deleted object. |
leOutOfMemory |
Not enough memory to allocate the requested amount in GetMem or AllocMem. |
leUninstalled |
The application called a memory manager function after finalization. The page names the usual cause: a shared memory manager with the wrong finalization order for DLLs. |
The values are combined in TMemoryErrors, "set of possible memory errors" (also ETypes) - a set of
TMemoryError - and its page names the variable that consumes the set: the global
MemLeaksErrorsToIgnore. That variable is how a kind of error becomes tolerable rather than fatal - the
page for CheckHeap (chapter 6, section 6.6) says the call does nothing at all when leOverflow is
listed in it, and the allocation pages point at the same variable for allocation failures. The type page
names its own consumer too: TMemoryError values are what the TMemLeaksError handler receives, and
MemLeaksError is the global variable holding that handler. Neither of those two globals nor the
handler type has a page of its own in the reference set, so no declaration of them is reproduced here.
The table's order is the declared one, and the type page is the reason it can be quoted: the declaration
is given in full - leNone, leOverflow, leMultiFree, leObjectInUse, leInvalidBlock,
leCorruption, leHang, leUseAfterFree, leOutOfMemory, leUninstalled - so an ordinal taken from
that order is documented, even though the page gives no numeric value for a member beyond its position.
12.8 What the checks cost, and what they cannot see
The cost depends on the build, allocator, bitness, enabled checks and allocation pattern. The limitation pages quote approximately 5% operation overhead and 200/400 bytes per allocation on x86-32/x86-64. Treat these as historical estimates, not a ceiling or capacity budget. KnowHow 92 discusses much larger slowdowns in allocation-heavy workloads; KnowHow 98 uses 32-byte headers without leak stacks and 272-byte overhead with leak checks in its examples. Those figures do not establish a like-for-like configuration with the earlier estimates.
The practical distinction is stable: the installed filter adds bookkeeping; registering allocations
adds call stacks and metadata. Disabling registration reduces that work but does not uninstall the
filter or eliminate all per-block overhead. Many tiny allocations can make metadata much larger than
the application's requested payload. Measure AllocMemSizeEx and AllocMemSizeEx2 in the actual build.
For a slowdown, first separate repeated exceptions, memory operations, thread creation, startup symbol work and internal troubleshooting logging (chapter 5). A temporary rebuild without the extended memory manager can isolate the memory path, but it removes checks and affects exception tracking too: it is not the default fix. Prefer reducing small-allocation churn, narrowing deliberate registration suppression, or disabling leak-stack collection while retaining the filter. Review chapter 22, section 22.2, before changing filter installation.
The documented leak-stack limit is 35 entries. Leak reports omit CPU/Assembler sections and standard reporting events because they are generated during shutdown; do not rely on live UI or ordinary event handlers to enrich them.
The checks also have edges you should know before you trust a report:
- They work only for the standard Delphi heap and memory manager -
GetMem,FreeMem,ReallocMemand their wrappers. Leaks made withHeapAlloc/HeapFreeorVirtualAlloc/VirtualFreeare invisible to this unit; resource leaks are the other feature's job. - With a shared memory manager, every module must be compiled with the same memory manager settings, and EurekaLog cannot produce a human-readable call stack if the DLL that created the leak has already been unloaded.
- In C++Builder, Dynamic RTL disables these memory features. Complete the bridge-unit, linker and initialization setup in chapter 2 before interpreting an empty report as success.
- Reproduce with checking explicitly enabled outside the debugger as well as in the IDE. A changed report is evidence to investigate, not proof that every outside-debugger entry is a real leak.
12.9 Contract limits
No complete public declarations are specified for ActivateMemoryLeakCheck and
SetMemLeaksRegistering (the report switch and the other registration setter), the globals
MemLeaksErrorsToIgnore and MemLeaksError, the handler type TMemLeaksError, or the
MemLeaksAdd handler of section 12.6 with its TMemLeaksAdd type. Do not infer a callable
signature or an ownership contract from those names alone.
The two types DumpAllocationsToFile takes for filtering and sorting are declared by EMemLeaks and
work on the block header:
TDumpFilterFunc = function(const ABlock: PPointerRecord): Boolean; // EMemLeaks
TDumpCompareFunc = function(const AL, AR: PPointerRecord): Integer; // EMemLeaks
The filter's result carries its meaning: True keeps the block in the report, False leaves it out. The
comparator's convention is worth stating, because it is not the left-operand-first one you might expect:
0 means the two blocks are equal, a positive result means AR is greater than AL, and a negative
result the opposite. The same type has a third role in the dump routine - counting duplicates - where
only equality matters: zero means equal, any non-zero result means different; the page notes that one
comparator can serve both roles.
PPointerRecord is ^TPointerRecord (both declared by ETypes). The record exposes the block's
original, single and total sizes, its Module, ThreadID, Name, Parent link, BlockState,
BlockType, and the CallStack of the allocation that created it. The type is subject to
change and should be considered undocumented. Sizes, block type and name are readable from a
filter or a comparator; the layout is not a stable contract.