Root > Advanced topics > Using EurekaLog in DLL > What is the proper way to handle exceptions in DLL > Your own API

Your own API

Previous pageReturn to chapter overviewNext page   

 

Important: as a general rule, you should write your application in a such way that it will behave correctly without EurekaLog on board. This means that if your or 3rd party code throws an exception in a DLL - it must be handled in a way that you expect. For example, by showing an error message, canceling action, retrying, etc. Once your application behaves like it is supposed to do - then you add EurekaLog for it. EurekaLog will auto-handle all unhandled exceptions automatically (by hooking few well-known places), and for everything else - you can call EurekaLog from your own handlers when needed. In other words:

Incorrect: your application does not show messages for exceptions (or do other things you want) from DLL and you are adding EurekaLog in hopes to fix this behavior;
Correct: your application correctly handles DLL exceptions and you are adding EurekaLog to receive reports about such exceptions.

 

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.

 

 

Incorrect (not recommended)

 

procedure DoSomething;
begin
  // Your payload
end;
 
exports
  DoSomething;

 

DoSomething function does not catch exceptions, and let exceptions escape to the caller module - which can be written in a different programming language. This is a big NO - unless you compile both DLL and caller in the very same version of the compiler.

 

Note: if you develop DLL that will only be used in executable compiled in exactly the same version of the compiler, and you want to pass exceptions between modules for simplicity - see this article.

 

 

COM - a default solution to design API

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.

 

 

Second best bet - HRESULT via interfaces or functions

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 payload
end;
 
function DoSomething(A1: Integer; const A2: WideString): Integer; safecall;
// the same as:
// function DoSomething(A1: Integer; const A2: WideString; 

//                      out AResult: Integer): HRESULT; stdcall; 
begin
  // Your payload

  Result := { ... };
end;
 
procedure Done; safecall
// the same as:
// function Done: HRESULT; stdcall; 
begin
  // Your payload
end;
 
exports
  Init, DoSomething, Done;
 
end;

 

As an alternative to a "safecall compiler magic" - you may write the same code as this:

 

library Project2;
 
uses
  Windows;
 
function Init: HRESULT; stdcall
// the same as:
// procedure Init; safecall; 
begin
  try
    // Your payload

 
    Result := S_OK;
  except
    on E: Exception do
      Result := ConvertExceptionToHRESULT(E);
  end;
end;
 
function DoSomething(A1: Integer; const A2: WideString; 

                     out AResult: Integer): HRESULT; stdcall
// the same as:
// function DoSomething(A1: Integer; const A2: WideString): Integer; safecall;
begin
  try
    // Your payload
    AResult := { ... };
 
    Result := S_OK;
  except
    on E: Exception do
      Result := ConvertExceptionToHRESULT(E);
  end;
end;
 
function Done: HRESULT; stdcall
// the same as:
// procedure Done; safecall; 
begin
  try
    // Your payload

 
    Result := S_OK;
  except
    on E: Exception do
      Result := ConvertExceptionToHRESULT(E);
  end;
end;
 
exports
  Init, DoSomething, Done;
 
end;

 

Both implementations are binary compatible with each other and do the same thing. The difference is that second implementation allows you to control exception handling - by manually specifying a custom ConvertExceptionToHRESULT function (see below for a sample implementation).

 

 

Using interaces

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. Example:

 

// Place this declaration into a common unit, which will be used by both DLL and caller (exe)
type
  IDLLAPI = interface
  // IMPORTANT: Replace with your own ID 
  // by pressing Ctrl + Shift + G in Delphi IDE's code editor
  ['{FD8708CF-21C9-40BA-A866-DE810C9198FE}'

    // Method below are just example

    // You can replace with your own code
    procedure Init; safecall;
    function DoSomething(A1: Integer; const A2: WideString): Integer; safecall;
    procedure Done; safecall;
  end;

 
// _____________________________________________

 
// Code below is for DLL
type
  TDLLAPI = class(TInterfacedObject, IDLLAPI)
  protected
    procedure Init; safecall;
    function DoSomething(A1: Integer; const A2: WideString): Integer; safecall;
    procedure Done; safecall;
  public
    function SafeCallException(ExceptObject: TObject; ExceptAddr: Pointer): HResult; override;
  end;

 
// _____________________________________________
 
procedure TDLLAPI.Init;
begin
  // Your code here
end;
 
function TDLLAPI.DoSomething(A1: Integer; const A2: WideString): Integer;
begin
  // Your code here
end;
 
procedure TDLLAPI.Done; 
begin
  // Your code here
end;
 

// _____________________________________________

 
function TDLLAPI.SafeCallException(ExceptObject: TObject; ExceptAddr: Pointer): HResult;
begin
  Result := ConvertExceptionToHRESULT(E);
end;
 
var
  GDLLAPI: TDLLAPI;
 
function GetAPI(out AResult: IDLLAPI): HRESULT; stdcall
// the same as:
// procedure GetAPI: IDLLAPI; safecall; 
begin

  AResult := nil;
  try
    if GDLLAPI = nil then
      GDLLAPI := TDLLAPI.Create;
    AResult := GDLLAPI;
 
    Result := S_OK;
  except
    on E: Exception do
      Result := ConvertExceptionToHRESULT(E);
  end;
end;
 
exports
  GetAPI;
 
initialization
finalization
  GDLLAPI := nil;
end;

 

// _____________________________________________

 

// The code below is for application:

 

var
  Lib: HMODULE;
  GetAPI: function: IDLLAPI; safecall;
  DLL: IDLLAPI;
 
Lib := LoadLibrary('YourDLL.dll');
Win32Check(Lib <> 0);
try
  GetAPI := GetProcAddress(Lib, 'GetAPI');
  Win32Check(Assigned(GetAPI));
  DLL := GetAPI;
 
  // Now, you can call DLL
  // Code below is just an example

  // We recommend to wrap this code into sub-routine

  // to finalize hidden auto-managed variables before unloading DLL
  DLL.Init;
  try
    Tag := DLL.DoSomething(42, 'Invoke');
  finally
    DLL.Done;
  end;
 
finally
  DLL := nil;
  FreeLibrary(Lib);
end;

 

 

Using framework(s) within your DLL

You may want to use frameworks inside your DLL. For example, you may want to display dialog with VCL within your DLL function. If this is the case - follow guidelines for frameworks.

 

 

Possible ways to handle exceptions within DLLs

ConvertExceptionToHRESULT in the examples above is some function that you need to write by yourself - which will handle exceptions and converts failure reasons to HRESULT codes. The simplest implementation may look like this:

 

function ConvertExceptionToHRESULT(E: Exception): HRESULT;
begin
  Result := E_FAIL;
end;

 

(The same function is used as default action for TObject.SafeCallException method.)

 

Obviously, such primitive function will ignore any exception's info and just report "something went wrong" to the caller. EurekaLog provides a ready-to-use function Exception2HRESULT which can be found in EAppDLL unit:

 

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

 

This example will additionally pass error class, message, and help context via IErrorInfo interface. Of course, you can simply call Exception2HRESULT directly, there is no need for ConvertExceptionToHRESULT. It is used only as example.

 

If you want to have manual control - a more complex implementation of this function can be found in System.Win.ComObj unit - see HandleSafeCallException function. (This function is used as default action for TComObject.SafeCallException method.)

 

uses
  ComObj;
 
function ConvertExceptionToHRESULT(E: Exception): HRESULT;
begin
  Result := HandleSafeCallException(E, ExceptAddr, GUID_NULL, GUID_NULL, '');
end;

 

This example will pass error message via IErrorInfo interface - same as Exception2HRESULT above. However, this example passes empty values for COM-related information (error ID, source ID and help file name). You can use these values as you want in your own API.

 

 

Creating bug reports for DLL exceptions

Please note that no implementation of ConvertExceptionToHRESULT in the above examples creates bug report for the exception.

 

Creating bug reports for DLL exceptions is not an easy question - because final handling of the exception is not under your control. It is the caller of your DLL who decides what to do with the exceptions from your DLLs. Surely, exception from your DLL may be handled as usual: by showing error message to end user, asking to send bug report to developers, etc. However, exception from your DLL may also be silently handled by the caller and failed action will be repeated. Or the caller may try to execute fallback method with alternative solution (for example, the code that tries to set application for auto-launch may write to HKEY_LOCAL_MACHINE registry key. If this action will fail due to application being run under limited user account - the code may switch to HKEY_CURRENT_USER key, e.g. re-try action with other params).

 

For these reasons you can not simply show error dialog and ask to send bug report - because:

If exception from your DLL will be handled as error by the caller - then the error message will appear twice: first from your DLL as bug report, second - from the caller;
If exception from your DLL will be handled as non-error by the caller (i.e. the caller may re-try action or try alternative solutions) - then your DLL will show "false" error message (user will see error dialog - even though requested action will be completed by the caller).

 

Some possible solutions to this problem are explored in this article.

 

 

See also:




Send feedback... Build date: 2024-09-30
Last edited: 2024-07-08
PRIVACY STATEMENT
The documentation team uses the feedback submitted to improve the EurekaLog documentation. We do not use your e-mail address for any other purpose. We will remove your e-mail address from our system after the issue you are reporting has been resolved. While we are working to resolve this issue, we may send you an e-mail message to request more information about your feedback. After the issues have been addressed, we may send you an email message to let you know that your feedback has been addressed.


Permanent link to this article: https://www.eurekalog.com/help/eurekalog/dll_api.php