EurekaLog
Menu

The EurekaLog Developer Guide

29. Reports from DLLs and packages

A DLL report crosses several ownership boundaries: the exception object, the code that collects information, the handler that processes it, and the symbols that describe each frame. Diagnose them separately. A correct stack with the wrong exception class is not the same failure as no stack or no report at all.

Chapter 16 explains module profiles, exception boundaries and shared memory. These cases add the evidence that changes an investigation: a rollback, a hook-setting workaround, a missed interface release, or a customer retest on a named build. Where an early support diagnosis was corrected later, the correction matters more than the first plausible explanation.

29.1 Who handles the exception

The documented split is short. The exception tracer - the code that notices an exception, builds the stack and produces the report - lives in one place per module: in the host, if the host carries EurekaLog and the DLLs are built with the lightweight profile that asks the host for options and handling; or in each DLL, if the DLLs are self-contained because the host may not have EurekaLog at all (chapter 16, section 16.2). The same line of source behaves differently depending on the profile its module was built with: HandleException called from a lightweight DLL is answered by the host's tracer, and the same call from a self-contained DLL runs the DLL's own.

The cases show how easy it is to be in neither state. One is a host with EurekaLog and a DLL built lightweight, in which exceptions raised inside the DLL were not managed at all: the host worked, the DLL's exceptions did not, and the trace went through EurekaLog's own code rather than the developer's. The exchange continues with a second report of the same symptom and no diagnosis. That observation alone does not tell you whether activation, handling or symbol lookup failed. A second case in the same configuration reached a decision instead: after working with both profiles, the developer moved the DLL to the self-contained one, on the reasoning that an exception raised inside the library would then be handled there. Its follow-up is instructive differently - the exception was handled, while an event handler that asked the exception information object for its call stack found it empty, so "handled" and "with a stack you can read" are two separate achievements (chapters 8 and 11).

Both directions of the boundary have their traps. In one sample illustrating the boundary failure described in chapter 16, the host called an exported function inside a try/finally, freed the library in the finally, and let the exception continue; the staff answer quotes the code back with the reason it cannot work - the exception object belongs to the library and is released when the library is, so the framework's own handler, reached after the block, touches freed memory (chapter 16, section 16.3).

ISAPI: correct stack, wrong exception class

Two ISAPI conversations have useful epilogues. Neither supports blaming the customer's exception handling simply because a report's class changed.

An exception changed even inside the DLL's own handler. A developer using EurekaLog 7.8.2.39 reported an external-exception result even when an exception was raised and caught wholly inside the DLL. That was stronger evidence than an exception escaping into IIS: the application boundary was not required to trigger the symptom. The conversation does not establish the IDE or bitness.

After support asked for a retest, the customer reported success with the then-latest build. Support believed the relevant low-level-hook fix was in 7.8.2.107 Beta 12, and believed the issue had been introduced in 7.8.2.31 RC 2. Those version attributions were explicitly tentative. Preserve the positive upgrade result without turning that recollection into a verified affected range or claiming the customer named the successful build.

A second case separates thread bookkeeping from exception conversion. With Delphi 10.3, Win64 and EurekaLog 7.8.2.70 Beta 8, the customer described the class-conversion symptom after moving from 7.8.1.0 through 7.8.2.61 and 7.8.2.70. The ISAPI DLL was affected while the desktop application was not; the customer also reported the symptom with Delphi 10.4.

The investigation first found a problem in EurekaLog's thread bookkeeping. Support identified a known typo in TThreadsManager.NotifyThreadStart and supplied a workaround. It also corrected an unrelated theory: Windows thread IDs remain 32-bit in Win64 code. This was not a customer-code bug whose correction restored the exception class.

The chronology makes that distinction decisive:

  1. After the offered 7.8.2.82 RC 11 build, the customer still obtained valid stacks with the wrong exception class.
  2. Rolling back to 7.8.1.0 restored the class.
  3. A low-level-hook configuration change proposed by support also restored it. The exact successful option value is not stated in the message text; it was communicated by screenshot.
  4. Support then reproduced the class-conversion failure locally, offered 7.8.2.107, and later received a customer report that the latest tested build worked in the ISAPI DLL. The reply does not repeat the build number, so the evidence is a successful retest following that offer, not a universal guarantee about every configuration.

When the stack is already correct, changing MAP settings is not the first discriminator for a wrong exception class. Compare a minimal exception in the affected DLL across the last working build and the suspect build, preserving host, bitness and settings. Internal EurekaLog logging can show where the exception record changes; chapter 5 covers enabling that logging in a host process with a writable output location.

ISAPI: nothing was saved

A later investigation supplies a separate checklist. Check whether EurekaLog is active in the request thread, whether an intervening try/except prevents automatic handling, whether the DLL has the ISAPI hooks, and whether the server still has an old DLL loaded. EAppISAPI enables EurekaLog for ISAPI threads when the integration is present. Calling BuildBugReport only creates report text; it is not the automatic processing-and-saving pipeline. Section 29.3 shows how this distinction affected an otherwise working shared-module design.

29.2 Whose debug information, and whose module

A call stack is addresses first and text second, and the text comes from the debug information of the module an address falls in (chapter 11). With a host and a library that is two questions, and the cases are about the second one failing quietly.

The first shape is a report with only the DLL's frames. Chapter 16 states the documented example: when the exception escapes to a host that has neither EurekaLog nor debug information, the report the DLL makes shows only lines within the DLL, because there is nothing to describe the host with. A case is the developer's version of the same discovery - the host was fine, the DLL's exceptions were not reported usefully, and the trace led into an internal unit the developer had no source for, so the question asked was about debug information and build settings rather than about the exception. That case has no recorded outcome.

The second shape is a stack whose lines are half readable: a package carries its own debug information, so an application whose executable's debug information was protected and whose package's was not produced a stack that came out partly encrypted and partly plain. In a 7.9-era case, this was offered as a hypothesis, with supplying the password at run time as the practical way to see the plain parts; the answer stated that there was no ready-made way to decrypt a stack afterwards.

The third shape turns on the difference between a module and your module. A case from a 7.13-era build documents a background thread raising an access violation repeatedly with no report, and the developer testing the two obvious switches - the memory filter and including stacks from other modules. The staff diagnosis is precise: the exception is ignored because its address belongs to no executable module, and the setting that governs exceptions from outside the current module decides whether it is included at all. The same answer states the limitation that makes this a case rather than a bug report: EurekaLog cannot know that dynamically generated code belongs to your program, so no option recovers the attribution, and that option has to be paired with the chained-exception logging setting for the report to be coherent. The customer's reading - that their code raised it and it should be treated as theirs - is a hypothesis the staff answer disputes, with the log as the evidence.

For an ordinary code package used by an EurekaLog-enabled host, the requirement is the package's debug information, not another independent tracer. The package profile in chapter 16 adds that information, so the host can describe package frames in its report. This is different from the deliberately shared reporting package in section 29.3. A support exchange also explains why symbols are sensitive to build settings: post-processing reads the project's map file to build the block it writes into the binary, so turning debug information off in the build removes unit names, method names and line numbers from future reports although the binaries still run. A related case - a package raising inside its own internal thread, with no report even when the package was compiled with EurekaLog and the thread derived from the product's own class - ends without a diagnosis, and it makes the point concrete: adding symbols or linking reporting code does not establish a thread's activation and handling path. The record does not determine which stage failed in that package.

29.3 One tracer, several tracers, and whose options apply

Chapter 16 documents the two schemes and when each is appropriate; the cases add what happens when a process ends up with more than one tracer for reasons nobody planned.

One case concerns an application that loads several libraries, one of them built self-contained. After that load, a later lightweight library addressed its requests to the self-contained one instead of the host, so reports went to a module whose dialogs were configured off and nothing visible happened. The explanation is the initialisation order: a self-contained library installs itself as the EurekaLog host when it initialises, and the next library to load finds that host rather than the executable. The recommendation is to rebuild the offending library lightweight; the alternative offered is a deliberate re-announcement from the host after the load, handing the library the host's own exception manager so that the host becomes the host again - and the customer confirmed that workaround in the sample. The useful distinction is between the registered host used by lightweight DLLs and the order in which independent tracers see exceptions. They are not one universal "last loaded wins" rule.

Two ISAPI DLLs under a host without EurekaLog

With EurekaLog 7.16.0.0, Delphi 13 and Win32, two ISAPI DLLs worked individually but loading the second caused a request timeout in Release builds. Debug builds did not show the same failure. The customer also compared 7.15.0.1, which stalled, with 7.14.2.x, which did not. That comparison identified a version-sensitive symptom, not proof that the earlier arrangement was safe.

Support initially blamed missing non-visual configuration, then retracted that diagnosis. The DLLs were correctly non-visual. According to the corrected explanation, an exception raised by the cross-module consistency check reached the RTL's own message-box handler while EurekaLog was already collecting exception information. The invisible dialog waited for input on a non-interactive desktop. Disabling EurekaLog's normal dialog was therefore not the missing setting.

The underlying problem was two complete EurekaLog instances observing the same exceptions. The first could associate its information object with an exception and the second could encounter an object created by a different instance, potentially a different version or a DLL about to unload. A configuration check made that incompatibility visible early.

Support investigated three arrangements. Their outcomes are different:

Arrangement Important condition Recorded outcome
One common standalone DLL; the ISAPI DLLs are lightweight Load the common DLL first, for example through a static import. It owns the reporting options. Initial generic sample worked; the ISAPI adaptation needed thread activation changed in the common DLL. The customer then confirmed both ISAPI DLLs could run and generate reports. Updated source accompanied the samples.
One custom runtime package shared by the ISAPI DLLs The package initializes EurekaLog from its own settings, rather than assuming the host EXE supplies them. After correcting unit placement and report handling, the customer confirmed the sample and real code worked. The exchange does not name a released build containing all accompanying source changes.
Keep the separate instances and disable strict checks All compatibility and lifetime conditions below must hold. Staff proposal and supplied example; no distinct customer retest of this route.

The common-DLL experiment exposed a subtle thread boundary: its "RTL threads" policy did not recognise threads created by the other DLL's RTL. Support diagnosed that as leaving the exception information unset, so manual report creation received no information object. Enabling all threads in the common DLL made the sample work and received a customer retest. That is the result for this architecture, not a recommendation to enable every thread in every application.

The custom-package route has a different ownership rule. It replaces EurekaLogCore for these DLLs: neither the custom package nor the DLLs should also reference the precompiled EurekaLogCore. The custom package and the DLLs can share the same settings. Units whose dependencies carry DENYPACKAGEUNIT cannot simply be moved into it: support placed EAppISAPI in the ISAPI DLLs, not in the common package; package-compatible integration units remained in the package, with rtl, vcl and inet dependencies as appropriate. The final successful retest followed these corrections, not merely selection of "build with packages."

The strict-check workaround is conditional, not a general repair. In the 7.16.0.0 investigation, support allowed setting ETypes._StrictChecks := False only when:

  • the host never loads an independently supplied EurekaLog-enabled DLL;
  • every participating DLL is rebuilt with the same EurekaLog version;
  • old and new DLL builds are never mixed in the process;
  • no participating DLL is unloaded while another is still working.

It must run before EurekaLog initialization. Put the assignment in the initialization section of a dedicated unit that uses ETypes; place that unit before all other EurekaLog units in the .dpr and add its name to the existing PreInitUnits option on Advanced / Custom/Manual (or in the .eof). Preserve the other entries in that option. Setting the flag in the program body is too late. This restores the older permissive behavior; it does not repair incompatible layouts or dangling callbacks. If you cannot enforce every condition, use a deliberately shared instance instead (chapter 16). Do not assume an IIS deployment satisfies the lifetime condition merely because ordinary requests do not unload DLLs.

One owner for each packaged unit

A design-time package must not be a runtime dependency. For a unit needed by several modules, decide whether it belongs in the application or in exactly one shared runtime package, then make the other modules require that owner. Read IDE dependency prompts in that context: blindly accepting or refusing one is not a universal recipe. In particular, the custom-package arrangement above deliberately declines a dependency on EurekaLogCore because it is replacing that package, not duplicating it.

One case in this group is worth keeping precisely because the answer was wrong and was corrected in the same exchange: a developer could not install a third-party package because a unit it contains is also contained in a EurekaLog package. The first answer attributed the unit to a standard library package; a later one corrected it - the unit belongs to no package at all, which is why the conflict exists - and said a future build would remove the dependency, while warning that any other package carrying the same unit would bring the conflict back. What to take from it is the shape of the question: when a package refuses to load because two packages contain the same unit, the interesting fact is who owns the unit.

29.4 Unloading

Unloading is where the split between modules becomes a lifetime problem. The cases line up with what chapter 16 documents, and add the product defects that were found this way.

The base case is a DLL unloaded while an exception from it is still alive. On one case the staff answer follows the log to the point where the application exits normally and exceptions from a logging DLL are recorded, then states the two rules: exceptions must not cross between the executable and the library, and without a shared memory manager the object the exception points to belongs to a module that can be gone before the object is released - so releasing it runs a destructor from an unmapped module. The answer notes that the log showed no error while EurekaLog was active, which is why nothing was reported: the fault happened after the reporting machinery had shut down. Chapter 16, section 16.3, documents the same rule and the remedy - handle the exception while the library is still loaded.

Another case concerns hook restoration. A host loaded one library, which loaded another; the unload order was not the reverse of the load order, and the result was a crash when any hooked function was called, because the chain of saved hooks was restored to point at a library that had already been unmapped. The answer reconstructs the five steps of the failure and adds the detail that makes it intermittent: on one Windows version the inner library is unloaded during the outer library's finalisation, on another it is unloaded afterwards, so the same application fails on one system and not the other - a difference credited to how delayed library loading works, in a Delphi 10.2-era project. Hooks are restored in the order the modules were finalised, not in the order they were loaded.

The third case is about which thread unloads. The staff answer reads the log and observes that the thread that loaded the library - which the library treats as its "main thread" - was explicitly terminated, while another thread unloaded the library, and comments that the RTL assumes a library is loaded and unloaded in the same thread. The same answer adds that if a library is never unloaded, the leak checks are skipped (chapter 22 is the reading side of leak reports).

Two different shutdown fixes in 7.16

The release notes for 7.16.0.0, 2026-06-26, list two separate fixes:

  • Lightweight DLL calling a finalized host. A Delphi 12 Athens / Win32 case reported against 7.15.0.2 Hotfix 2 initially received an application-lifetime diagnosis: unload the DLL before closing the EXE. Support later retracted the assertion that no host-side unlinking existed. EurekaLog already had code to unregister a finalized host and let the DLL revert to RTL handling, but an additional interface introduced in 7.14 RC 5 had been omitted from that cleanup. Support offered 7.16, and the customer reported that the sample no longer reproduced the errors. That is a diagnosed product defect and positive sample retest, not proof that late exceptions can still be reported through a host that has already finalized.
  • False finalization-order panic with a shared manager. A later staff explanation identifies a wrong array-element size in EMemLeaks.TModulesList.InsertItem. An EurekaLog-enabled host loading two or more EurekaLog-enabled DLLs that share its memory manager could corrupt the module list and abort with EUsingMMAfterFinalization even when all DLLs unloaded cleanly. The release notes state the fix in 7.16.0.0. The explanatory exchange contains no reproduced match to the customer's old, infrequent startup crashes; those cannot be retroactively attributed to this shutdown bug.

Keep these boundaries when reading a changelog. An interface left linked to a dead host and a wrong element size in a shared-memory module list can both fail at shutdown, but they need different reproductions. Neither fix makes exceptions escaping an unloaded library safe.

29.5 Shared memory managers, and what they do to reports

Chapter 16, section 16.3, documents when a shared memory manager is needed and which managers are compatible; the cases show what it does to the reports.

The first is about which module's settings win. In an application whose shared manager was installed by a library that initialises before the executable - a statically imported library - the answer is that EurekaLog connects to the manager already there, so leak checking in the executable is not active and the checks have to be enabled in the library that hosts the manager. A second answer in the same exchange generalises it: when sharing is enabled and a shared manager is detected, the shared manager is used and the debugging work belongs to it rather than to EurekaLog, so the settings to look at are the manager's, in the module that installed it. The order of initialisation decides which module that is, and static linking is the usual reason it is not the executable.

The second is a configuration failure with no recorded cause: enabling sharing in both host and library as documented produced an error at load time - the manager cannot be installed or shared because memory has already been allocated through the default manager - and the application stopped. Support could not reproduce it from the supplied sample. By the time installation is attempted, allocations have already occurred through a different manager. The exchange does not establish which allocation caused this case, and the staff's inability to reproduce it does not establish a working fix.

The third is the argument against the whole arrangement, from a staff answer: leak detection with a shared memory manager is "a very bad idea", because of an asymmetry - the executable loads a library, the library allocates through the shared manager, the library is unloaded without freeing, and at shutdown the leak is found by the executable, which cannot produce a meaningful report because the module that allocated the memory is no longer mapped. The recommendation is to refactor the library's API so it does not depend on a shared manager, and the same exchange disposes of the legacy route: ShareMem and the Borland memory-manager DLL are obsolete from the point at which the compiler began shipping a manager of its own, and the unit a modern project uses to share is a one-line dummy telling the manager to do it. A second answer gives the same conclusion from the other side - with packages no shared manager is needed at all, because the code already references the run-time library's own manager, and with a shared manager a leak in a dynamically unloaded library is reported at process exit, when the library is gone. Both are staff reasoning about the mechanism rather than reproduced results, and they should be read as such; they agree with each other and with the documented rule that the checks run at the application's exit (chapters 12 and 22).

29.6 What these cases do not establish

  • Lightweight DLL exceptions not managed, or a package thread producing no report: the respective exchanges do not identify the failed stage. Check activation, handling, profile and symbols separately rather than borrowing a diagnosis from a similar-looking case.
  • ISAPI exception-class conversion: these cases do have positive upgrade epilogues. The first has tentative staff fix/introduction version attributions; the second has a hook-setting workaround, a vendor reproduction and a positive retest after a 7.8.2.107 offer. They do not establish the exact hook setting hidden in a screenshot or every affected build.
  • Multiple ISAPI DLLs: the common-DLL and custom-package routes received positive retests after source/configuration changes. The strict-check bypass remains a conditional proposal without its own customer retest, and the released version containing the supplied source changes is not named.
  • Shutdown fixes: the two 7.16.0.0 release-note claims have distinct causes and prerequisites. The lightweight-interface case additionally has a positive customer sample retest.

For your own case, record who owns the exception, which instance collects it, who handles it, whose options apply and when each module unloads. Then choose one comparison that can distinguish your competing explanations. A similar message in another version is a lead, not a root cause.