EurekaLog
Menu

The EurekaLog Developer Guide

16. EAppDLL: reporting from a DLL and across modules

Start a DLL integration by deciding two things: how the DLL returns failure and which module owns reporting. Solve them separately. A Delphi exception object is not a portable DLL error value: another compiler, or even another Delphi version, may interpret its fields and memory ownership differently. Catch it inside the DLL and return the error form the API promises.

Deployment Reporting configuration
You build the host and its DLLs Put the tracer in the host; use Lightweight DLL for the DLLs. Change reporting/dialog options in the host.
Your DLL must work in a host without EurekaLog Use Standalone DLL and handle failures inside the DLL before returning an error code.
A system or third-party API defines the entry point Preserve that API's calling convention and failure values; do not substitute an arbitrary HRESULT.
Modules deliberately exchange Delphi objects and exceptions Prefer compatible runtime packages; DLL crossings require additional compiler and memory-manager agreements (section 16.3).

The same HandleException call routes to the host in a lightweight DLL and to the DLL's own tracer in a standalone DLL. Neither arrangement makes an invalid binary interface safe. Use chapter 2 for matching builds and deployment, and chapter 29 for version-specific DLL and package failures, including multi-DLL hosts and unload behaviour.

16.1 Keep the lightweight side lightweight

The Lightweight DLL profile adds EAppDLL automatically. This support unit installs callbacks to the EurekaLog-enabled host; use EBase for basic calls into that host. EAppDLL also declares Exception2HRESULT, the conversion helper in section 16.5.

Do not add ExceptionLog7, EExceptionManager, EClasses or EModules to a lightweight DLL, and do not link it with the EurekaLogCore runtime package. That mixes incompatible integration modes and can terminate the process at startup. In particular, do not paste a CurrentEurekaLogOptions example into this DLL to change the host's dialog.

The profile explicitly permits EBase, EAppDLL, EDLLs, ETypes, EMemLeaks, EResLeaks and ELowLevel. Its own compilation, debug-information, memory-manager and project-hook settings can still matter; most reporting options belong to the host. A standalone DLL follows the other model: it contains the tracer and its own options.

16.2 Which module does the reporting

EurekaLog's reporting machinery in a process is the exception tracer: the code that notices the exception, builds the call stack, and produces the dialog or the report file. With one executable and no DLLs there is one tracer and no question to answer. With DLLs there are two documented schemes, and the choice changes who reports, whose options apply and what a call such as HandleException does.

One tracer, in the host

The recommended scheme when you compile every module yourself: the host .exe carries EurekaLog as usual, and every DLL is compiled with the "Lightweight DLL" profile. That profile will inject debug information into DLL, but will not include exception tracer code; rather DLL will ask host application for information. Consequences the documentation draws:

  • Most EurekaLog options are ignored for a lightweight DLL, "since there will be no EurekaLog's code in your DLL". Dialogs in particular live in the host, so dialog settings belong on the host project. The profile page names the options that still affect such a DLL: project-type hooks, the memory manager, compilation options and debug-information options.
  • A lightweight DLL that shows VCL forms needs the "VCL Forms application" hook, which adds the EAppVCL unit: exceptions raised in a form's event handlers are handled by the framework, not by the try/except around the call that showed the form, so the hook is what lets the host's tracer see them.
  • The DLL must still have debug information, since the call stack for its frames has to be built from something. Enabling EurekaLog for the DLL is one way; the documentation notes it is not strictly necessary, because supplying debug information by other means does the same job.
  • HandleException, called from inside such a DLL, is answered by the host's tracer.

If your API follows the rule of the next section, no other options change in this scheme. If it does not, the documentation allows it as long as the DLLs are lightweight, again with no option changes; DLLs compiled without EurekaLog at all require two options to be turned off on the host, named in section 16.3.

One tracer per module

The second scheme applies when the DLL must work in a host you do not control, and the documentation is direct about when to prefer it: this approach "should be used only when you develop DLLs to be used in non-EurekaLog enabled host". Each DLL is compiled with the "Standalone DLL" profile, which will add exception tracer in each DLL and inject debug information. The host does not need EurekaLog; it may have it, but then the documentation asks you to consider the single-tracer scheme instead.

The behavioural difference is worth stating in the documentation's own words: with a tracer in the DLL, HandleException function will just invoke ExceptionManager.Handle in DLL. It will not try to communicate with exe host. The same call in the same source line therefore produces a report from the host in the first scheme and a report from the DLL in the second. What a tracer in the DLL does not do is report an exception that leaves the DLL: in the documented example the host has neither EurekaLog nor debug information, so the escaping exception is caught there and produces no bug report at all, and the report the DLL does make shows only lines within DLL, "because exe do not have any debug infomation", unless the host supplies debug information of its own.

Two limits come with the scheme. Combining the "Standalone DLL" profile with the "VCL Forms application" hook sets the same options as the "VCL Forms Application" profile, so that is the project type the settings show the next time they are opened. And the scheme leans on the operating system's support for several exception hooks in one process: with multiple tracers only single module can install such hook reliably for the same routine, and two modules attempting it will "either fail or crash". Windows 2000 has no documented way to install such a hook at all; Windows XP and later allow any number of them through Vectored Exception Handling, so for Windows 2000 the advice is Delphi 2009 or above plus "Use low-level hooks" turned off.

16.3 The boundary: an exception must not leave the DLL

Make the exported API behave correctly without EurekaLog first. If it returns an HRESULT, return S_OK on success and a failure HRESULT on error; move the useful result into an out parameter. If it implements an existing API, use that API's success and failure values instead.

Here is a small, explicit HRESULT boundary for a Windows Delphi DLL. Put the routine and exports declaration in the library project, adding Windows and SysUtils to its existing uses clause without removing the EurekaLog-generated units:

function IncrementValue(const Value: Integer; out NextValue: Integer): HRESULT; stdcall;
begin
  NextValue := 0;
  try
    if Value = High(Integer) then
      raise ERangeError.Create('Value cannot be incremented');
    NextValue := Value + 1;
    Result := S_OK;
  except
    Result := E_UNEXPECTED;
  end;
end;

exports
  IncrementValue;

This deliberately returns only a generic failure code; it does not ask EurekaLog to create a report. A caller must check the HRESULT before using NextValue. Section 16.5 adds richer COM error information, and section 16.6 adds an explicit report before conversion. Keep the try/except inside every exported operation that can raise: a handler around the host's call is not a replacement for a library-side boundary.

Why this matters even for two Delphi modules: older exception classes stored Message as an ANSI string; Delphi 2009 and later use Unicode. Sharing the class name does not establish a compatible object layout, allocator or lifetime contract.

Packages, and the escape hatch

If you genuinely want an exception raised in a DLL to be caught by the caller, the documentation's answer is not to use a DLL: "do not use DLLs. Use packages instead." A package guarantees a compatible compiler for host and library and is "generally less problematic with all cross-modules communications". Staying with DLLs and letting exceptions cross anyway is possible, but it needs a shared memory manager - objects are shared between modules, and without one "your application may work without sharing memory manager, but there is no guarantee". EurekaLog is compatible with a shared memory manager as implemented in FastMM and with no other, so ShareMem, borlndmm.dll and third-party managers mean turning "Enable extended memory manager" off in every EurekaLog module instead; the FastMM case needs ShareMM on the FastMM side, both "Enable extended memory manager" and a sharing option on the EurekaLog side, and FastMM's unit before EurekaLog's in the main program's uses clause where one module has both. It also needs the single-tracer scheme of section 16.2, and two host options changed: "Capture stack only for exceptions from current module" must be disabled ("By default EurekaLog captures only exceptions within the same module"), and chained exceptions support must be set to its "Classic" position, that is turned off - tracking an exception's lifetime is what chained exceptions rest on, and that is not possible in the general case, when modules come from different compilers and the RTL gives no help. Neither change is needed if the DLL was compiled with the lightweight profile.

A shared memory manager together with leak detection is called out as "a very bad idea", and the reason comes with an instruction: memory-leak checks are to be turned off when a manager is shared, because the block that leaks belongs to a module that is no longer there to describe it, while the other memory-debugging features stay usable. A DLL linked statically to the host is initialized first, so the manager it brings can become the process's manager and switch the host's own memory debugging off; the documented ways out are to load the DLL dynamically, to use EurekaLog in every module, or not to share a manager at all.

One practical trap belongs here: if a host loads a DLL, calls a function that raises, and unloads the DLL before the default handler runs, the report is built from a module that is no longer in memory, and that step will fail. The documented answer is to call the handler yourself while the DLL is still loaded - Application.HandleException inside an explicit try/except around the call - which is unnecessary when loading, calling and unloading happen in separate handlers, because the default handler guards each.

16.4 The safecall and HRESULT boundary

Windows' standard currency for a failure that crosses a binary boundary is the HRESULT, and Delphi offers two ways to produce one from a routine that can raise.

With an explicit stdcall API, each operation catches its exceptions, returns S_OK or a failure HRESULT, and writes its useful result to an out parameter. A Delphi caller can use OleCheck from ComObj to turn a failure HRESULT into a new, local exception.

On the documented Win32 COM boundary, safecall lets the compiler provide this error-code translation. These are alternative declarations of the same binary shape, not two routines to declare together:

// Explicit HRESULT form:
function ReadValue(out Value: Integer): HRESULT; stdcall;

// Delphi safecall view of that boundary:
function ReadValue: Integer; safecall;

The return value of the safecall form becomes an output parameter at the binary boundary. Exceptions within the operation become failure HRESULTs; the Delphi caller checks the HRESULT and raises a local exception on failure. For an object method, override its virtual SafeCallException to control conversion. A free exported function has no current object and therefore no instance override: use an explicit stdcall/HRESULT wrapper when you need to control reporting and conversion yourself.

Do not use safecall for an API that needs a particular nonfailure result such as S_FALSE. For example, DllCanUnloadNow must retain its explicit HRESULT contract. Do not generalize the Win32 calling-convention illustration into a contract for a different target ABI.

An interface with safecall methods can make ownership and error conversion easier, without requiring a shared memory manager for the interface itself. The shared interface declaration must still use mutually compatible types; returning arbitrary Delphi-managed data does not become portable just because it is a method.

Finish interface use before unloading. Put creation and use of the interface inside a subroutine. Return from that subroutine before calling FreeLibrary, so both explicit interface variables and compiler-created managed temporaries have finalized. Clearing one visible variable alone does not prove that no interface reference remains.

COM's own rules, and the options they imply

COM objects are implemented in DLLs loaded into the host process, and follow "usual rules for DLLs" plus COM's additional restrictions on error handling. As the documentation summarises them: each exception must be handled in the same method and must not escape to the caller; each method that has to report a failure is a function returning an HRESULT, with the stdcall calling convention; the return value must be S_OK (zero) for success, E_UNEXPECTED for unknown errors, or any specific error or success code for a known problem; and optionally extra detail for a failure code may be reported through the IErrorInfo interface.

Two of those points have consequences for the report side. Because a COM object may be loaded into any host - with or without EurekaLog - each COM server project "must be standalone", that is, it must carry its own tracer; if several COM objects are loaded, several tracers are active at once. And because the exception is converted at the boundary and re-raised by the caller, each conceptual failure is represented by two different exceptions - one in the server and one in the caller - so a failure that both sides report costs two bug reports and two dialogs. Hence the recommendation: for COM server projects, switch the dialog to "None" and keep the visual dialog for the host. The server writes its report file without showing or sending it, and the caller's own report carries the user-visible message; a caller that knows about the tracing in the server can attach the server's report file to its own. The chain can be longer than two steps - "exception -> HRESULT -> exception" repeats once per COM boundary crossed

  • and there is no standard COM facility to carry a call stack from callee to caller, so the original exception's information cannot be handed over as such.

The settings the documentation lists for a COM server project: profile "Standalone DLL"; optionally the framework hooks, if the object uses VCL, CLX or FireMonkey forms; "Capture stack only for exceptions from current module" enabled, because exceptions in COM objects are handled in the same module, even in the same method; dialog switched to "None"; and, for a multi-threaded object, "Default EurekaLog state in new threads" set to "Enabled for RTL threads, disabled for Windows threads", or EurekaLog enabled manually per thread. With the framework support in the object - the path through ComObj and TComObject - the list adds "Handle every SafeCall exception", the option that turns SafeCallException failures into something EurekaLog processes with no try/except block and no manual call, optionally "Fix TObject.SafeCallException for hardware exceptions", and at least one of "Use low-level hooks", "Enable extended memory manager", or Delphi 2009 or above. "Catch handled exceptions" is not required for this: safecall exceptions are counted as handled exceptions, so that option adds nothing here; telling one kind of failure from another is a job for the event handlers of chapter 7. One caveat for the manual case: if the COM server does not use the ComObj unit at all, "Handle every SafeCall exception" has no effect, and EurekaLog still has to be invoked from your own SafeCallException override.

16.5 Exception2HRESULT

Everything above converges on one documented function, the only routine EAppDLL publishes.

function Exception2HRESULT(const AExceptObject: TObject; const AExceptAddr: Pointer;
  const AClassName: String = ''; const AInvokeEurekaLog: Boolean = True;
  const ASafecall: Boolean = True): HRESULT;   // EAppDLL

Call this inside the DLL's except block with the exception object and address. It returns the HRESULT to return to the caller. The optional defaults are '', True, True. Nondefault flag combinations require checking the installed implementation rather than assuming a release-independent behavior.

Version-local clarification — implementation 7.15.0.0. In this version, the flags work as follows; these details are not a promise for every 7.x release:

  • Conversion runs through the internal cross-module helper regardless of AInvokeEurekaLog.
  • AInvokeEurekaLog=True then calls the host manager's handling callback. False skips that call, not all bookkeeping and not the conversion itself.
  • ASafecall selects the safecall marker in the manager's deletion/bookkeeping call and is forwarded to the handling callback. It is not simply another spelling of “show a dialog”.
  • A nonempty AClassName is appended to the uppercased DLL base name to form the COM error source/ProgID string. It does not replace the actual exception class.

Keep the defaults for the documented lightweight-DLL usage. In particular, do not prepend a second manual HandleException call on the assumption that this helper only converts. Whether handling ultimately saves or sends a report still depends on the host and its options.

The DLL guidance describes the richer conversion as carrying exception class, message and help context through IErrorInfo, rather than returning only a generic E_FAIL.

Its companion on the other kind of boundary is GetExceptionErrorCode (EBase, chapter 10), which returns a Cardinal for the SetLastError and exit-code style of reporting. Projects usually want one place where the mapping is decided, so it is called through a converter of your own - and because the function lives in EAppDLL, this is the DLL that carries no EurekaLog code of its own (section 16.1):

uses
  Windows, SysUtils, EAppDLL;

function ConvertExceptionToHRESULT(E: Exception): HRESULT;
begin
  Result := Exception2HRESULT(E, ExceptAddr);
end;

This helper belongs in a lightweight DLL and is called while handling the exception, not later after its object or address has gone out of scope. The wrapper is optional; it merely centralizes the conversion. Do not include EAppDLL in a standalone DLL solely to reuse this helper.

For a separately explicit report-then-convert path usable in either profile, use HandleException followed by the RTL's HandleSafeCallException, as below. This avoids depending on undocumented nondefault Exception2HRESULT flag combinations.

16.6 A report as well as an error code

The host decides whether a failed DLL call is fatal, recoverable or worth retrying. A DLL that shows a dialog before returning its error code may therefore produce a duplicate message, or alarm the user about an operation the host successfully recovers. Prefer a silent DLL-side report and let the host own the user interaction.

Returning a stack in the error message is not a full report. It can help a host that has no tracer, but it lacks the report's environment and diagnostic sections. Passing E + E.StackTrace to HandleSafeCallException is not valid Delphi: an exception object is not a string. Use the explicit file-reporting route below when you need a reproducible, self-contained integration.

Write the report file silently, then convert. Configure Save bug report to file, a writable output folder, dialog None, and no sending in the module that owns the tracer. In a standalone DLL these are the DLL's options; in a lightweight DLL they are the host's. Then add this helper to the DLL's implementation:

uses
  Windows, SysUtils, ActiveX, ComObj, EBase;

function ReportExceptionAsHRESULT(E: Exception): HRESULT;
begin
  HandleException(E);
  Result := HandleSafeCallException(E, ExceptAddr, GUID_NULL, '', '');
end;

The last two arguments are empty strings for ProgID/source and help-file information. Do not pass GUID_NULL in the ProgID position: that parameter requires a string.

Call it inside an exported operation's exception handler:

except
  on E: Exception do
    Result := ReportExceptionAsHRESULT(E);
end;

That last fragment replaces the except block of an HRESULT-returning export such as section 16.3. The successful path must still assign S_OK. This is not a request to let a Delphi exception escape the DLL: report locally, convert locally, then let the host decide what the error means. After a controlled failure, check both the failed HRESULT and the saved report while the DLL is still loaded. Chapter 4 explains how to inspect the resulting file.

With the dialog set to "None" and no sending configured, this produces a report for each exception leaving the DLL, in the output folder of whichever tracer made it. The documentation then splits by host: a host without EurekaLog cannot automatically show or send those files, but they are there for a developer to open by hand; a host with EurekaLog can attach them to its own report as additional files. In the single-tracer scheme - host has the tracer, DLLs are lightweight - both use the same tracer and the same options, so all reports land in the same file; that file must be allowed to hold more than one report ("Max. reports in one file" set higher than 1), and "Send entire bug report file with multiple reports" is the option that makes those extra reports leave the machine. The documentation marks this combination "Recommended". In the multiple-tracer scheme - host and DLLs each with their own tracer - the documented trick is to pass the name of the report file as the "help file name" of the COM error, so the caller can find the file, show it, or attach it; in a standalone reporting module the name comes from ExceptionManager.Info(E, ExceptAddr).Options.OutputLogFile(''). Do not use that expression in a lightweight DLL: it would require the forbidden exception-manager unit. The multiple-tracer arrangement costs another tracer per module and is not the preferred design when you own the host.

If the host needs interactive reporting but the DLL must remain silent for a particular exception, do not temporarily flip global options during the failure: the exception already owns a copy. Use a host-side handler that adjusts that exception's Options (sections 17.4 and 24.3). Chapter 3 covers attachments and delivery; do not automatically disclose a server-side report path to untrusted callers merely because the COM error has a place to store it.

16.7 When the tracer is not the only thing that varies

Two further variations are documented, and both come back to the same conclusion: one tracer in the process, in the host. DLLs post-processed by other tools are supported for migration - the host must be EurekaLog-enabled with support for the format in question, the DLLs may mix lightweight ones with DLLs processed by JCL, madExcept and the like, and the compatibility is stated precisely: a lightweight DLL is interchangeable with a DLL carrying third-party debug information and no working tracer, while the "Standalone DLL" profile is not compatible with third-party exception tracers. Nothing is converted in this case: the documentation calls conversion from a post-processor's format neither supported nor useful, because those formats are close enough to each other to be read as they are.

DLLs from other compilers fit the same scheme as long as they can emit debug information in a format EurekaLog supports, and the documentation gives three cases in order of preference. The first converts some compiler formats into EurekaLog's own, by post-processing the DLL with EurekaLog's command-line tool and an external configuration without recompiling it. Use the command-line workflow in chapter 2; do not infer an ecc32 command from a list of filenames. With supported injected information, the DLL does not need its original symbol sidecar for those frames. A .map file from a compiler other than Delphi or C++Builder is not a supported substitute. The second route keeps the third-party format and enables its provider; this is where deployment begins: the .pdb file has to sit next to the DLL, and a DbgHelp.dll from Microsoft's Debugging Tools or from EurekaLog's own installation is required ("Default DbgHelp.dll from C:\Windows is not suitable for such usage") - both requirements are marked as belonging to the "Microsoft Dbg/PDB" provider, while most other formats can be injected into the module and bring no helper DLL with them. That provider also needs its symbol sources, an IDE setup rather than a code one, and it comes at the documented cost that symbol downloads slow the building of call stacks and reports, especially with an empty cache - which is why the documentation places it on developer machines and says to disable hang detection while it is in use. The third case is the DLL exports provider - the default configuration - which reads only exported function names and may yield entries such as (possible fnMSSample+132): a byte offset from the export's start, and a guess rather than an identification, because a JMP or RET instruction between the two addresses means the address may just as well belong to an internal function past the export, and the larger the offset, the likelier that is.

Two settings govern the whole of it, not one. Which debug information is read is decided by the provider options above; whether an exception from another module is captured at all is decided by the option from section 16.3 - "Capture stack only for exceptions from current module", off when exceptions may cross modules, with chained exceptions set off as well, and neither change needed when exceptions do not escape the DLL.