EurekaLog
Menu

The EurekaLog Developer Guide

19. Resources: resource strings and resource leaks

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 / HeapDestroy
  • HeapAlloc / HeapReAlloc / HeapFree
  • VirtualAlloc / VirtualAllocEx / VirtualAllocExNuma / VirtualFree / VirtualFreeEx
  • GlobalAlloc / GlobalReAlloc / GlobalFree
  • LocalAlloc / LocalReAlloc / LocalFree
  • SysAllocString / 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

  1. 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.
  2. Enable Catch resource leaks, select the relevant APIs to monitor category, and verify the debugger-only/manual-activation settings (chapter 22, section 22.2).
  3. 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.
  4. 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 EMemLeaks counter.

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.
  • AAPICategory and AAPIType usually 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.
  • AResultParam is the only parameter with a rule inside it. It has three documented cases: -1 when the resource is the return value of the call, an index >= 0 when the resource comes back through (or is accepted as) one of the arguments, and -2 when there is no exposed handle at all and the only thing to watch is the pairing of the two calls. The default -1 is 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 ACheckValue together with ctNone, ctHResult or ctNTStatus is not merely ignored - the note says it must be 0, so keep the default there;
  • A destroy-side registration always needs its AResultParam spelled 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 ctEqual and ctNotEqual, both values are yours. ACheckValue is the value being compared against, and the type decides which side of the comparison is the failure.
  • With ctNone, ctHResult and ctNTStatus, the check value is not. ACheckValue is ignored and must be 0 - leave the default in place.
  • With ctHResult and ctNTStatus, the resource cannot be the return value. AResultParam must 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.
  • HookWin32API registers monitoring; its returned pointer is opaque. Do not invent an unhook or cleanup operation for that pointer.
  • IsMemLeaksInstalled does not certify resource coverage. Verify EResLeaks coverage 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.
  • ResStringCleanupCache addresses a string cache, not outstanding Windows handles or general allocation growth.