12 December, 2012

Working with exceptions in DLLs

This article will describe pitfalls and "gotcha"s for using and handling exceptions in DLLs. It will also discuss using exception tracer tools in DLLs.

Important Note: please see most recent version of this documentation here.

Content:
Important Note: please see most recent version of this documentation here.

Note: this article will use EurekaLog 7.0.4.3 or higher as exception tracer.

What is DLL?

DLL is a library of functions. Application (.exe file) can load DLL (.dll file) and call a function from it. DLL is isolated file, not related to application. Therefore, DLL and application can be written in different programming languages.

Application is often called a "host application" or just "host" . Both DLL and host are also called "executable modules" or just "modules". Sometimes "application" is referred to host with loaded DLLs (not just host itself).

DLL and application need to understand each other. So, a set of rules must be established to communicate. Such set of rules is called "API".


API can be developed by you or some other developers/company. If you're the API developer - then you can decide how DLL will work with host. If you're not the API developer - then you can only follow already established rules, but you can't invent your own rules.

What is exception?

Exception is represented by an object (class instance) in most modern high-level programming languages. This means that exceptions can be inherited from base classes, as well as be extended with arbitrary properties.

Since exception is a way to interrupt normal execution path of a code - it requires support from hardware level. Modern CPUs provides such support. However, user-mode applications do not have direct access to the hardware. Therefore, operating system provides method to use exceptions on particular hardware. This is called SEH ("Structured Exception Handling") in Windows.

Exception on operating system level is represented by its address, code, options ("flags") and up to 15 4-byte integers ("params"). High-level programming languages use SEH and this low-level representation as basis for their own exception handling. For example, exception in high-level programming language (i.e. exception object) is implemented as OS exception with special code (for example: $EEECFADE for Delphi) and pointer to object is stored in exception params. Exceptions with other codes are wrapped in generic class (EExternalException for Delphi).

Short conclusion:
  1. There are 3 levels of exceptions support: hardware, OS, and programming language.
  2. User-mode code has access to OS and language levels.
  3. OS exceptions are compatible among all programming languages.
  4. Language exceptions are specific to programming language and could not be properly used in another programming language.

How DLLs report about failures?

Remember that object and class implementations are specific for programming language and compiler. I.e. a Delphi application doesn't know how to work with objects/classes from (for example) Microsoft C++ (and visa versa). This means that hi-level exception raised in DLL could not be properly handled by host application, unless both DLL and host are compiled by the same compiler and exception class uses the virtual destructor.

Also note that mixing OS and language exceptions within same module is confusing/problematic thing.

Therefore, APIs for DLLs usually do not use exceptions as a way to report errors. Instead: functions can use error codes - such as numeric codes, success/failure flags (booleans) and so on. There are de facto standard ways to report errors - provided by operating system (for example: GetLastResult, HRESULT - on Windows). However, 3rd party DLLs may use arbitrary error reporting method.

What is the proper way to handle exceptions in DLL?

As you should already understood by now: rule #1 when working with exceptions in DLLs is "never let exception escape DLL". All exceptions in DLL functions must be captured and handled by translating them to error code or other error signature as required by DLL API.

How this should be done? That highly depends on what your DLL API is. This also depends on what framework you do use. There are 3 possible cases:
  1. You develop DLL by using a framework.
    For example: you write a control panel applet by using VCL. Or you write ISAPI module by using IntraWeb.
  2. You develop DLL for already established API without using a ready framework.
    For example: you write a plugin for 3rd party application (like Total Commander). Or you write a global system hook (which requires DLL).
  3. You develop both DLL and API specification.
    For example: you write your own DLL to be used by different applications.

Case 1: Framework

This is the simplest case - because all pitfalls are already handled by a framework. All your code is called by the framework. All exception from your code are handled by the framework. Framework handles exceptions and convert them to something (what is required by the API).


In this case you can just write your code as you usually do. Framework will provide a default handling and error reporting. Some frameworks also allow you to alter default handling (useful for customizations). You should refer to the documentation of your framework if you want to do such customizations. Usually, there is some sort of global Application.OnException event, which you may assign to your handler's code.

Case 2: 3rd party API

This case is a more complex. Basically, you need to study the API and figure out how you should report about errors in your function. You can not use arbitrary nor default way - because API is already established by someone. It's not you who develop API. You only develop a DLL.

Let's consider a little example. Suppose that you want to write a global system hook - the one that is installed via SetWindowsHookEx function. Global hook requires you to place your handler code inside DLL, so that DLL can be injected in all running programs (which makes the hook a global one).

Naturally, API (i.e. communication rules between OS and your code) is already established - it's defined by Microsoft (as a developer of hooking functions). Therefore, the first thing that you should do is to study documentation for the functions. You pass a pointer to your handler's code via second argument in SetWindowsHookEx function (lpfn param). Prototype of the handler depends on what kind of hook do you want to use. Let's use WH_GETMESSAGE hook for this example. This means that we must study description of GetMsgProc callback.

The important part for error handling looks like this:
If code is less than zero, the hook procedure must return the value returned by CallNextHookEx.

If code is greater than or equal to zero, it is highly recommended that you call CallNextHookEx and return the value it returns; otherwise, other applications that have installed WH_GETMESSAGE hooks will not receive hook notifications and may behave incorrectly as a result. If the hook procedure does not call CallNextHookEx, the return value should be zero.
In other words, your code could not report any failure reason. All that you can do is either return 0 or return whatever CallNextHookEx returns.

Therefore, your DLL code must looks at least like this:
library Project2;

uses
  Windows;

function MyHook(Code: Integer; _wParam: WPARAM; _lParam: LPARAM): LRESULT; stdcall;
begin
  try
    if Code >= 0 then
    begin
      // <- your code
    end;
  except
    // There is no way to report errors, so we must handle all exceptions
  end;
  Result := CallNextHookEx(Code, _wParam, _lParam);
end;  
Usually it's not a good idea to silently hide all exceptions. If API doesn't allow you to report about errors - then you should at least implement some kind of logging, so you can store information about exception in the log.

Let's see another example. Suppose you're writing a control panel applet without using any framework. This means that you must write and register DLL. DLL must export CPlApplet function. This function will be used for all communication between OS and your code. Description of CPlApplet says:
The return value depends on the message.
For more information, see the descriptions of the individual Control Panel messages.
This means that you also must study each message from the system that you want to process. Luckily, most messages require you to handle errors in the same way:
If the CPlApplet function processes this message successfully, the return value is zero; otherwise, it is nonzero.
So, you should write your DLL at least like this:
library Project2;

uses
  Windows;

function CPlApplet(hwndCPl: HWND; uMsg: UINT; lParam1, lParam2: LPARAM): LongInt; stdcall;
begin
  try
    case uMsg of
      ...
    end;

    Result := 0;
  except
    Result := 1;
  end;
end;

exports
  CPlApplet;

end.
Since you can't report what is actual report is - a good idea would be to report error to the user. We can safely do this because control panel applet is a single interactive GUI application. Showing error as dialog box is not a good idea for non-interactive applications (such as services) or code that may be used multiply times (such as global hook).
library Project2;

uses
  Windows;

function CPlApplet(hwndCPl: HWND; uMsg: UINT; lParam1, lParam2: LPARAM): LongInt; stdcall;
begin
  try
    case uMsg of
      ...
    end;

    Result := 0;
  except
    on E: Exception do
    begin
      MessageBox(hwndCPl, PChar(E.Message), 'Error', MB_OK or MB_ICONERROR);
      Result := 1;
    end;
  end;
end;

exports
  CPlApplet;

end.
Please note that code above is just example. Not all messages to control panel applet have the same requirements. You should study description of each message that you're going to handle in your code. For example, CPL_INIT message has different requirements:
If initialization succeeds, the CPlApplet function should return nonzero. Otherwise, it should return zero.
If CPlApplet returns zero, the controlling application ends communication and releases the DLL containing the Control Panel application.
Therefore, you need to use such code to handle CPL_INIT message:
library Project2;

uses
  Windows;

function CPlApplet(hwndCPl: HWND; uMsg: UINT; lParam1, lParam2: LPARAM): LongInt; stdcall;
var
  SuccessCode, FailureCode: LongInt;
begin
  // "If initialization succeeds, the CPlApplet function should return nonzero. Otherwise, it should return zero."
  if uMsg = CPL_INIT then
  begin
    SuccessCode := 1;
    FailureCode := 0;
  end
  else
  // "If the CPlApplet function processes this message successfully, the return value is zero; otherwise, it is nonzero."
  begin
    SuccessCode := 0;
    FailureCode := 1;
  end;

  try
    case uMsg of
      ...
    end;

    Result := SuccessCode;
  except
    on E: Exception do
    begin
      MessageBox(hwndCPl, PChar(E.Message), 'Error', MB_OK or MB_ICONERROR);
      Result := FailureCode;
    end;
  end;
end;

exports
  CPlApplet;

end.
Next example would be a Shell extension. Shell extensions are implemented as COM objects. That means that you need to write and register a DLL, which follows COM rules. A COM rule for error handling is to use HRESULT as return value of any method. There are two ways to work with HRESULT. First one is quite direct: you write a function/method that returns HRESULT and you convert each exception to HRESULT value:
...

function ConvertExceptionToHRESULT(const E: Exception): HRESULT;
begin
  Result := E_FAIL; // <- this is just a simple example
  // See HandleSafeCallException function from ComObj unit to see more complicated example
end;

type
  ICopyHook = interface(IUnknown)
  ['{000214FC-0000-0000-C000-000000000046}']
    function CopyCallback(Wnd: HWND; wFunc, wFlags: UINT; pszSrcFile: PWideChar;
      dwSrcAttribs: DWORD; pszDestFile: PWideChar; dwDestAttribs: DWORD): HRESULT; stdcall;
  end;

  TMyHook = class(TInterfacedObject, ICopyHook)
  protected
    function CopyCallback(Wnd: HWND; wFunc, wFlags: UINT; pszSrcFile: PWideChar;
      dwSrcAttribs: DWORD; pszDestFile: PWideChar; dwDestAttribs: DWORD): HRESULT; stdcall;
  end;

function TMyHook.CopyCallback(Wnd: HWND; wFunc, wFlags: UINT; pszSrcFile: PWideChar;
      dwSrcAttribs: DWORD; pszDestFile: PWideChar; dwDestAttribs: DWORD): HRESULT; stdcall;
begin
  try 
    // your code

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

function DllCanUnloadNow: HRESULT; stdcall;
begin
  try 
    if { it's OK to unload DLL } then
      Result := S_OK
    else
      Result := S_FALSE;
  except
    on E: Exception do
      Result := ConvertExceptionToHRESULT(E);
  end; 
end;

...
The second way is to use Delphi wrapper for HRESULT. Delphi compiler provides assisting for HRESULT returning methods via safecall keyword. Any function like this:
function Funcensten1(... some arguments ...): HRESULT; stdcall;
function Funcensten2(... some arguments ...; out AResult: TSomeType): HRESULT; stdcall;
has the same protype and the same calling convention as such function:
procedure Funcensten1(... some arguments ...); safecall;
function Funcensten2(... some arguments ...): TSomeType; safecall;
In other words, the above code fragments are binary compatible with each other. So, for example, DLL may use first code block and host may use second code block - and both will work correctly.

The difference between HRESULT/stdcall and safecall headers is assisting from Delphi compiler. Each safecall function and method automatically handles all exceptions within itself. Moreover, each call to safecall function/method automatically converts HRESULT return value back to exception.

So, the second way to work with HRESULT is:
...

type
  ICopyHook = interface(IUnknown)
  ['{000214FC-0000-0000-C000-000000000046}']
    procedure CopyCallback(Wnd: HWND; wFunc, wFlags: UINT; pszSrcFile: PWideChar;
      dwSrcAttribs: DWORD; pszDestFile: PWideChar; dwDestAttribs: DWORD); safecall;
  end;

  TMyHook = class(TInterfacedObject, ICopyHook)
  protected
    procedure CopyCallback(Wnd: HWND; wFunc, wFlags: UINT; pszSrcFile: PWideChar;
      dwSrcAttribs: DWORD; pszDestFile: PWideChar; dwDestAttribs: DWORD); safecall;
  end;

procedure TMyHook.CopyCallback(Wnd: HWND; wFunc, wFlags: UINT; pszSrcFile: PWideChar;
      dwSrcAttribs: DWORD; pszDestFile: PWideChar; dwDestAttribs: DWORD); safecall;
begin
  // your code
end;

function DllCanUnloadNow: HRESULT; stdcall;
// Unfortunately, it's not possible to customize return code to be S_FALSE for simple function.
// Otherwise DllCanUnloadNow could have been written like this:
//  procedure DllCanUnloadNow; safecall; 
begin
  try 
    if { it's OK to unload DLL } then
      Result := S_OK
    else
      Result := S_FALSE;
  except
    on E: Exception do
      Result := ConvertExceptionToHRESULT(E);
  end; 
end;

...
Converting exception to HRESULT value will be done automatically by Delphi's RTL code.

Notes:
  • Barebone converting to HRESULT may be insufficient for your needs. It's possible to customize it by overriding SafeCallException method. See Delphi help for more information.
  • COM also allow you to ship additional information with exception. See SetErrorInfo function.

Case 3: your own API

When you want to develop a new DLL which will be used by many applications ("common DLL") or if you want to write an application which may be extended with 3rd party DLLs ("plugins") - then you need to develop API, i.e. set of rules which will be used to communications between host and DLLs.

It's a good idea to provide an informative and easy way to report and handle errors. An easy solution would be to use COM. That's because COM is relatively modern API, which provides a decent way to work with errors. COM also has support in many frameworks.

If you think that COM is an "overkill" for your application, then you have to develop your own API. It would be a good idea to use HRESULT as base of error handling part in your API. That's because HRESULT offers a good range of possible error values, it has additional support in Delphi (via safecall) and it's familiar for many Windows developers.

So, functions from your DLL may looks like this:
library Project2;

uses
  Windows;

procedure Init; safecall; 
// the same as:
// function Init: HRESULT; stdcall; 
begin
  // your code
end;

function DoSomething(A1: Integer; const A2: WideString): Integer; safecall;
// the same as:
// function DoSomething(A1: Integer; const A2: WideString; our AResult: Integer): HRESULT; stdcall; 
begin
  // your code
end;

procedure Done; safecall; 
// the same as:
// function Done: HRESULT; stdcall; 
begin
  // your code
end;

exports
  Init, DoSomething, Done;

end;

Note: it's also a good idea to use interfaces instead of simple functions in your DLLs. Interfaces allow you to customize safecall handling by overriding SafeCallException method. Interfaces also allow you to simplify memory management and avoid using shared memory manager.

Using exception tracer tool in DLLs

Many developers prefer to use exception tracer tool in their DLLs. Exception tracer collects information about problems in your code, allowing you to diagnose failures more easily.

Remember what exception tracer does to your application:

A typical executable module with exception tracer (EurekaLog is used as example)

Exception tracer includes its code in your module. It also injects some data - debug information and options. Both (code and data) are required for exception tracer to function.

When you have more than just single executable module - things become interesting. Exception tracer could be inserted into one module or into each module:
  1. There is single instance of exception tracer in application
  2. Each module has its own exception tracer code

First case is good when you can afford enabling exception tracer in host application. Centralized management will allow you to reduce performance cost when you have many DLLs. For example, consider application with 50 DLLs (keep in mind "plugins" scenario). Each exception must be analyzed by exception tracer. If each DLL has its own exception tracer - then each exception will be analyzed 50 times. A good idea would be to have only one instance of exception tracer, so information is collected only once. Any DLL can ask central exception tracer for information about exception.
Second case is good when host application is out of your control. Since you can not use exception tracer in host - then the only choice left is to add it to DLL. Each DLL will have its own isolated exception tracer.

Now, let's examine both cases on practice.

Note: this article will use EurekaLog 7.0.4.3 or higher as exception tracer.

Case 1: Single instance of exception tracer: "DLL" profile

This case require you to enable exception tracer for host application. You should do this in the same way as you do it for typical application without any DLLs. For example, if you have VCL Forms application as the host - then you need to enable EurekaLog for host application and set application type to "VCL Forms Application". This will add EurekaLog code and data into final .exe file. It would also set hook for Forms.TApplication.HandleException method, which will allow to automatically handle exceptions without writing any code.

Now, the host has exception tracer. It will catch any exception regardless of where exception was raised. It will also collect information about each exception. Exceptions from DLL will be also analyzed by tracer in the host.

Each DLL must also has EurekaLog enabled and application type must be set to "DLL". Such settings will inject debug information into DLL, but will not include exception tracer code. Rather DLL will ask host application for information. Please note that majority of EurekaLog settings will be ignored, since there will be no EurekaLog code in your DLL.

Note: it's not strictly necessary to enable EurekaLog for DLLs in this example. You can just supply debug information and keep EurekaLog off. For example, you may create .map file or use JCL debug information. This will work fine.

Host application loads multiply DLLs with "DLL" profile
(click image to enlarge/zoom in)

Let's see this on practice. Create a new VCL application and place buttons to invoke functions from DLL.
...

type
  TForm1 = class(TForm)
    Button1: TButton;
    Button2: TButton;
    Button3: TButton;
    procedure Button1Click(Sender: TObject);
    procedure Button2Click(Sender: TObject);
    procedure FormCreate(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
    procedure Button3Click(Sender: TObject);
  private
    FDLL: HMODULE;
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

procedure TForm1.FormCreate(Sender: TObject);
begin
  FDLL := LoadLibrary('Project2.dll');
  Win32Check(FDLL <> 0);
end;

procedure TForm1.FormDestroy(Sender: TObject);
begin
  FreeLibrary(FDLL);
  FDLL := 0;
end;

procedure TForm1.Button1Click(Sender: TObject);
begin
  raise Exception.Create('Error Message');
end;

procedure TForm1.Button2Click(Sender: TObject);
var
  P: procedure;
begin
  P := GetProcAddress(FDLL, 'Test1');
  Win32Check(Assigned(P));
  P;
end;

procedure TForm1.Button3Click(Sender: TObject);
var
  P: procedure;
begin
  P := GetProcAddress(FDLL, 'Test2');
  Win32Check(Assigned(P));
  P;
end;

...
Code is pretty simple: we load DLL on form's creating and unload it when form is gone. There are 3 buttons on the form to invoke different testing routines. First button raises exception in application itself. The 2nd and 3rd buttons raise exceptions in DLL.

Don't forget to enable EurekaLog for host and set application type to "VCL Forms Application". That's all.

Now, create a new DLL project:
library Project2;

uses
  // Added automatically
  EAppDLL, // "DLL" profile

  // Added manually
  Windows,
  SysUtils,
  Classes;

{$R *.res}

procedure Test1;
begin
  // This is just example! It's highly not recommended to do this.
  raise Exception.Create('Error Message');
end;

procedure Test2;
var
  Msg: String;
begin
  // Recommended way: handle exceptions, do not let them escape DLL
  try
    raise Exception.Create('Error Message');
  except
    on E: Exception do
    begin
      Msg := E.ToString + sLineBreak + E.StackTrace;
      MessageBox(0, PChar(Msg), 'Error', MB_OK or MB_ICONSTOP);
    end;
  end;
end;

exports
  Test1, 
  Test2;

end.
This simple DLL exports 2 functions to test exceptions. First function raises exception and lets it escape DLL, so it will be catched by caller. In our test example caller would be the host application. Such approach is not recommended - as it's already explained above: you should never let exceptions escape DLL. This is done only for example (illustration). It will work correctly for our case, because DLL and exe are both compiled by the same compiler. This will not work properly for generic case. So, it's only suitable for testing.

Note: if you want to raise exception in DLL and catch it by the caller - then do not use DLLs. Use packages instead. Using packages will guarantee compatible compiler for host and DLL (package). It's also generally less problematic with all cross-modules communications.

Second function illustrate more correct approach: we catch exceptions in function and handle them somehow. For this example we will do very simple handling: just display error message with stack trace. More proper approach was discussed above: you should use some kind of error indication (such as boolean flag, HRESULT, etc.) to indicate failure condition to the caller.

Now, enable EurekaLog for this DLL and set application type to "DLL".

Note: EAppDLL unit will be added automatically when you set profile to "DLL".

Compile both host and DLL project, run host application.

Hit buttons 1-3.

Typical exception in host application
Call stack shows only items for exe
(click image to enlarge/zoom in)

Exception escaped DLL
Call stack shows mixed exe/DLL lines
Notice line numbers for routine in DLL
(click image to enlarge/zoom in)

Exception did not escape DLL, it was handled by DLL by displaying error message
(screenshot was cut to save space)
Call stack shows mixed exe/DLL lines
Notice line numbers for routine in DLL
(click image to enlarge/zoom in)

Please note that last case was a simple example of trivial exception handling in DLL. You may be not satisfied with looks of error dialog. You may want not just "error message", but complete bug report. To do this - you need to replace the call to MessageBox with a call to exception manager. Normally, it would be ExceptionManager.Handle. However, there is no exception manager in our DLL. We'll show how to do this below (see "Working with frameworks and exception tracers in DLLs" section).

Case 2: Multiply instances of exception tracer: "Standalone DLL" profile

This case does not require you to enable exception tracer for host application. You can do it, but it's not required. Typically this approach should be used only when you develop DLLs to be used in non-EurekaLog enabled host. If you have EurekaLog enable for the host - please try to implement case 1 approach above.

Since host application do not necessary have exception tracer - you must to include tracer in each of your DLLs. Each DLL will have exception tracer. All tracers and DLLs will be independent of each other. Each exception will be catched by each exception tracer in each DLL.

Therefore, each DLL must has EurekaLog enabled and application type must be set to "Standalone DLL". Such settings will add exception tracer in DLL and inject debug information.

Host application loads multiply DLLs with "Standalone DLL" profile
(click image to enlarge/zoom in)

Let's see this on practice. We'll use the same host application for this example. Of course, it has EurekaLog enabled, but remember that it's not necessary. You may turn EurekaLog off for host application, if you want. Actually, let's do this for the sake of better illustration. So, open your host application project, disable EurekaLog for it and recompile (all source code will remain the same as above).

We'll use the same DLL project for this example. We'll make only few changes. Open DLL project and change application type from "DLL" to "Standalone DLL". This will also replace EAppType unit in uses clause with multiply EurekaLog units. Also, go to dialogs options and change "None" to any dialog that you like. We will use "MS Classic" for this example.

This could be all, but since now we have full exception tracer on board - why not ask it to handle exceptions? We can replace our old MessageBox with a call to exception manager. So full changed code will looks like:
library Project2;

uses
  // Automatically generated:
  EMemLeaks, 
  EResLeaks,
  EDialogWinAPIMSClassic,
  EDialogWinAPIEurekaLogDetailed,
  EDialogWinAPIStepsToReproduce,
  EDebugExports,
  EDebugJCL,
  ExceptionLog7,

  // Added manually:
  EExceptionManager,
  Windows,
  SysUtils,
  Classes;

{$R *.res}

procedure Test1;
begin
  raise Exception.Create('Error Message');
end;

procedure Test2;
begin
  try
    raise Exception.Create('Error Message');
  except
    ExceptionManager.ShowLastExceptionData;
  end;
end;

exports
  Test1, Test2;

end.
Save all and recompile. Run application and hit all 3 buttons:

Typical exception in host application
There is no bug report, since host application do not have exception tracer


Exception escaped DLL
There is no bug report, since exception was caught by host application (without exception tracer)


Exception did not escape DLL, it was handled by DLL by displaying complete bug report
Call stack shows only lines within DLL
There is no information about exe, because exe do not have any debug infomation
(click image to enlarge/zoom in)


This example gives you full EurekaLog support within DLL, but exe completely lacks any support. It doesn't even have debug information, so even exception tracer from DLL is unable to display call stack for exe. Of course, this can be fixed by enabling EurekaLog for exe. Just remember that host application is not always under your control.

Working with frameworks and exception tracers in DLLs

The above sections assumed that you write DLLs without using any frameworks. If you use framework (such as VCL or IntraWeb), then your actions will be slightly different. That's because framework already contain some sort of exception handling code.

A general concept would be the same. You can use either "DLL" or "Standalone DLL" profiles for your DLLs. So the above facts would be the same. Additionally, you have to configure DLLs for your framework. EurekaLog has support for common cases out of the box. For example, if you use Forms unit in your DLL (i.e. your DLL has forms) - then you need to hook Application.HandleException method. This can be done by enabling "VCL Forms application" option on Advanced/Code/Hooks page in EurekaLog project options. This is true for both "DLL" and "Standalone DLL" profiles.

Note: "Standalone DLL" profile with "VCL Forms application" option on Hooks page is equal to "VCL Forms Application" profile. When you enable such options - DLL profile will be switched to "VCL Forms Application" profile. This is normal behavior. After all, a profile is just set of predefined options. If you change options to match another profile - it will be shown as used. There is no build-in profile for "DLL" profile with "VCL Forms application" option, so "DLL" profile will not be changed after enabling option.

Now, let's change our example to illustrate this on practice. As usual, host application will remain unchanged. All changes will be done for DLL.

Case 1: Single instance of exception tracer: "DLL" profile

Open DLL, enable EurekaLog and set application type to "DLL". Since we're going to use forms in our DLL - go do Advanced/Code/Hooks page in EurekaLog project options and enable "VCL Forms application" option.

Now, create a new form for DLL, place a button to raise exception:
...

type
  TForm2 = class(TForm)
    Button1: TButton;
    procedure Button1Click(Sender: TObject);
  end;

implementation

{$R *.dfm}

procedure TForm2.Button1Click(Sender: TObject);
begin
  raise Exception.Create('Error Message');
end;

...
And change DLL code as:
library Project2;

uses
  // Automatically generated by EurekaLog
  EAppDLL,  // "DLL" profile
  EAppVCL,  // "VCL Forms application" hook

  // Added manually
  EAppType,
  Windows,
  SysUtils,
  Classes,
  Unit2 in 'Unit2.pas' {Form2};

{$R *.res}

procedure Test1;
begin
  try
    raise Exception.Create('Error Message');
  except
    on E: Exception do
      // Ask exception manager in host application to process this exception
      _ExceptionManagerHandle(E, ExceptAddr);
  end;
end;

procedure Test2;
var
  Form: TForm2;
begin
  try
    Form := TForm2.Create(nil);
    try
      Form.ShowModal;
    finally
      FreeAndNil(Form);
    end;
  except
    on E: Exception do
      // Ask exception manager in host application to process this exception
      _ExceptionManagerHandle(E, ExceptAddr);
  end;
end;

exports
  Test1, Test2;

end.
Normally, if you want to ask EurekaLog to process exception (display error dialog with bug report, send it to developer, etc.) - then you have to call ExceptionManager.Handle. However, we can not do this in our case, because we've used "DLL" profile, which means no exception tracer (and no exception manager) in our DLL. That's why we use _ExceptionManagerHandle function instead of ExceptionManager.Handle.

_ExceptionManagerHandle function is a lightweight exception manager. If there is exception tracer code in current module - the function will invoke it (i.e. ExceptionManager.Handle). If there is no tracer in the module - the function will try to invoke exception manager from host application. If there is no tracer in host application either - the function will act as if EurekaLog was disabled.

Therefore, you can use _ExceptionManagerHandle function to handle exceptions when you don't know if there will be EurekaLog in your project. This function will automatically use suitable way to process exceptions.

Okay, so the first function in our DLL will just raise exception in DLL function. The difference with first example is that we handle it properly now: there is try/except block which handles exception by asking exception manager from host application to perform full processing (displaying bug report, sending it to developer, etc.).

Second function will create and show a modal form. There is no exception inside function itself, but form contains button to raise exception. This exception will not be catched by our try/except block, because exceptions in form's event handlers are handled by VCL framework. That's why we need EAppVCL unit (it contains hooks for VCL). Try/except block in second function will catch exceptions only for form's creating or destroying.

That's all. Save all and compile. Run application and hit all buttons. First button is not changed at all. Second button and third button behave differently:

Button #2: Exception did not escape DLL, it was handled by DLL by displaying complete bug report
Call stack shows mixed exe/DLL lines
(click image to enlarge/zoom in)
Button #3: Exception was raised by form. It was handled by VCL.
EurekaLog hook displays full bug report
Call stack shows mixed exe/DLL lines
(click image to enlarge/zoom in)

Case 2: Multiply instances of exception tracer: "Standalone DLL" profile

Open DLL, enable EurekaLog and set application type to "Standalone DLL". Since we're going to use forms in our DLL - go do Advanced/Code/Hooks page in EurekaLog project options and enable "VCL Forms application" option. Also change dialog to "MS Classic" or any other desired type.

Note: a combination of "Standalone DLL" profile + "VCL Forms application" hook will set the same options as "VCL Forms Application" profile. That's why you'll see "VCL Forms Application" instead of "Standalone DLL" in "Application type" option when you open project settings next time. That's totally expected behavior. You can also initially only switch profile to "VCL Forms Application" and do nothing else - that's because this profile will set dialogs and turn on hooks for VCL.

The code for both DLL and exe remain unchanged from previous example. Run application and hit the buttons. You should see the same behavior and dialogs as in previous example.

Note that even if visual appearance seems the same - the internals are working differently now. DLL now has its own exception tracer. _ExceptionManagerHandle function will just invoke ExceptionManager.Handle in DLL. It will not try to communicate with exe host.

Conclusion

This article has described the basics for using EurekaLog in DLLs. It's also useful source of information for learning general concepts of using exceptions in DLLs. Of course, you're not limited to examples shown in this article. You may extend them or use your own combination of settings. For example, you host application may install callbacks for DLL - to allow DLL to request arbitrary services from the host.

1 comment:

You can use some HTML-tags like:

<b>Bold</b>
<i>Italic</i>
<a href="http://www.example.com/">Link</a>