If a request opens a handle and never closes it, Delphi's heap counters may look perfectly healthy.
Use EResLeaks to investigate the supported Windows API calls that acquire and release that resource.
Start from the acquisition/release pair, not from the word "memory": direct heap and virtual-memory
allocations are resource leaks in EurekaLog's terminology even though they consume memory.
This chapter also covers EResourceStrings, whose similarly named task is unrelated: clearing
EurekaLog's cache of loaded resource strings when an application changes resource DLLs. Cache
cleanup is not a general leak repair.
19.1 Switching resource DLLs: clear the string cache deliberately
Delphi resourcestring values live in a module's resources. An application can use resource DLLs to
switch translations without replacing executable logic. EurekaLog's EResourceStrings loading
fix-up uses a heap buffer instead of the RTL loader's stack buffer to avoid that stack-allocation
pressure. Its published cache operation is:
procedure ResStringCleanupCache; // EResourceStrings
Call it as part of your application's controlled resource-DLL switch, at the point where the
application invalidates already loaded strings. Add EResourceStrings to that unit's implementation
uses and invoke ResStringCleanupCache; there. It clears EurekaLog's internal loaded-string
cache; it does not select a language, load or unload a DLL, or refresh captions already copied into
controls. Those remain the translation system's work.
The operation is parameterless and provides no status result or per-module scope. Do not rely on locking, concurrent-loader safety, cache entry lifetime or storage reclamation as API guarantees. Do not introduce a background-thread cache clear on the assumption that it is synchronized. Coordinate the switch with your existing translation mechanism and verify freshly loaded messages as well as already-visible controls. Do not assume the operation cannot raise an exception.
19.2 What resource-leak detection covers
Chapter 6, section 6.6, lists the memory-leak side of leak detection: routines about blocks allocated
through EurekaLog's memory filter, and about errors inside such blocks. EResLeaks is the other side,
and it works differently - not by replacing an allocator and remembering what went through it, but by
watching named Windows routines and checking the pairing between calls. The two parameters that carry
that idea are in the registration routine of section 19.3: ACreate, documented as "True - function
creates resource, False - function destroys resource", and AResultParam, whose description ends with
the phrase "so you only need to watch for pairing calls".
The list of functions that are already watched
The monitored functions include these acquisition/release families; coverage can vary by release:
HeapCreate/HeapDestroyHeapAlloc/HeapReAlloc/HeapFreeVirtualAlloc/VirtualAllocEx/VirtualAllocExNuma/VirtualFree/VirtualFreeExGlobalAlloc/GlobalReAlloc/GlobalFreeLocalAlloc/LocalReAlloc/LocalFreeSysAllocString/SysAllocStringByteLen/SysAllocStringLen/SysReAllocString/SysReAllocStringLen/SysFreeString/VariantClear
Read as a coverage statement, the families are four kinds of raw memory - heaps, virtual memory,
global memory and local memory - plus the COM strings, where VariantClear is grouped with the
Sys*String calls because it is the call that releases a string owned by a variant. Every name on the
list is a Windows API; there is no Delphi RTL routine and no VCL member anywhere in it. The unit's
subject, in other words, is not "anything you allocate" but the specific creation-and-release pairs the
vendor has tested.
Turn coverage into a checkable experiment
- Identify the actual acquisition API and its matching release, including wrappers or third-party
code. For example, monitoring the Delphi heap will not identify a missing
VirtualFree. - Enable Catch resource leaks, select the relevant APIs to monitor category, and verify the debugger-only/manual-activation settings (chapter 22, section 22.2).
- In a disposable diagnostic build, exercise a supported pair and close normally. Then deliberately omit the release in a separate local run and check that the resource entry appears. Restore the release before investigating the real workload.
- If the real allocation uses another entry point, investigate that path's coverage; do not infer
coverage from a similar API name or a healthy
EMemLeakscounter.
This list is not an exhaustive promise for every release. Only supported monitored functions
are traced, and the mechanism hooks imported DLL functions rather than observing all kernel
allocations. A missing report may therefore be a coverage or activation problem, not a clean bill
of health. For an uncovered API, HookWin32API supplies the registration described next.
19.3 HookWin32API: adding a function to the monitoring
The one routine of EResLeaks exists for exactly one purpose, stated in one line: it "Adds a function
to resource monitoring". Everything else about it is in its parameters, and those deserve a table.
The signature
function HookWin32API(const AModuleName, AAPIName, AAPICategory, AAPIType: String;
AParamCount: Integer; ACreate: Boolean; ACheckType: TCheckType = ctEqual;
ACheckValue: PtrUInt = 0; AResultParam: Integer = -1): Pointer; // EResLeaks
Three of the nine parameters have defaults: ACheckType defaults to ctEqual and ACheckValue to
0 - those two are the ones that decide how a call is judged - while AResultParam defaults to -1,
which places the resource in the call's return value.
Treat the returned Pointer as opaque: its meaning and ownership are not part of the public contract.
Discard it rather than calling through it, freeing it, interpreting nil as a failure code, or
inventing an unhook operation from its value.
The parameters
The nine parameters, including four in a shared const group, have these roles:
| Parameter | Meaning |
|---|---|
AModuleName |
"DLL file name to hook. Case insensitive" |
AAPIName |
"DLL's function name to hook. Case sensitive" |
AAPICategory |
"Category for API." |
AAPIType |
"Sub-type of API. Typically equals APICategory" |
AParamCount |
"Number of functions arguments" |
ACreate |
"True - function creates resource, False - function destroys resource" |
ACheckType |
"Specifies valid/invalid conditions for function call. See TCheckType description for more details" |
ACheckValue |
"Value for failure call. See TCheckType description for more details" |
AResultParam |
"Index of function's argument which returns resource (for Create-like functions) or accepts resource (for Destroy-like functions). Use -1 for functions and >= 0 for procedures. Use -2 for resources without handles at all (i.e. handle is never exposed, so you only need to watch for pairing calls)" |
Three details in that table are worth pulling out, because they are the ones that trip a reader:
- The two names are treated differently. The module name is documented as case insensitive and the
function name as case sensitive, so
'Kernel32.dll'and'kernel32.dll'are the same registration while'HeapCreate'and'heapcreate'are not. AAPICategoryandAAPITypeusually have the same value. A distinct type name is useful when you need a finer grouping inside a category; the example below otherwise uses the same name for both.AResultParamis the only parameter with a rule inside it. It has three documented cases:-1when the resource is the return value of the call, an index>= 0when the resource comes back through (or is accepted as) one of the arguments, and-2when there is no exposed handle at all and the only thing to watch is the pairing of the two calls. The default-1is therefore the case that needs no typing on a create-like function that returns its own handle.
Register a complete acquisition/release pair
Add the following complete Delphi unit to a controlled diagnostic project, after EurekaLog's required early units and before the code that uses the pair. Enable resource checking as above. First establish that your installed configuration does not already register this pair: duplicate registration has no public safety guarantee.
unit DiagnosticDCHooks;
interface
implementation
uses
EResLeaks;
initialization
HookWin32API('Gdi32.dll', 'CreateCompatibleDC',
'Device Contexts', 'Device Contexts', 1, True, ctEqual, 0, -1);
HookWin32API('Gdi32.dll', 'DeleteDC',
'Device Contexts', 'Device Contexts', 1, False, ctEqual, 0, 0);
end.
Both APIs take one argument. CreateCompatibleDC returns the resource, so AResultParam is -1;
DeleteDC receives it in argument zero, so its value is 0. Both return zero on failure, hence
ctEqual, 0. Registering the acquisition alone would not describe the resource's lifecycle.
This pair is for compatible device contexts; do not substitute DeleteDC for ReleaseDC when
working with a DC obtained through an API that requires the latter.
These additional registration examples are reference data, not instructions to install every hook:
some overlap the built-in list, and exact exported entry-point names must match the actual DLL/import.
In particular, the unsuffixed CreateDC/CreateIC names must not be assumed to cover all
ANSI/Unicode entry points.
| DLL / API | Category / type | Arguments | Creates? | Resource location |
|---|---|---|---|---|
Kernel32.dll / HeapCreate |
Heaps / Heaps | 3 | Yes | -1 |
Kernel32.dll / HeapDestroy |
Heaps / Heaps | 1 | No | 0 |
Gdi32.dll / CreateCompatibleDC |
Device Contexts / Device Contexts | 1 | Yes | -1 |
Gdi32.dll / CreateDC, CreateIC |
Device Contexts / Device Contexts | 4 | Yes | -1 |
Gdi32.dll / DeleteDC |
Device Contexts / Device Contexts | 1 | No | 0 |
AdvAPI32.dll / CreateRestrictedToken |
Handles / Tokens | 9 | Yes | 8 |
AdvAPI32.dll / OpenProcessToken |
Handles / Tokens | 3 | Yes | 2 |
AdvAPI32.dll / OpenThreadToken |
Handles / Tokens | 4 | Yes | 3 |
AdvAPI32.dll / DuplicateToken |
Handles / Tokens | 3 | Yes | 2 |
AdvAPI32.dll / DuplicateTokenEx |
Handles / Tokens | 6 | Yes | 5 |
Kernel32.dll / CloseHandle |
Handles / Handles | 1 | No | 0 |
All these registration examples use ctEqual and failure value 0. Token APIs demonstrate why
AResultParam is about resource location, not whether a routine syntactically is a function:
they return a success flag and write a handle through an output argument. The zero-based index
identifies that argument. CloseHandle instead consumes the handle in its first argument.
Before adapting a registration, write down the exact argument count, failure convention, resource location and matching release. Exercise successful creation/release and a failure case locally. Do not rely on the return value to certify installation, on duplicate hooks being safe, or on a public unregister operation. Do not deploy an unverified hook merely because the unit compiled.
Parameter constraints
Three constraints rule out combinations of parameters:
- ACheckValue is ignored (and must be 0) for ctNone, ctHResult, ctNTStatus.
- AResultParam can't be a return value, if check type is ctHResult or ctNTStatus
- Dispose-like function (ACreate = False) can't return handle - handle should be only passed as argument or be an internal handle (AResultParam = -2 or AResultParam >= 0)
Read together with the meanings of the check kinds, the notes separate two things that are easy to lump
together. The check value stops being yours to choose whenever the outcome is decided by the API's own
convention rather than by a value you compare - that is the case for ctNone, ctHResult and
ctNTStatus alike, and it is what the first note says. Where the resource can come from is a narrower
matter: the second note bars a return value only for ctHResult and ctNTStatus, so a ctNone
registration may still leave AResultParam at -1. For those two kinds the resource comes back through
an argument, or - when it never surfaces at all - the registration watches the pairing instead (-2).
The third note is the destroy side of the same idea: a call that releases a resource has no handle to
return, so its resource is either an argument or, again, the pairing itself.
Two practical consequences follow, and both are the sort of thing that only bites when the registration is wrong:
- Passing a meaningful
ACheckValuetogether withctNone,ctHResultorctNTStatusis not merely ignored - the note says it must be 0, so keep the default there; - A destroy-side registration always needs its
AResultParamspelled out, because there is no handle in the return value to fall back on.
19.4 TCheckType: how a call is judged
Each registration carries a check kind, declared as TCheckType. It answers a narrow question:
given a call that has just returned, was it a success or a failure? Three identifiers use a failure
condition; the other three select a result convention. The type controls how HookWin32API classifies
the outcome, not what the monitored function does.
The enumeration is declared in this order:
TCheckType = (ctNone, ctEqual, ctNotEqual, ctHResult, ctNTStatus, ctGetLastError); // EResLeaks
The table groups members by behavior rather than declaration order. For ordinals - an integer cast or a stored value - use the declaration above.
| Identifier | Documented meaning |
|---|---|
ctEqual |
"Function fails if return value equals to CheckValue" |
ctNotEqual |
"Function fails if return value wasn't equal to CheckValue" |
ctGetLastError |
"Function fails if return value equals to CheckValue and GetLastError returns value other than ERROR_SUCCESS" |
ctNone |
"Function always successful" |
ctHResult |
"Function returns standard HResult value" |
ctNTStatus |
"Function returns standard NTSTATUS value" |
The first two are your own criterion: you name a value, and the call either counts as failed when it
matches (ctEqual) or when it does not (ctNotEqual). ctGetLastError is the only kind that looks at
anything besides the result: the result must equal the check value and the last error must say
something other than ERROR_SUCCESS. The last three are conventions rather than comparisons: ctNone for a call
that always counts as successful, and ctHResult and ctNTStatus for calls that report their own
outcome in their result.
How the type meets the other two parameters
TCheckType is not read on its own: it combines with ACheckValue and AResultParam under the notes
of section 19.3.
- With
ctEqualandctNotEqual, both values are yours.ACheckValueis the value being compared against, and the type decides which side of the comparison is the failure. - With
ctNone,ctHResultandctNTStatus, the check value is not.ACheckValueis ignored and must be0- leave the default in place. - With
ctHResultandctNTStatus, the resource cannot be the return value.AResultParammust point at an argument (or be-2), because the result is spent on the outcome convention.
These checks classify the API call's result; they do not establish resource ownership beyond the acquisition/release pairing.
What the example does and does not exercise
The registration examples above use ctEqual. For ctNotEqual, ctGetLastError, ctNone,
ctHResult or ctNTStatus, apply the corresponding result rule and the parameter constraints
in section 19.3, then exercise both success and failure for the actual API.
19.5 Read resource reports within their limits
A shutdown entry means the monitor still considers the resource outstanding at its checkpoint. It does not prove that every later RTL finalizer has run. Investigate intended ownership and finalization order before deciding whether the entry is a real leak or an understood shutdown artifact (chapter 22, sections 22.5 and 22.9).
The recorded stack is documented as limited to 35 entries. Leak reports omit CPU/Assembler sections and do not invoke standard reporting events; an allocating DLL unloaded before reporting can also prevent a human-readable stack. Preserve module/build information while reproducing.
The resource limitation page quotes approximately 5% operation overhead and 200/400 bytes per allocation for x86-32/x86-64. These are historical estimates, not universal maxima. The amount of hook traffic, stack collection, build and workload matter. The different memory-filter figures in KnowHow 98 are not a measured resource-hook cost; do not transfer them to every Windows API. Chapter 12, section 12.8, and chapter 22, section 22.8, explain how to investigate actual cost.
Keep these boundaries explicit:
- Hooking imported calls is not exhaustive process-wide resource accounting. An unmonitored path is outside the evidence, even if the resource consumes memory.
HookWin32APIregisters monitoring; its returned pointer is opaque. Do not invent an unhook or cleanup operation for that pointer.IsMemLeaksInstalleddoes not certify resource coverage. VerifyEResLeakscoverage with the controlled acquisition/release experiment instead.- Changing a report filter hides entries; it does not close handles or correct a registration whose failure convention or argument index is wrong.
ResStringCleanupCacheaddresses a string cache, not outstanding Windows handles or general allocation growth.