Or: don't create your own DLLs without reading this article!
This article is not about EurekaLog, but about writing your own DLLs in general. This article is based on questions on the forums: "How do I return a string from a DLL?", "How do I pass and return an array of records?", "How do I pass a form to a DLL?".
So that you do not spend half your life figuring it out - in this article I will bring everything on a silver platter.
Important note: the article must be read sequentially. Code examples are given only as examples: the code of examples is added with new details at each step of the article. For example, at the very beginning of the article there is no error handling, "classic" methods are used (such as using
GetLastError
, the sdtcall
convention, etc.), which are replaced by more adequate ones in the course of the article. This is done for the reason that "new" ("unusual") designs do not raise questions. Otherwise, with each example, one would have to insert a note like: "this is discussed in that paragraph below, but that - in this one here." In any case, at the end of the article there is a link to the sample code written by taking into account everything said in the article. You can just grab it and use it. And the article explains why it is created the way it is. If you are not interested in "why" - scroll to the end to the conclusion and find a link to download the example.Content
- General Concepts
- Data Types
- String Data and Encodings
- Shared Memory Manager (and why you shouldn't use it)
- API Memory Management
- Error Handling (and calling convention)
- DllMain Workaround
- Callback Functions
- Other Rules
- Conclusion (and code examples)
General Concepts
When you develop your own DLL, you must come up with the prototypes of the functions exported from it (i.e. "headers"), as well as the contract based on them (i.e. calling rules). Together, this forms your DLL's API. API or Application Programming Interface (Application Programming Interface) is a description of the ways in which one code can interact with another. In other words, API is a tool for application integration.When you develop your DLL, you must determine under what conditions it will be used:
- Can it be used by applications written in another programming language (for example, Microsoft Visual C++) - "universal DLL";
- Or the DLL can only be used by applications written in the same language, "Delphi DLL".
Extended
, sets, etc.) - in general, all that does not exist in other languages. It also means the ability to share memory, do transparent error handling (cross-module exceptions).If you go this route, then you should consider using run-time packages (BPLs) instead of DLLs. BPL packages are specialized DLLs that are specifically tailored for use only in Delphi, which gives you a lot of goodies. But more on that later.
On the other hand, if you're developing a "generic DLL" then you can't use features in your language that don't exist in other programming languages. And in this case, you can only use "well-known" data types. But more on that below.
This article is mainly about "generic DLLs" in Windows.
What you will need to create when developing your DLL API:
- Headers, header files - a set of source files that contain declarations of structures and functions used in the API. As a rule, they do not contain implementations. Header files are provided in several languages - as a rule, this is the language in which the program is written (in our case, Delphi), C++ (as a standard) and some additional ones (Basic, etc.). All of these files are equivalent and are simply a translation from one programming language to another. The more languages included, the better. If you do not provide header files for a particular language, programmers in that language will not be able to use your DLL unless they themselves translate the files from the provided language (Delphi or C++) into their language. Therefore, the absence of headings in some language is not a red "stop", but a sufficient obstacle.
- Documentation - is a verbal description of the API and it should specify additional rules not included in the header syntax. For example, the "specific function can be called by passing a number to it" fact is an information from the headers. And "before calling this function you need to call another function" fact is an information from the documentation. Such documentation should at least contain a formal description of the API - a listing of all functions, methods, interfaces and data types with explanations of "how" and "why" (the so-called Reference). Additionally, the documentation may contain an informal description of the process of using the DLL (guide, how-to, etc.). In the simplest cases, documentation is written directly in headers (comments), but most often it is a standalone file (or files) in chm, html or pdf format.
Data Types
If you want a "generic DLL", then you can't use Delphi-specific data types in your API because they don't have a counterpart in other languages. For example,string
, array of
, TObject
, TForm
(and in general - any objects, and even more so components) and so on. What can be used? Integer types (
Integer
, Cardinal
, Int64
, UInt64
, NativeInt
, NativeUInt
, Byte
, Word
etc., I wouldn't recommend using Currency
unless you really need it), real types (Single
and Double
; I would recommend avoiding the Extended
and Comp
types unless you really need them), TDateTime
(it is alias for system's OLEDate
), enumerated and subrange types (with some caveats), character types (AnsiChar
and WideChar
- but not the Char
), strings (only as WideString
/BSTR
), boolean (BOOL
, but not the Boolean
), interfaces (interface
) whose methods use valid types, records (record
) from the above types, and pointers to them (including pointers to arrays of the above types, but not dynamic arrays). Arrays are usually passed as two parameters: a pointer to the first element of the array and the number of elements in the array.How do you know which type can be used and which can't? A relatively simple rule - if you don't see a type in this list, and the type is not in the
Windows
unit (Winapi.Windows
unit since Delphi XE2), then that type cannot be used. If the type is listed above or it is in the Windows
/Winapi.Windows
unit, use it. This is a rather rough rule, but it will do for a start.In case of using records (
record
) - you need to specify data alignment. Use either the packed
keyword (no alignment) or the {$A8} directive (8-byte alignment) at the beginning of the header file.In case of using enumerated types (
Color = (clRed, clBlue, clBlack);
) - add the {$MINENUMSIZE 4}
directive to the beginning of the headers (the size of the enumerated type is at least 4 bytes ).String Data and Encodings
If you need to pass strings to the DLL or return strings from the DLL - use only theBSTR
type. Why?
- The
BSTR
type available in all programming languages.
Note: For historical reasons, theBSTR
type is calledWideString
in Delphi. Therefore, to make the contents of your Delphi headers more understandable to developers in other languages, add the following code to their beginning:
type BSTR = WideString;
and then use theBSTR
everywhere in API/headers. - The
BSTR
(WideString
) type is one of Delphi's automagic types, i.e. you don't have to manually allocate and deallocate memory. The compiler will automatically do everything for you; - The
BSTR
type has a fixed encoding: Unicode. Therefore you won't have problems with a wrong code page; - Delphi compiler magic allows you to simply assign
BSTR
(via the assignment operator:=
) to any Delphi string and vice versa. All necessary conversions will be done automatically under the hood of the language, no conversion functions need to be called; - Memory for
BSTR
strings is always allocated through the same memory manager so you will never have problems transferring memory between executable modules (see below);
If for some reason you can't use the
BSTR
type then use PWideChar
:
- Don't use
PAnsiChar
because it's 2023, not 1995. UsingPAnsiChar
gives you a lot of encoding headaches; - Do not use
PChar
because it is not uniquely defined: it can be eitherPAnsiChar
orPWideChar
(depending on compiler version).
BSTR
name instead of the Delphi's WideString
name: you can also do this for the PWideChar
type:type LPWSTR = PWideChar;and then use
LPWSTR
. The LPWSTR
is the name of the system data type, which is called PWideChar
in Delphi.Of course, you get a bunch of cons when using
LPWSTR
/PWideChar
instead of WideString
:
- You need to manually allocate and deallocate memory for
PWideChar
, which increases the chances of memory leak problems; - While in some cases you can make direct assignments (e.g.
PWideChar
to a string), more often you can't. You will have to call conversion functions and/or memory allocation/copy functions; - Memory for
PWideChar
strings is allocated as usual (without a dedicated memory manager), i.e. you have a problem with passing memory across a module boundary (see below); PWideChar
has no field for length. So if you want to pass strings with#0
inside and/or you want to pass large strings, then you have to explicitly pass the length of the string along with the string (two parameters instead of one).
ANSI and Unicode
From the above, it directly follows that all your exported functions must be in Unicode. Do not just look at the Windows API to make two versions of functions (with -A and -W suffixes) - just make one version (no suffix, just Unicode). Yes, do that even if you are developing on the ANSI version of Delphi (like Delphi 7): you don't need to make ANSI versions of the exported functions. It's not 1995 now.Shared Memory Manager
(and why you shouldn't use it)
In programming languages, dynamic memory is allocated and deallocated by a special code in the program - the so-called memory manager. For example, the memory manager implements functions likeGetMem
and FreeMem
in Delphi. All other memory management methods (New
, SetLength
, TForm.Create
, etc.) are just adapters (i.e. somewhere internally they call GetMem
and FreeMem
).The problem is that each executable module (be it a DLL or an exe) has its own memory manager code, and, for example, the Delphi memory manager does not know anything about the Microsoft C++ memory manager (and vice versa). Therefore, if you allocate memory in Delphi and, for example, try to transfer it to Visual C++ code, then nothing good will happen. Moreover, even if you allocate memory in Delphi DLL and return it to Delphi exe, things will be even worse: both executable modules use two different, but same type memory managers. The exe memory manager will look at the memory and it will seem to him that this is his memory (after all, it is allocated by a similar memory manager), he will try to free it, but only damage the accounting data.
The solution to this problem is simple - you need to use the rule: whoever allocates memory, frees it.
This rule can be enforced in a number of ways. Often mentioned method: using the so-called shared memory manager. The essence of the method is that several modules "agree" to use the same memory manager.
When you create a DLL, you are told about this feature by a comment at the beginning of the .dpr file of the DLL:
{ Important note about DLL memory management: ShareMem must be the first unit in your library's USES clause AND your project's (select Project-View Source) USES clause if your DLL exports any procedures or functions that pass strings as parameters or function results. This applies to all strings passed to and from your DLL--even those that are nested in records and classes. ShareMem is the interface unit to the BORLNDMM.DLL shared memory manager, which must be deployed along with your DLL. To avoid using BORLNDMM.DLL, pass string information using PChar or ShortString parameters. }This is a monstrously wrong comment:
- The comment talks about the need to use a shared memory manager as if it is the only way to solve the memory sharing problem - which is fundamentally wrong (see below);
- The comment only talks about strings, although the problem described applies to any data with dynamic memory allocation: objects, dynamic arrays, pointers;
- The comment does not mention in any way what to do with non-string data;
- Using a shared memory manager does not correlate in any way with using a standalone DLL. This is just one of the possible implementations;
- The comment requires the use of the
PChar
type to avoid the described problem - which is also wrong (see our discussion above about encodings); - The comment requires the use of the
ShortString
- which, again, is wrong from a "generic DLL" point of view (ShortString
is a Delphi-specific type). Although, this is already a nitpick, since the use of Delphi strings and Delphi DLL as a common memory manager already puts an end to the "universal DLL" concept.
What's wrong with using a shared memory manager?
- Other programming languages know nothing about Delphi's memory manager;
- And since you're targeting only Delphi, why do you need a DLL? Build the program with run-time packages (BPL) - this will automatically give you:
- Shared memory manager in
rtl.bpl
; - Guaranteeing the compatibility of the structure of objects, since all modules will be assembled by one compiler;
- No duplication of RTL and VCL (errors like "
TForm
is not compatible withTForm
", twoApplication
objects, etc.); - Easy error handling with exceptions.
- Shared memory manager in
- The shared memory manager makes it very difficult to find memory leaks, because a module can load, allocate memory, unload, and the created leak will only be found during the finalization of the memory manager when the program exits.
API Memory Management
So how do you transfer memory from the DLL to the caller and vice versa? There are several ways.Incorrect Way
First, what not to do.First, don't "do it like Delphi": don't use a shared memory manager - for the reasons mentioned above.
Secondly, don't "do it like Windows": many look at the Windows API and do the same. But they miss the fact that this API was created in 1995, and many functions come from even earlier: 16-bit Windows. Those environments and conditions for which these functions were created no longer exist today. Today there are much simpler and more convenient ways.
For example, here is a typical Windows function:
function GetUserName(lpBuffer: PWideChar; var nSize: DWORD): BOOL; stdcall;
To get a result from such a Windows function, it must be called twice. First you call the function to determine the size of the buffer, then you allocate the buffer, and only then you call the function again. But what if the data changes during this time? Function may run out of space again (on the second call). Thus, to reliably get the complete data, you have to write a loop. This is horror. Don't do that.Parameters
lpBuffer
A pointer to the buffer to receive the user's logon name. If this buffer is not large enough to contain the entire user name, the function fails.
pcbBuffer
On input, this variable specifies the size of the lpBuffer buffer, in TCHARs. On output, the variable receives the number of TCHARs copied to the buffer, including the terminating null character.
If lpBuffer is too small, the function fails and GetLastError returns ERROR_INSUFFICIENT_BUFFER. This parameter receives the required buffer size, including the terminating null character.
Strings
Strings are easy - just use theBSTR
(which is WideString
). We have discussed this above in details.Note that in some cases you can return complex structured data (objects) as JSON or a similar way of packing the data into a string. And if this is your case - you can also use the
BSTR
type.In all other cases, you need to use one of the three methods below.
System Memory Manager
You can fulfill the "who allocates memory - releases it" rule as follows: ask a third party to allocate and release memory, which both the called and the caller know about. For example, such a third party could be any system memory manager. This is exactly howBSTR
/WideString
works. Here are some options you can use:
- Process system heap:
- The
HeapAlloc
andHeapFree
called for theGetProcessHeap
; - The
GlobalAlloc
andGlobalFree
; - The
LocalAlloc
andLocalFree
.
- The
- COM-like memory managers:
- The
CoTaskMemAlloc
andCoTaskMemFree
; - The
SHAlloc
andSHFree
;
- The
- The
VirtualAlloc
andVirtualFree
.
Pretty big list. Which one is better to use?
- The
VirtualAlloc
/VirtualFree
allocate memory with a granularity of 64Kb, so you should only use them if you need to exchange huge data; GlobalAlloc
/GlobalFree
andLocalAlloc
/LocalFree
are pretty much outdated and have more overhead thanHeapAlloc
/HeapFree
, so you don't need to use them;
HeapAlloc
/HeapFree
and COM. The Heap option may very well be the default. COM memory managers may be more familiar to some programming languages. In addition, there is a ready-made memory manager interface (see below). In general, it is more like taste choice, not much real difference.Here is an example of how it might look in code. In DLL (simplified code without error handling):
uses ActiveX; // or uses OLE2; function GetDynData(const AFlags: DWORD; out AData: Pointer; out ADataSize: DWORD): BOOL; stdcall; var P: array of Something; begin P := { ... prepare data to return ... }; ADataSize := Length(P) * SizeOf(Something); AData := CoTaskMemAlloc(ADataSize); Move(Pointer(P)^, AData^, ADataSize); Result := True; end;exe:
uses ActiveX; // or uses OLE2; var P: array of Something; Data: Pointer; DataSize: DWORD; begin GetDynData(0, Data, DataSize); SetLength(P, DataSize div SizeOf(Something)); Move(Data^, Pointer(P)^, DataSize); CoTaskMemFree(Data); // Work with P end;Note: it is just an example. In real applications, you can (on the callee side) both prepare data immediately in the returned buffer (provided that you know its size in advance), and (on the caller side) work with the returned data directly, without copying it to another type of buffer.
Of course, at the same time, your SDK should have documentation on the
GetDynData
function, which will explicitly say that the returned memory must be freed by calling CoTaskMemFree
, like this: GetDynData
Returns XYZ.Syntaxfunction GetDynData(const AFlags: DWORD;out AData: Pointer;out ADataSize: DWORD): BOOL; stdcall;ParametersAFlags [in, optional]Type: DWORDOptional flags: ...AData [out]Type: PointerPointer to a data of theADataSize
bytes. The caller should free this data by calling theCoTaskMemFree
function.ADataSize [out]Type: DWORDSize of theAData
in bytes.ReturnsIf the function succeeds - it returnsTrue
.
If the function fails - it returnsFalse
. You can call theGetLastError
function to learn the failure reason.Remarks...ExamplesA usage example can be found in the Getting the data sample example code.Requirements
API version 1 Headers MyDll.pas
Note: of course, the
CoTaskMemAlloc
/CoTaskMemFree
calls can be replaced with HeapAlloc
/HeapFree
or any other option convenient for you. Note that with this method, you typically need to copy the data twice: in the callee (to copy the data from the prepared location to a location suitable for return to the caller) and possibly in the caller (to copy the returned data into structures suitable for further use). Sometimes you can get away with a single copy if the caller can use the data right away. But it is rare to get rid of copying data in the callee.
Dedicated Wrapper Functions
Another option is to wrap your preferred memory manager in an exportable function. Accordingly, the documentation for the function should indicate that to free the returned memory, you need to call notCoTaskMemFree
(or whatever you used there), but your wrapper function. Then you can simply return prepared data immediately, without copying. For example, in a DLL (simplified code without error handling):function GetDynData(const AFlags: DWORD; out AData: Pointer; out ADataSize: DWORD): BOOL; stdcall; var P: array of Something; begin P := { ... prepare your data ... }; ADataSize := Length(P) * SizeOf(Something); Pointer(AData) := Pointer(P); // copy the pointer, not the data itself Pointer(P) := nil; // block the auto-release Result := True; end; procedure DynDataFree(var AData: Pointer); stdcall; var P: array of Something; begin if AData = nil then Exit; Pointer(P) := Pointer(AData); // and again: copy just the pointer AData := nil; Finalize(P); // a matching release function // (it is optional in this particular case) end;exe:
var P: array of Something; Data: Pointer; DataSize: DWORD; begin GetDynData(0, Data, DataSize); SetLength(P, DataSize div SizeOf(Something)); Move(Data^, Pointer(P)^, DataSize); DynDataFree(Data); // Work with P end;Note: we can't just copy the pointer to the array on the caller's side because the
GetDynData
contract says nothing about the compatibility of the returned data with Delphi's dynamic array. Indeed, a DLL can be written in MS Visual C++, which does not have dynamic arrays.As in the previous case, this contract must also be explicitly stated in your SDK's documentation:
Note that by using a wrapper function you can reduce the amount of data copying, because now you don't need to copy the data on the caller's side, because you use the same memory manager for both calculations and for returning the data. The disadvantage of this method is the need to write additional wrapper functions. Sometimes you can get away with one generic wrapper function common to all exported functions. But more often than not, you will need an individual cleanup function for each exported function (returning data) if you want to use "just one copy" advantage.AData [out]Type: PointerPointer to a data of theADataSize
bytes. The caller should free this data by calling theDynDataFree
function.
If you use one generic cleanup function, you can return it as the
IMalloc
interface. This will be more familiar to those familiar with the basics of COM. But it will also allow you not only to return memory to the caller, but also to accept memory from the caller with transfer of ownership. For example:uses ActiveX; // or Ole2 type TAllocator = class(TInterfacedObject, IMalloc) function Alloc(cb: Longint): Pointer; stdcall; function Realloc(pv: Pointer; cb: Longint): Pointer; stdcall; procedure Free(pv: Pointer); stdcall; function GetSize(pv: Pointer): Longint; stdcall; function DidAlloc(pv: Pointer): Integer; stdcall; procedure HeapMinimize; stdcall; end; { TAllocator } function TAllocator.Alloc(cb: Integer): Pointer; begin Result := AllocMem(cb); end; function TAllocator.Realloc(pv: Pointer; cb: Integer): Pointer; begin ReallocMem(pv, cb); Result := pv; end; procedure TAllocator.Free(pv: Pointer); begin FreeMem(pv); end; function TAllocator.DidAlloc(pv: Pointer): Integer; begin Result := -1; end; function TAllocator.GetSize(pv: Pointer): Longint; begin Result := -1; end; procedure TAllocator.HeapMinimize; begin // does nothing end; function GetMalloc(out AAllocator: IMalloc): BOOL; stdcall; begin AAllocator := TAllocator.Create; Result := True; end; //_______________________________________ function GetDynData(const AOptions: Pointer; out AData: Pointer; out ADataSize: DWORD): BOOL; stdcall; var P: array of Something; begin P := { ... prepare the data with AOptions ... }; // Assume the AOptions was passed with ownership FreeMem(AOptions); ADataSize := Length(P) * SizeOf(Something); AData := GetMem(ADataSize); Move(Pointer(P)^, Pointer(AData)^, ADataSize); Result := True; end;exe:
var A: IMalloc; Options: Pointer; P: array of Something; Data: Pointer; DataSize: DWORD; begin GetMalloc(A); Options := A.Alloc({ options' size }); { Preparing Options } GetDynData(Options, Data, DataSize); // Do not free Options, because we have passed ownership to the GetDynData SetLength(P, DataSize div SizeOf(Something)); Move(Data^, Pointer(P)^, DataSize); A.Free(Data); // Work with P end;Note: of course, it is a bit of a nonsensical example, because in this particular case there is no need to pass the ownership of the
AOptions
to the GetDynData
function: the caller can clean up the memory himself, then the callee will not need to free the memory. But it is just an example. In real applications, you may need to keep AOptions
inside the DLL for longer than the function's call. The example shows how this can be done by hiding the memory manager behind a facade.Also note that if you implement the
TAllocator.GetSize
method, then the ADataSize
parameter can be removed.Interfaces
Instead of using the system memory manager and/or special export functions (two ways above), it is much more convenient to useinterface
s for the following reasons:- An interface is a record with function pointers, an analogue of a class with virtual functions. Due to this, each method automatically becomes a wrapper function from the previous paragraph, i.e. always works with the right memory manager. In other words, there is no need to use a fixed third-party memory manager, nor to introduce wrapper functions;
- Any programming languages can understand interfaces;
- Interfaces can pass complex data (objects);
- Interfaces are self-cleanup types (in Delphi), no need to explicitly call cleanup functions;
- Interfaces can be easily modified by extending them in future versions of your DLL;
- The way Delphi implements interfaces using compiler magic makes it easy to implement proper error handling (see the next section below).
type IData = interface ['{C79E39D8-267C-4726-98BF-FF4E93AE1D44}'] function GetData: Pointer; stdcall; function GetDataSize: DWORD; stdcall; property Data: Pointer read GetData; property DataSize: DWORD read GetDataSize; end; TData = class(TInterfacedObject, IData) private FData: Pointer; FDataSize: DWORD; protected function GetData: Pointer; stdcall; function GetDataSize: DWORD; stdcall; public constructor Create(const AData: Pointer; const ADataSize: DWORD); end; constructor TData.Create(const AData: Pointer; const ADataSize: DWORD); begin inherited Create; if ADataSize > 0 then begin GetMem(FData, ADataSize); Move(AData^, FData^, ADataSize); end; end; function TData.GetData: Pointer; stdcall; begin Result := FData; end; function TData.GetDataSize: DWORD; stdcall; begin Result := FDataSize; end; //________________________________ function GetDynData(const AFlags: DWORD; out AData: IData): BOOL; stdcall; var P: array of Something; begin P := { ... preparing the data ... }; AData := TData.Create(Pointer(P), Length(P) * SizeOf(Something)); Result := True; end;exe:
var P: array of Something; Data: IData; begin GetDynData(0, Data); SetLength(P, Data.DataSize div SizeOf(Something)); Move(Data^, Data.Data^, Data.DataSize); // Work with P end;In this case, we have made one universal
IData
interface that can be written once and be used in all functions. Although it does not require writing special code for every function, it also results in data copying on the side of the callee, as well as lack of typing. Here's what an improved DLL might look like:type IData = interface ['{C79E39D8-267C-4726-98BF-FF4E93AE1D44}'] function GetData: Pointer; stdcall; function GetDataSize: DWORD; stdcall; property Data: Pointer read GetData; property DataSize: DWORD read GetDataSize; end; TSomethingArray = array of Something; TSomethingData = class(TInterfacedObject, IData) private FData: TSomethingArray; FDataSize: DWORD; protected function GetData: Pointer; stdcall; function GetDataSize: DWORD; stdcall; public constructor Create(var AData: TSomethingArray); end; constructor TSomethingData.Create(var AData: TSomethingArray); begin inherited Create; FDataSize := Length(AData) * SizeOf(Something); if FDataSize > 0 then begin Pointer(FData) := Pointer(AData); Pointer(AData) := nil; end; end; function TSomethingData.GetData: Pointer; stdcall; begin Result := Pointer(FData); end; function TSomethingData.GetDataSize: DWORD; stdcall; begin Result := FDataSize; end; function GetDynData(const AFlags: DWORD; out AData: IData): BOOL; stdcall; var P: TSomethingArray; begin P := { ... preparing the data ... }; AData := TSomethingData.Create(P); Result := True; end;In this case, the outer wrapper (that is, the interface) remains unchanged, only the DLL code changes. So the caller's code (in the exe) doesn't change either. But if you change the contract (interface), then you can do the following:
type ISomethingData = interface ['{CF8DF791-1E8D-4363-94A2-9FF035A9015A}'] function GetData: Pointer; stdcall; function GetDataSize: DWORD; stdcall; function GetCount: Integer; stdcall; function GetItem(const AIndex: Integer): Something; stdcall; property Data: Pointer read GetData; property DataSize: DWORD read GetDataSize; property Count: Integer read GetCount; property Items[const AIndex: Integer]: Something read GetItem; default; end; TSomethingArray = array of Something; TSomethingData = class(TInterfacedObject, ISomethingData) private FData: TSomethingArray; FDataSize: DWORD; protected function GetData: Pointer; stdcall; function GetDataSize: DWORD; stdcall; function GetCount: Integer; stdcall; function GetItem(const AIndex: Integer): Something; stdcall; public constructor Create(var AData: TSomethingArray); end; constructor TSomethingData.Create(var AData: TSomethingArray); begin inherited Create; FDataSize := Length(AData) * SizeOf(Something); if FDataSize > 0 then begin Pointer(FData) := Pointer(AData); Pointer(AData) := nil; end; end; function TSomethingData.GetData: Pointer; stdcall; begin Result := Pointer(FData); end; function TSomethingData.GetDataSize: DWORD; stdcall; begin Result := FDataSize; end; function TSomethingData.GetCount: Integer; stdcall; begin Result := Length(FData); end; function TSomethingData.GetItem(const AIndex: Integer): Something; stdcall; begin Result := FData[AIndex]; end; function GetDynData(const AFlags: DWORD; out AData: ISomethingData): BOOL; stdcall; var P: TSomethingArray; begin P := { ... preparing the data ... }; AData := TSomethingData.Create(P); Result := True; end;and then the caller turns into this:
var Data: ISomethingData; begin GetDynData(0, Data); // No need to copy, just work with Data: for X := 0 to Data.Count do AddToList(Data[X]); end;In general, there are quite wide possibilities, you can do almost anything you want. And even if you first made a contract through
IData
, then later you can add ISomethingData
by simply extending the interface with inheritance. However, older clients of your version 1 DLL will use IData
, while version 2 clients may request the more convenient ISomethingData
.As you can see from the code above, interfaces are more useful the more complex the returned data. It is very easy to return complex objects as interfaces, while returning a simple block of memory means writing a lot of code.
The obvious downside is the need to write more code for interfaces, since you need a thunk object to implement the interface. But this minus is easily neutralized by the next paragraph (see "Error Handling" below). It's also partially removed if you originally need to return an object (because it doesn't require a thunk object, the returned object itself can implement the interface).
Note: the code above is just an example. In real code, you need to add error handling and move the
IData
/ISomethingData
interface definitions to separate files (your SDK headers).Error Handling
(and calling convention)
When a programmer writes code, he determines the sequence of actions in the program, placing operators, function calls, and so on in the right order. At the same time, the implemented sequence of actions corresponds to the logic of the algorithm: first we do this, then this, and finally this. The main code corresponds to the "ideal" situation, when all files are in their places, all variables have valid values, and so on. But during the actual operation of the program, situations inevitably occur when the code written by the programmer will operate in an unacceptable (and sometimes unforeseen) environment. Such (and some other) situations are called by the generalized word "error". Therefore, the programmer must somehow determine what he will do in such situations. How will he determine the admissibility of the situation, how to react to it, etc.As a rule, the minimum blocks subject to control are a function or procedure (subroutine). Each subroutine performs a specific task. And we can expect a different level of "success" for this task: the task was successful or an error occurred during its execution. To write reliable code, we absolutely need a way to detect error situations - how do we determine that an error has occurred in a function? And responding to them is the so-called "error recovery" (i.e.: what will we do when an error occurs?). Traditionally, there are two main ways to handle errors: error codes and exceptions.
Error codes
(and why not to use them)
Error codes are perhaps the easiest way to respond to errors. Its essence is simple: the subroutine must return some sign of the success of the task. There are two options here: either it will return a simple sign (success/failure), or it will return the execution status (in other words - "error description"), i.e. a certain code (number) of one of several predefined situations: the function parameters are incorrectly set, the file is not found, etc. In the first case, there may be an additional function that returns the execution status of the last function called. With this approach, errors found in the function are usually passed up (to the calling function). Each function must check the results of other function calls for errors and perform appropriate processing. Most often, the processing is simply passing the error code even higher, to the "higher" calling function. For example: function A calls B, B calls C, C detects an error and returns an error code to B. B checks the return code, sees that an error occurred, and returns an error code to A. A checks the return code and issues an error message (or decides to do something else).For example, here is a typical Windows API function:
It is a typical way to handle errors in the classic Windows API. In this case: the so-called Win32 error codes. The Win32 error code is a usualRegisterClassEx
Registers a window class for subsequent use in calls to theCreateWindow
orCreateWindowEx
function.Syntaxfunction RegisterClassEx(const AClass: TWndClassEx): ATOM; stdcall;ParametersAClass [in]Type: TWndClassExA pointer to aWNDCLASSEX
structure. You must fill the structure with the appropriate class attributes before passing it to the function.ReturnsIf the function succeeds, the return value is a class atom that uniquely identifies the class being registered. This atom can only be used by theCreateWindow
,CreateWindowEx
,GetClassInfo
,GetClassInfoEx
,FindWindow
,FindWindowEx
, andUnregisterClass
functions and theIActiveIMMap.FilterClientWindows
method.
If the function fails, the return value is zero. To get extended error information, callGetLastError
.Remarks... cut ...ExamplesFor an example, see Using Window Classes.Requirements
Minimum supported client Windows 95 Headers Winuser.h Library User32.dll
DWORD
number. Error codes are fixed and declared in the Windows
unit. The absence of an error is taken as ERROR_SUCCESS
or NO_ERROR
(equal to 0). Constants are defined for all possible errors. Those begin (usually) with the ERROR_
word, for example:
{ Incorrect function. } ERROR_INVALID_FUNCTION = 1; { dderror } { The system cannot find the file specified. } ERROR_FILE_NOT_FOUND = 2; { The system cannot find the path specified. } ERROR_PATH_NOT_FOUND = 3; { The system cannot open the file. } ERROR_TOO_MANY_OPEN_FILES = 4; { Access is denied. } ERROR_ACCESS_DENIED = 5; { The handle is invalid. } ERROR_INVALID_HANDLE = 6; // ... and so onA description of the Win32 error can be obtained via the
FormatMessage
function. There is (specifically for our case) a more convenient wrapper in Delphi for this system function with a bunch of parameters: the SysErrorMessage
function. It returns the human-readable description of the passed Win32 error code. By the way, note that messages are returned localized. In other words, if you have Russian Windows, then the messages will be in Russian. If English - in English.Summarizing what has been said, you have to call such functions like this:
{ prepare the WndClass } ClassAtom := RegisterClassEx(WndClass); if ClassAtom = 0 then begin // some error occurred, failure reason is indicated by GetLastError Application.MessageBox( PChar('There was an error: ' + SysErrorMessage(GetLastError)), PChar('Error'), MB_OK or MB_ICONSTOP); Exit; end; // ... continue normal execution
As in the memory management case - it is the same: do not follow the example of Windows. This style has long been outdated. And here's what's wrong with him (it is not a complete list):
- To call a function, two calls are required: the function itself and the
GetLastError
(add to this the need to call the function itself twice to get memory from it - it turns out to be a terrible horror as much as four function calls instead of one); - You need to explicitly write a check like
if something then error
. And if you forget to write this code, you will get a bug: your program will continue executing ignoring the error. Probably corrupting the data and making it difficult to localize the bug (the visible problem will happen later);- If-like checks also clog the code visually;
- If in case of an error you need to free some resources, and even if there are several of them and there are also several function calls, then the correct code for freeing resources can become very non-trivial;
- You can not pass any additional information. For example, you can not specify in any way which argument is incorrect, or which file you do not have access to;
- You have no way of knowing which function failed, whether it was the function you called, or maybe some other function that the one you called might have called;
- The debugger will not notify you of the problem in any way (although, hypothetically, you could put a breakpoint on the
GetLastError
).
Despite all the disadvantages, error codes have a plus: since they are just numbers, they are understandable to any programming language. In other words, error codes are compatible between different languages.
Exceptions
(and why not to use them)
Exceptions don't have many of the disadvantages of error codes:- Exceptions do not need to be explicitly checked, the default reaction is an error response;
- The program is not "polluted" with the check code, it is taken out of your main code;
- Easy to release resources (via
try
-finally
); - Exceptions are easy to extend, inherit, add additional fields, make nested exceptions;
- The debugger will notify you when an exception occurs;
- You can assign your code to diagnose exceptions (the so-called exception tracer).
But despite all the pluses, exceptions have one significant minus, which crosses out all the pluses (in relation to the DLL API).
Recall how exceptions are raised in Delphi:
var E: Exception; begin E := EMyExceptionClass.Create('Something'); raise E; end;I split the typical "
raise EMyExceptionClass.Create('Something');
" line into two to make the problem even more obvious. We create a Delphi object (exception) and "throw" it. And whoever wants to handle this exception does this:except on E: EMyException do begin ShowMessage(E.Message); end; // - E will be deleted here end;It means that the Delphi object is passed from the callee (DLL) where the exception is thrown to the caller (exe) where the exception is handled. As we learned earlier (see the Data Types section above), this is a problem. Other programming languages don't know what a Delphi object is, nor how to read it, nor how to delete it. Even Delphi itself doesn't always know this (for example, if an exception is thrown by code built on Delphi 7 but caught by code built on Delphi XE, or vice versa). Other programming languages use similar constructs: an exception is represented by an object. Accordingly, Delphi code has no idea how to work with objects in other languages.
In other words, exceptions should not be used due to language incompatibilities.
Corollary 1: Exceptions should not leave your DLL.
Corollary 2: You must catch all exceptions in your exported functions.
Corollary 3: all exported functions must have the global construct
try
-except
.function GetDynData(const AFlags: DWORD; out AData: IData): BOOL; stdcall; begin try // ... function's code, useful payload ... Result := True; except // ... exception handling ... Result := False; end; end;
What can and should be used
(and which calling convention to use)
If we can't use error codes and we can't use exceptions, then what should we use? Well, we need to use their combination - and here is how it looks.Delphi has built-in compiler magic that can wrap any function in a hidden
try
-except
block with an automatic (hidden) call to the processing function. And there is a compiler magic that works the other way around: on the returned error code, it automatically raises the appropriate exception.Before we get to know this magic, we need to get acquainted with error codes in the form of the
HRESULT
type. The HRESULT
is also a number, but now of the Integer
type. The HRESULT
is no longer just an error code, it consists of several parts, which we will not go into in detail, but suffice it to say that they include the error code itself (what used to be Win32 code), a sign success or failure, identifier of the error agent. Error codes typically start with the E_
prefix (for example, E_FAIL
, E_UNEXPECTED
, E_ABORT
or E_ACCESSDENIED
), and success codes typically start with the S_
prefix (for example, S_OK
or S_FALSE
). It is easy to determine the success of the HRESULT
code by comparing it with zero: HRESULT
error codes must be less than zero.Highlighting the success/error indicator means that now there is no need for the function to return only the success/failure (via
BOOL
), and the error code itself - through a separate function (GetLastError
). Now the function can return both information at once, in one call:function GetDynData(const AFlags: DWORD; out AData: IData): HRESULT; stdcall; begin try // ... function's code, useful payload ... Result := S_OK; except // ... exception handling ... Result := E_FAIL; // some error code end; end;
Along with the introduction of the
HRESULT
type, the IErrorInfo
interface was added - which allows you to associate additional information with the returned HRESULT
: arbitrary description, GUID of the raising party (interface), the location of the error (arbitrary line), help. You don't even need to implement this interface, the system already has an object ready - returned by the CreateErrorInfo
function.Finally, Delphi has the already mentioned compiler magic that can make writing such code easier. To do this, the function must have a calling convention
stdcall
and return the HRESULT
type. If before the function returned some Result
, then this Result
should be converted to the last out-parameter, for example:// Was: function GetDynData(const AFlags: DWORD): IData; // Became: function GetDynData(const AFlags: DWORD; out AData: IData): HRESULT; stdcall;If a function satisfies these requirements, then you can declare it like this:
function GetDynData(const AFlags: DWORD): IData; safecall;This would be binary equivalent (i.e. fully compatible) to:
function GetDynData(const AFlags: DWORD; out AData: IData): HRESULT; stdcall;
By declaring a function as
safecall
you turn on compiler magic for it, namely:- The returned result will be automatically converted to the last out parameter;
- The function will implicitly return the
HRESULT
(and possiblyIErrorInfo
); - The function's call will be wrapped in an if-check on the return code. If an erroneous
HRESULT
is received, an exception will be raised:var Data: IData; begin Data := GetDynData(Flags); // throwns exception when error occurs // execution continues only when there is no error
- The function itself will be wrapped in a hidden
try
-except
block that converts the exception toHRESULT
(and possibly toIErrorInfo
):function GetDynData(const AFlags: DWORD): IData; safecall; begin // ... function's code, useful payload ... end; // - a hidden try-except block
HRESULT
of the stdcall
function and parsing it (perhaps along with IErrorInfo
).How to use safecall
correctly
Now that we've covered the benefits of the safecall
, it's time for a fly in the ointment. The fact is that the safecall
magic works in a minimal mode "out of the box". And to get maximum benefit from it, we need to take additional steps. Luckily, they only need to be made once and can be reused in the future.Item number one: simple exported functions:
procedure DoSomething; safecall; begin // ... function's code, useful payload ... end; exports DoSomething;Unfortunately, the compiler does not allow customizing the process of converting an exception to
HRESULT
for ordinary functions, always returning a fixed code and losing additional error information. Therefore, instead of exported functions, you need to use interfaces with methods. Before:procedure DoSomething; safecall; begin // ... function's code, useful payload ... end; function GetDynData(const AFlags: DWORD): IData; safecall; begin // ... function's code, useful payload ... end; function DoSomethingElse(AOptions: IOptions): BSTR; safecall; begin // ... function's code, useful payload ... end; exports DoSomething, GetDynData, DoSomethingElse;After:
type IMyDLL = interface ['{C5DBE4DC-B4D7-475B-9509-E43193796633}'] procedure DoSomething; safecall; function GetDynData(const AFlags: DWORD): IData; safecall; function DoSomethingElse(AOptions: IOptions): BSTR; safecall; end; TMyDLL = class(TInterfacedObject, IMyDLL) protected procedure DoSomething; safecall; function GetDynData(const AFlags: DWORD): IData; safecall; function DoSomethingElse(AOptions: IOptions): BSTR; safecall; end; procedure TMyDLL.DoSomething; safecall; begin // ... function's code, useful payload ... end; function TMyDLL.GetDynData(const AFlags: DWORD): IData; safecall; begin // ... function's code, useful payload ... end; function TMyDLL.DoSomethingElse(AOptions: IOptions): BSTR; safecall; begin // ... function's code, useful payload ... end; function GetFunctions(out AFunctions: IMyDLL): HRESULT; stdcall; begin try AFunctions := TMyDLL.Create; Result := S_OK; except on E: Exception do Result := HandleSafeCallException(E, ExceptAddr); end; end; exports GetFunctions;where
HandleSafeCallException
is our function, which we will describe below.As you can see, we have placed all exported functions in a single interface (object) - this will allow us to set up/control the process of converting exceptions to
HRESULT
. In this case, the DLL exports the only function that we had to write manually, without safecall
- which also allowed us to control the conversion process. Don't forget that it is binary compatible with safecall
, so if you want to use this DLL in Delphi you can do this:function GetFunctions: IMyDLL; safecall; external 'MyDLL.dll';and it will work just fine.
For objects, when an exception is thrown in a
safecall
method, the compiler calls the TObject.SafeCallException
virtual method which does nothing useful by default and which we can replace with our own method:type TMyDLL = class(TInterfacedObject, IMyDLL) protected procedure DoSomething; safecall; function GetDynData(const AFlags: DWORD): IData; safecall; function DoSomethingElse(AOptions: IOptions): BSTR; safecall; public function SafeCallException(ExceptObject: TObject; ExceptAddr: Pointer): HResult; override; end; function TMyDLL.SafeCallException(ExceptObject: TObject; ExceptAddr: Pointer): HResult; begin Result := HandleSafeCallException(ExceptObject, ExceptAddr); end;
Further, when the code calls a
safecall
method, the compiler wraps the method's call in a CheckAutoResult
wrapper, which (in case of erroneous code) raises an exception through the SafeCallErrorProc
global variable function, which, again, we can replace with our own:procedure RaiseSafeCallException(ErrorCode: HResult; ErrorAddr: Pointer); begin // ... our code ... end; initialization SafeCallErrorProc := RaiseSafeCallException; end.Now we just need to make our
HandleSafeCallException
and RaiseSafeCallException
work as a pair and do something useful.First we need two helper wrapper functions:
uses ActiveX; // or Ole2 function SetErrorInfo(const ErrorCode: HRESULT; const ErrorIID: TGUID; const Source, Description, HelpFileName: WideString; const HelpContext: Integer): HRESULT; var CreateError: ICreateErrorInfo; ErrorInfo: IErrorInfo; begin Result := E_UNEXPECTED; if Succeeded(CreateErrorInfo(CreateError)) then begin CreateError.SetGUID(ErrorIID); if Source <> '' then CreateError.SetSource(PWideChar(Source)); if HelpFileName <> '' then CreateError.SetHelpFile(PWideChar(HelpFileName)); if Description <> '' then CreateError.SetDescription(PWideChar(Description)); if HelpContext <> 0 then CreateError.SetHelpContext(HelpContext); if ErrorCode <> 0 then Result := ErrorCode; if CreateError.QueryInterface(IErrorInfo, ErrorInfo) = S_OK then ActiveX.SetErrorInfo(0, ErrorInfo); end; end; procedure GetErrorInfo(out ErrorIID: TGUID; out Source, Description, HelpFileName: WideString; out HelpContext: Longint); var ErrorInfo: IErrorInfo; begin if ActiveX.GetErrorInfo(0, ErrorInfo) = S_OK then begin ErrorInfo.GetGUID(ErrorIID); ErrorInfo.GetSource(Source); ErrorInfo.GetDescription(Description); ErrorInfo.GetHelpFile(HelpFileName); ErrorInfo.GetHelpContext(HelpContext); end else begin FillChar(ErrorIID, SizeOf(ErrorIID), 0); Source := ''; Description := ''; HelpFileName := ''; HelpContext := 0; end; end;As you can easily imagine, they are intended to pass and receive additional information along with
HRESULT
.Next, we need a way to somehow pass the class name of the exception. You can do this in different ways. For example, pass it directly to
HRESULT
. To do this, it needs to be encoded. For example, like this:uses ComObj, // for the EOleSysError and EOleException VarUtils; // for the ESafeArrayError const // ID for our DLL API rules ThisDllIID: TGUID = '{AA76E538-EF3C-4F35-9914-B4801B211A6D}'; // "Customer" bit, it is always 0 for Microsoft-defined codes CUSTOMER_BIT = 1 shl 29; // Delphi uses this value to pass EAbort // It is assumed that E_Abort should show "Aborted" message, // while EAbortRaisedHRESULT should be handled silently. EAbortRaisedHRESULT = HRESULT(E_ABORT or CUSTOMER_BIT); function Exception2HRESULT(const E: TObject): HRESULT; function NTSTATUSFromException(const E: EExternal): DWORD; begin // ... end; begin if E = nil then Result := E_UNEXPECTED else if not E.InheritsFrom(Exception) then Result := E_UNEXPECTED else if E.ClassType = Exception then Result := E_FAIL else if E.InheritsFrom(ESafecallException) then Result := E_FAIL else if E.InheritsFrom(EAssertionFailed) then Result := E_UNEXPECTED else if E.InheritsFrom(EAbort) then Result := EAbortRaisedHRESULT else if E.InheritsFrom(EOutOfMemory) then Result := E_OUTOFMEMORY else if E.InheritsFrom(ENotImplemented) then Result := E_NOTIMPL else if E.InheritsFrom(ENotSupportedException) then Result := E_NOINTERFACE else if E.InheritsFrom(EOleSysError) then Result := EOleSysError(E).ErrorCode else if E.InheritsFrom(ESafeArrayError) then Result := ESafeArrayError(E).ErrorCode else if E.InheritsFrom(EOSError) then Result := HResultFromWin32(EOSError(E).ErrorCode) else if E.InheritsFrom(EExternal) then if Failed(HRESULT(EExternal(E).ExceptionRecord.ExceptionCode)) then Result := HResultFromNT(Integer(EExternal(E).ExceptionRecord.ExceptionCode)) else Result := HResultFromNT(Integer(NTSTATUSFromException(EExternal(E)))) else Result := MakeResult(SEVERITY_ERROR, FACILITY_ITF, Hash(E.ClassName)) or CUSTOMER_BIT; end;Here we are checking for a few special predefined classes, and we also have the ability to pass Win32 codes and hardware exception codes directly. For all other (Delphi specific) exception classes, we use the hash on the class name along with
FACILITY_ITF
. As a hash, you can use, for example, SDBM - this is a very simple hash function with good randomization of the result. Of course, you can use any other method - for example, just manually extract and fix the codes for each exception class.That's why in the code above we also definedHRESULT
s withFACILITY_NULL
andFACILITY_RPC
codes have a generic value because they are defined by Microsoft.HRESULT
withFACILITY_ITF
code are defined by the interface function or method from which they are returned. This means that the same 32-bit value inFACILITY_ITF
but returned by two different interfaces can have different meanings. In this way, Microsoft can define multiple generic error codes while still allowing other programmers to define new error codes without fear of conflict. The coding convention looks like this:
HRESULT
with codes other thanFACILITY_ITF
can only be defined by Microsoft;HRESULT
ofFACILITY_ITF
are defined solely by the implementer of the interface or function that returnsHRESULT
. To avoid conflictingHRESULT
, whoever defines an interface is responsible for coordinating and publishing theHRESULT
codes associated with that interface;- All
HRESULT
defined by Microsoft have an error code value in the $0000-$01FF range. Although you can use any code withFACILITY_ITF
, it is recommended to use values in the $0200-$FFFF range. This recommendation is intended to reduce confusion with Microsoft codes.
ThisDllIID
which is an "interface" identifier that gives meaning to returned codes of type FACILITY_ITF
. This value must be passed as ErrorIID
to the SetErrorInfo
defined above.The 29th "Customer" bit was originally a reserved bit, which was later allocated to be used as a flag indicating whether the code is defined by Microsoft (0) or by a third party (1). In a way, this bit duplicates
FACILITY_ITF
. Usually even third party developers only use FACILITY_ITF
. In this case, we set it to reduce possible problems with bad code (which does not take into account the GUID of the interface).Wverything is a little more complicated with the reverse conversion (code to exception): we need tables to search for the exception class by code. A simple implementation might look like this:
function HRESULT2Exception(const E: HRESULT): Exception; function MapNTStatus(const ANTStatus: DWORD): ExceptClass; begin // ... end; function MapException(const ACode: DWORD): ExceptClass; begin // ... end; var NTStatus: DWORD; ErrorIID: TGUID; Source: WideString; Description: WideString; HelpFileName: WideString; HelpContext: Integer; begin if GetErrorInfo(ErrorIID, Source, Description, HelpFileName, HelpContext) then begin if Pointer(StrToInt64Def(Source, 0)) <> nil then ErrorAddr := Pointer(StrToInt64(Source)); end else Description := SysErrorMessage(DWORD(E)); if (E = E_FAIL) or (E = E_UNEXPECTED) then Result := Exception.Create(Description) else if E = EAbortRaisedHRESULT then Result := EAbort.Create(Description) else if E = E_OUTOFMEMORY then begin OutOfMemoryError; Result := nil; end else if E = E_NOTIMPL then Result := ENotImplemented.Create(Description) else if E = E_NOINTERFACE then Result := ENotSupportedException.Create(Description) else if HResultFacility(E) = FACILITY_WIN32 then begin Result := EOSError.Create(Description); EOSError(Result).ErrorCode := HResultCode(E); end else if E and FACILITY_NT_BIT <> 0 then begin // Get exception's class by code NTStatus := Cardinal(E) and (not FACILITY_NT_BIT); Result := MapNTStatus(NTStatus).Create(Description); // Create a dummy ExceptionRecord just in case ReallocMem(Pointer(Result), Result.InstanceSize + SizeOf(TExceptionRecord)); EExternal(Result).ExceptionRecord := Pointer(NativeUInt(Result) + Cardinal(Result.InstanceSize)); FillChar(EExternal(Result).ExceptionRecord^, SizeOf(TExceptionRecord), 0); EExternal(Result).ExceptionRecord.ExceptionCode := cDelphiException; EExternal(Result).ExceptionRecord.ExceptionAddress := ErrorAddr; end else if (E and CUSTOMER_BIT <> 0) and (HResultFacility(E) = FACILITY_ITF) and CompareMem(@ThisDllIID, @ErrorIID, SizeOf(ErrorIID)) then Result := MapException(HResultCode(E)).Create(Description) else Result := EOleException.Create(Description, E, Source, HelpFileName, HelpContext); end;In general, the code is fairly straightforward, with the exception of hardware exceptions. We make emulation for them.
Also note that the
Source
field of the IErrorInfo
interface must point to the location where the error occurred. This field is arbitrary and is determined by the interface developer (ie, again, by GUID). In this case, we just write the address of the exception there. But, for example, if you use an exception tracer (such as EurekaLog), you can write the call stack there.Then with the above helper functions, our
HandleSafeCallException
and RaiseSafeCallException
become trivial:function HandleSafeCallException(ExceptObj: TObject; ErrorAddr: Pointer): HRESULT; var ErrorMessage: String; HelpFileName: String; HelpContext: Integer; begin if ExceptObj is Exception then ErrorMessage := Exception(ExceptObj).Message else ErrorMessage := SysErrorMessage(DWORD(E_FAIL)); if ExceptObj is EOleException then begin HelpFileName := EOleException(ExceptObj).HelpFile; HelpContext := EOleException(ExceptObj).HelpContext; end else begin HelpFileName := ''; if ExceptObj is Exception then HelpContext := Exception(ExceptObj).HelpContext else HelpContext := 0; end; Result := SetErrorInfo(Exception2HRESULT(ExceptObj), ThisDllIID, '$' + IntToHex(NativeUInt(ErrorAddr), SizeOf(ErrorAddr) * 2), ErrorMessage, HelpFileName, HelpContext); end; procedure RaiseSafeCallException(ErrorCode: HResult; ErrorAddr: Pointer); var E: Exception; begin E := HRESULT2Exception(ErrorCode, ErrorAddr); raise E at ErrorAddr; end;Note: in our model, we do not use help fields of the
IErrorInfo
interface.It should be noted that if an interface uses
HRESULT
and IErrorInfo
together, then it should also implement the ISupportErrorInfo
interface. Some programming languages require this. By calling ISupportErrorInfo.InterfaceSupportsErrorInfo
, the client side can determine that an object supports additional information.And the last point - in the Delphi implementation for Windows 32-bit there is nasty bug that doesn't exist in 64-bit RTL, as well as on other platforms. The fix for this bug is included in the code examples at the link at the end of the article.
DllMain Workaround
TheDllMain
is a special function in a DLL that is called by the system when the DLL is loaded into, unloaded from a process (and attached/detached to/from a thread). For example, the initialization
and finalization
sections of your Delphi modules are executed inside DllMain
.The problem is that
DllMain
is a very special function. It is called while holding the critical section of the loader (modules) of the operating system. In long and detailed terms - see the links at the end of this paragraph, and in short: DllMain
is a weapon from which you can easily shoot yourself. There are not many things that can be done legally in DllMain
. But it's incredibly easy to do something forbidden - you constantly need to be sure that this very function that you just called can never, under any circumstances, do something forbidden. This makes it incredibly difficult to use code written elsewhere. The compiler won't tell you anything. And the code will most of the time work like it should... but sometimes it will crash or freeze.The solution to the problem is to do nothing in
DllMain
(read: don't write code in initialization
and finalization
sections of your units when you create a DLL).Instead, you need to make separate DLL initialization and finalization functions. You need to do them even if your DLL doesn't need any initialization or cleanup steps. After all, such a need may arise in the future, and if you do not provide separate initialization and finalization functions in your API, you will not be able to solve this problem later.
Here is the code template:
// In headers: type IMyDll = interface ['{C5DBE4DC-B4D7-475B-9509-E43193796633}'] procedure InitDLL(AOptional: IUnknown = nil); safecall; procedure DoneDLL; safecall; // ... end; // In DLL: type TInitFunc = procedure(const AOptional: IUnknown); TDoneFunc = procedure; TInitDoneFunc = record Init: TInitFunc; Done: TDoneFunc; end; procedure RegisterInitFunc(const AInitProc: TInitFunc; const ADoneFunc: TDoneFunc = nil); // ... var GInitDoneFuncs: array of TInitDoneFunc; procedure RegisterInitFunc(const AInitProc: TInitFunc; const ADoneFunc: TDoneFunc); begin SetLength(GInitDoneFuncs, Length(GInitDoneFuncs) + 1); GInitDoneFuncs[High(GInitDoneFuncs)].Init := AInitProc; GInitDoneFuncs[High(GInitDoneFuncs)].Done := ADoneFunc; end; procedure TMyDLL.InitDLL(AOptional: IUnknown); safecall; var X: Integer; begin for X := 0 to High(GInitDoneFuncs) do if Assigned(GInitDoneFuncs[X].Init) then GInitDoneFuncs.Init(AOptional); end; procedure TMyDLL.DoneDLL; safecall; var X: Integer; begin for X := 0 to High(GInitDoneFuncs) do if Assigned(GInitDoneFuncs[X].Done) then GInitDoneFuncs.Done; end; // In your units: procedure InitUnit(const AOptional: IUnknown); begin // ... code from unit's initialization sections end; procedure DoneUnit; begin // ... code from unit's finalization section end; initialization RegisterInitFunc(InitUnit, DoneUnit); end;The
AOptional
parameter is designed for possible future use. It is not used in the code above, but later (in the next version of the DLL) you can use it to pass initialization parameters. IUnknown
is the base interface from which all other interfaces are inherited (i.e. some analogue of TObject
for interfaces).I hope this code is clear enough. Of course, it must be distributed among different units and sections. Interface - in headers,
RegisterInitFunc
declaration - in interface
of the common DLL module, you need to call it from the initialization
section of other units.Of course, your SDK documentation should say that the user (client) of your DLL must call the
InitDLL
method immediately after loading your DLL with the LoadLibrary
function and call the DoneDLL
just before the DLL is unloaded by FreeLibrary
:var DLL: HMODULE; DLLApi: IMyDll; begin DLL := LoadLibrary('MyDLL.dll'); Win32Check(DLL <> 0); try DLLApi.InitDLL(nil); // working with DLL, for example, calling DLLApi.GetDynData finally DLLApi.DoneDLL; DLLApi := nil; FreeLibrary(DLL); end; end;
More information about the
DllMain
:- DllMain and life before birth
- DllMain : a horror story
- Some reasons not to do anything scary in your DllMain
- Another reason not to do anything scary in your DllMain: Inadvertent deadlock
Callback Functions
A callback function - passing the executable code as one of the parameters of another code. For example, if you want to set a timer using the Windows API, you can callSetTimer
function, passing it a pointer to your own function, which will be the callback function. The system will call your function every time the timer fires:procedure MyTimerHandler(Wnd: HWND; uMsg: UINT; idEvent: UINT_PTR; dwTime: DWORD); stdcall; begin // Will be called after 100 ms timeout end; procedure TForm1.Button1Click(Sender: TObject); begin SetTimer(Handle, 1, 100, @MyTimerHandler); end;Here's another example: if you want to find all windows on the desktop, you can use the
EnumWindows
function:function MyEnumFunc(Wnd: HWND; lpData: LPARAM): Bool; stdcall; begin // Will be called for each found window end; procedure TForm1.Button1Click(Sender: TObject); begin EnumWindows(@MyEnumFunc, 0); end;Since the callback function usually performs the same task as the code that sets it up, it turns out that both pieces of code need to work with the same data. Therefore, the data from the setting code must somehow be passed to the callback function (or visa versa). For this purpose, so-called user parameters are provided in the callback functions: it is either a pointer or an integer (necessarily of the
Native(U)Int
type, but not just (U)Int
), which are not used by the API itself in any way and are transparently passed to the callback function. Or (in rare cases) it can be some value that uniquely identifies the function's call.For example,
SetTimer
has idEvent
and EnumWindows
has lpData
. We can use these parameters to pass arbitrary data. For example, here is how you can find all windows of a given class:type PEnumArgs = ^TEnumArgs; TEnumArgs = record ClassName: String; Windows: TStrings; end; function FindWindowsOfClass(Wnd: HWND; lpData: LPARAM): Bool; stdcall; var Args: PEnumArgs; WndClassName, WndText: String; begin Args := Pointer(lpData); SetLength(WndClassName, Length(Args.ClassName) + 2); SetLength(WndClassName, GetClassName(Wnd, PChar(WndClassName), Length(WndClassName))); if WndClassName = Args.ClassName then begin SetLength(WndText, GetWindowTextLength(Wnd) + 1); SetLength(WndText, GetWindowText(Wnd, PChar(WndText), Length(WndText))); Args.Windows.Add(Format('%8x : %s', [Wnd, WndText])); end; Result := True; end; procedure TForm1.Button1Click(Sender: TObject); var Args: TEnumArgs; begin // Edit can contain values like: // 'TForm1', 'IME', 'MSTaskListWClass', 'Shell_TrayWnd', 'TTOTAL_CMD', 'Chrome_WidgetWin_1' Args.ClassName := Edit1.Text; Args.Windows := Memo1.Lines; Memo1.Lines.BeginUpdate; try Memo1.Lines.Clear; EnumWindows(@FindWindowsOfClass, LPARAM(@Args)); finally Memo1.Lines.EndUpdate; end; end;
Note: here's another example of how not to do it - don't do it like in Windows. If you just need to get a list of something - don't make a callback, just return the list in an array (wrap it in an interface or pass it as a block of memory - as discussed above). The callback function should only be used if creating the list can take a long time and you don't need all the elements. Then the callback function can return the "stop" flag without completing the list to the end.
Note: some analogue of user-parameters are
Tag
and Data
properties, although their use is not always ideologically correct (correct: create a derived class).The conclusion follows from the above: if your API needs to make a callback function, then it must have a custom
Pointer
size parameter that will not be used by your API. For example:// Incorrect! type TNotifyMeProc = procedure; safecall; IMyDllAPI = interface // ... procedure NotifyMe(const ANotifyEvent: TNotifyMeProc); safecall; end;
// Correct type TNotifyMeProc = procedure(const AUserArg: Pointer); safecall; IMyDllAPI = interface // ... procedure NotifyMe(const ANotifyEvent: TNotifyMeProc; const AUserArg: Pointer = nil); safecall; end;And if you forget to do this, the caller will have to use ugly hacks to get around your bad API design.
Naturally, instead of a function + parameter, you can just use an interface:
// Correct type INotifyMe = interface ['{07FA30E4-FE9B-4ED2-8692-1E5CFEE4CF3F}'] procedure Notify; safecall; end; IMyDllAPI = interface // ... procedure NotifyMe(const ANotifyEvent: INotifyMe); safecall; end;This is preferable, because error handling via
safecall
in interfaces is simpler, and the interface can contain as many parameters as you like, and it is even more convenient to integrate with objects (form). For example:type TForm1 = class(TForm, INotifyMe) // ... procedure Notify; safecall; private FAPI: IMyDllAPI; public function SafeCallException(ExceptObject: TObject; ExceptAddr: Pointer): HResult; override; end; // ... procedure TForm1.FormCreate(Sender: TObject); begin // ... load DLL, get API FAPI.NotifyMe(Self); // Ask DLL to call us on some event end; procedure TForm1.Notify; begin ShowMessage('Something just happenned'); // Our form is available here (in the callback), // so we can just use it, no need to pass it manually end;
Other Rules
- If you are not only developing but also using a DLL, then load the DLL correctly. I won't go into details here, as it deserves its own article, but you can take a look at the
LoadDLL
function from the sample code below: do NOT useLoadLibrary
!; - If for some reason you don't use
safecall
then don't return complex types viaResult
, make it an out parameter. The problem is that Delphi and MS Visual C++ disagree on how to interpret the result returned by reference by a stdcall function: asvar
or asout
. Accordingly, forsafecall
there is no such problem, sinceResult
for it is alwaysInteger
(HRESULT
) - a simple type, for whichvar
andout
are equivalent; - All APIs must have a unique IID/GUID (non-API interfaces (not mentioned in headers) might not have a GUID, although I would recommend always specifying an IID). You can create a GUID to use as an IID (Interface ID) by pressing
Ctrl + Shift + G
in the Delphi code editor - this combination will insert an expression like['{C5DBE4DC-B4D7-475B-9509- E43193796633}']
(of course, each time with a unique GUID) directly below the cursor in the editor; - Once you have published some type (interface), i.e. released your DLL with this interface - you shouldn't change it. If you need to expand or change it, you introduce a new interface (a new version of the interface), but do not change the old one
// Was: type IMyDLL = interface ['{C5DBE4DC-B4D7-475B-9509-E43193796633}'] procedure InitDLL(AOptional: IUnknown = nil); safecall; procedure DoneDLL; safecall; function GetDynData(const AFlags: DWORD): IData; safecall; end; // You can't do that after publishing a production build: type IMyDLL = interface ['{C5DBE4DC-B4D7-475B-9509-E43193796633}'] procedure InitDLL(AOptional: IUnknown = nil); safecall; procedure DoneDLL; safecall; procedure DoSomething; safecall; // was added function GetDynData(const AFlags: DWORD): IData; safecall; end; // However, you can do that: type IMyDLLv1 = interface ['{C5DBE4DC-B4D7-475B-9509-E43193796633}'] procedure InitDLL(AOptional: IUnknown = nil); safecall; procedure DoneDLL; safecall; function GetDynData(const AFlags: DWORD): IData; safecall; end; IMyDLLv2 = interface(IMyDLLv1) ['{69E77989-64DC-4177-975C-487818598C70}'] procedure DoSomething; safecall; // added end;
- If a function or method returns an interface, then don't do this:
// Incorrect! function GetSomething: ISomething; safecall; // ... var Something: ISomething; begin Something := GetSomething;
Surely, it is a convenient solution in the beginning: you can call functions "as usual" and even chain them into chains likeControl
.GetPicture
.GetImage
.GetColorInfo
.GetBackgroundColor
. However, this state of affairs will exist only in the very first version of the system. As soon as you start developing the system, you will start to have new interfaces. In the not so distant future, you will have a bunch of advanced interfaces, and the basic interfaces that were in the program initially, at the time of its birth, will implement only trivially uninteresting functions. As a result, very often the calling code will need new interfaces, not the original ones. What does it mean? This means that the code needs to call the original function, get the original interface, then ask it for a new one (viaSupports
/QueryInterface
) and only then use the new interface. It turns out not so convenient, even rather inconvenient: we have a triple call (original + conversion + required). The best solution is for the calling code to tell the called function which interface it is interested in: the new one or the old one:
// Correct procedure GetSomething(const AIID: TGUID; out Intf); safecall; // ... var Something: ISomething; begin GetSomething(ISomething, Something);
- If an object implements an interface, then your code should not contain variables of this class. I.e.:
type TSomeObject = class(TSomeOtherClass, ISomeInterface) // ... end; var Obj: TSomeObject; // - incorrect! Obj: ISomeInterface; // - correct begin Obj := TSomeObject.Create; // ...
- If you're implementing an interface extension by inheritance, don't forget to explicitly list all of its ancestors in the implementing object. For example:
type ISomeInterfaceV1 = interface ['{A80A78ED-5836-49C4-B6C2-11F531103FE7}'] procedure A; end; ISomeInterfaceV2 = interface(ISomeInterfaceV1) // ISomeInterfaceV2 is inherited from ISomeInterfaceV1 ['{EBDD52A1-489B-4564-998E-09FCCF923F48}'] procedure B; end; // Incorrect! TObj = class(TInterfacedObject, ISomeInterfaceV2) // ISomeInterfaceV2 is mentioned, but not ISomeInterfaceV1 protected procedure A; procedure B; end; var SI1: ISomeInterfaceV1; SI2: ISomeInterfaceV2; begin Supports(SI2, ISomeInterfaceV1, SI1); Assert(Assigned(SI1)); // will fire, since SI1 = nil (Supports returned False) end;
A correct way would be:
// Correct TObj = class(TInterfacedObject, ISomeInterfaceV1, ISomeInterfaceV2) // ...
- It is not necessary to make the implementation of interface methods virtual:
type ISomeInterfaceV1 = interface ['{C25F72B0-0BC9-470D-8F43-6F331473C83C}'] procedure A; end; TObj = class(TInterfacedObject, ISomeInterfaceV1) protected // Incorrect procedure A; virtual; end;
Do like so instead:
TObj = class(TInterfacedObject, ISomeInterfaceV1) protected // Correct procedure A; end;
- Do not use the
const
modifier with interface parameters:
// Incorrect! procedure DoSomething(const AArg: ISomething); safecall; // Correct procedure DoSomething(AArg: ISomething); safecall;
- Other unspoken rules.
Conclusion
Download sample DLL API here. The archive contains a group of two projects (a DLL and an application that uses it). The DLL implements an sample API with example functions. The SDK folder contains the SDK, which consists of:- SDK:
SampleDLLHeaders.pas
header file;- CHM and PDF documentation (and project sources Help&Manual);
- As well as the Delphi-specific support file
DelphiSupport.pas
.
SampleDLLHeaders.pas
and Delphi developers can use SampleDLLHeaders.pas
+ DelphiSupport.pas
.Headers are presented only in the form of Delphi code. Translation into other programming languages left as homework.
The
DelphiSupport.pas
module can be included both in the DLL and in applications that use it. It contains:- Processing
safecall
(along with fixing RSP-24652); - A base object
TBaseObject
for implementing interfaces withsafecall
processing support and debugging checks; - Prebuilt classes:
TMalloc
allocator andTNotify
wrapper; - The function
RegisterInitFunc
for registering the initialization of modules in the DLL; LoadDLL
function for correct loading of the DLL.
The DLL API has sample functions:
- Returning an array of strings
GetData
; - Returning dynamic memory
GetMemory
; - Callback function set by
NotifyMe
; - Error/Exception Test
TryAbort
,TryAccessViolation
, etc
The calling application shows both "load-use-upload" and "load, use, upload".