DLL dos and don’ts by Rudy Velthuis
NOTE: Rudy died in 2019! - My condolences!
NOTE: Rudy died in 2019! - My condolences!
I don’t care if it works on your machine! We are not shipping your machine!
— Vidiu Platon
[SHOWTOGROUPS=4,20]
In my perusals on Stack Overflow and in the Embarcadero newsgroups, and recently also the new English language en.Delphi-Praxis and the original German language Delphi-Praxis, as well as the Idera forums, I have seen lots of examples of people trying to interface with DLLs that were written without any consideration on their use in other languages than the one it was written in.
They export functions with language-specific arguments or result types, like C++ objects or Delphi strings, or use calling conventions that can only be found in certain compilers, like Visual C++’s __fastcall or Delphi’s register.
People using a different language, or even a different version of the same language are unable or hardly able to use such DLLs.
In this article, I try to lay down my experience with DLLs and how I think they should be written, so they can be used from (almost) every language on Windows.
Most of my experience with, well, badly — often stupidly — written DLLs comes from conversions as described in my article Pitfalls of converting and from many questions on Для просмотра ссылки Войдиили Зарегистрируйся and the Embarcadero Discussion Forums.
Although this is a Delphi-centric site, most of what I write applies to writing DLLs in any language.
Note that this is not a tutorial on how you can produce a DLL in Delphi — or in any other language. That is well described in the documentation and I bet there are online tutorials for it as well. This article is merely a discussion of the dos and don’ts when writing a DLL.
DLLs
DLLs are, basically, libraries providing a set of functions (or procedures) to be called from a program or another DLL. There are certain limits to what most languages can use. Therefore, if I write a DLL, I follow these rules:
If you can, use packages, not DLLs
In Delphi, it makes no sense to export Delphi-specific types from a DLL, even if you are pretty sure that your DLL will only be used by a program written in Delphi. There will still be issues like memory management or RTTI differences.
Rather use packages (.BPL files). These contain a lot of metadata that makes them much more suitable to interface with Delphi, and you can pass around any datatype Delphi knows, without any memory management or RTTI issues. Packages can also export types and variables without the need to declare them and without an extra import unit.
In other words: Do not use DLLs if you can use packages.
Parameters
When interfacing with a DLL, parameters are one of the main problems to deal with.
Types
I found that, for a DLL, the common denominator for the types used is the language C. Almost all languages on Windows are able to interface with a DLL that only exposes C Для просмотра ссылки Войдиили Зарегистрируйся types. POD stands for “plain old data” and more or less defines the scalar types like integers, floating point types, character types and pointers, or compound structures (structs, unions, arrays) which contain only such data types.
A (non-exhaustive) list of the Windows types that can be safely passed across DLL borders are:
Alternatively, if a fixed size is important, you can use the fixed size integral types, like UInt8 (Byte) or Int32 (32 bit signed integer), UInt64, etc. Generally, the name IntXX denotes a signed integer, and the name UIntXX an unsigned integer. The XX part denotes the number of bits, i.e. 8, 16, 32 or 64. Most other languages will have equivalents for these, often even with the same naming convention.
If you pass around compound types (structs, arrays), pass them around as pointers or references.
Bitfields
If you are a C or C++ programmer: although they are a valid C construct, do not use C bitfields. These are very awkward to use in any other language but C or C++.
Extended
Extended is an IEEE-754 type, and some compilers are able to handle it, but certainly not all. There could also be alignment issues around them, i.e. one compiler may expect them aligned on 16 bytes, while the other aligns them on 10 bytes (in a struct, or in an array). It is probably best to avoid Extended parameters (or return values) in DLLs.
COM Types
Another class of types that can usually be passed across DLL borders are the COM-compatible types, like (IUnknown-based) interfaces. Note that in C and C++, interfaces are declared as pointer types to structs, while in Delphi, they are reference types already, so the following Delphi declaration:
procedure SetInstanceExplorer(const punk: IUnknown); stdcall;
is equivalent to C++’s:
References
Many languages handle references differently. C strictly uses pointers, and that is, in my opinion, the best way to handle them too, because almost all languages on Windows can deal with pointers (even so called “managed” languages like the .NET languages can deal with them, through marshalling). So instead of doing something like:
You should probably do something like:
Now, Embarcadero (and before, Borland) advise the use of var. I am a bit ambivalent about this. The simple code example above, with GetCurMoniker, is easier to read with var, but using var can make translating to and from C a little harder, since in C, you need one extra explicit level of indirection. In other words, if you use var, you are not translating as verbatim as possible anymore. Reading documentation, which often shows the C syntax, is also a little harder that way.
I think it is best to use what you prefer.
Arrays
Arrays are best passed around as pointers to the first element.
Because in C and in many other languages, arrays do not have an inherent length that can somehow be retrieved from such a pointer, you should have an extra parameter that passes the length (in elements) of that array.
An example:
// Usage:
Do not declare such array parameters as var parameters. This makes it a lot harder to use pointer arithmetic or to use casts to access the single elements of the array. How this can be done can be found in my conversion article
[/SHOWTOGROUPS]
In my perusals on Stack Overflow and in the Embarcadero newsgroups, and recently also the new English language en.Delphi-Praxis and the original German language Delphi-Praxis, as well as the Idera forums, I have seen lots of examples of people trying to interface with DLLs that were written without any consideration on their use in other languages than the one it was written in.
They export functions with language-specific arguments or result types, like C++ objects or Delphi strings, or use calling conventions that can only be found in certain compilers, like Visual C++’s __fastcall or Delphi’s register.
People using a different language, or even a different version of the same language are unable or hardly able to use such DLLs.
In this article, I try to lay down my experience with DLLs and how I think they should be written, so they can be used from (almost) every language on Windows.
Most of my experience with, well, badly — often stupidly — written DLLs comes from conversions as described in my article Pitfalls of converting and from many questions on Для просмотра ссылки Войди
Although this is a Delphi-centric site, most of what I write applies to writing DLLs in any language.
Note that this is not a tutorial on how you can produce a DLL in Delphi — or in any other language. That is well described in the documentation and I bet there are online tutorials for it as well. This article is merely a discussion of the dos and don’ts when writing a DLL.
DLLs
DLLs are, basically, libraries providing a set of functions (or procedures) to be called from a program or another DLL. There are certain limits to what most languages can use. Therefore, if I write a DLL, I follow these rules:
- Limit your types to the types the C language has, or to structs/records of these types. In other words, do not export data types that are specific to a certain language, like C++ templates, objects (eg. std::string) or Delphi objects, AnsiStrings, UnicodeStrings, etc.
C is an exception to this, and can be considered some kind of “common denominator”. - Do not export data or shared memory. Not all languages can handle those. Just export functions. If you want to expose data, use functions for that.
- Careful with (certain) return values! These are handled differently by different compilers.
- Take care to use the proper calling convention. Some language-specific calling conventions like Delphi’s default register calling convention, or C++ ’s fastcall and thiscall calling conventions cannot be handled by many languages.
- Be sure to use and document valid structure and array alignment. If necessary, compare with other compilers and use filler bytes.
- In a DLL, do not allocate data that is passed to the user. The user may not have a proper way to deallocate/free such data. If possible, let the user allocate the structure (e.g. a string buffer) and just fill it in the DLL.
- Never let exceptions escape a DLL. Exceptions are language-specific and other languages can very likely not handle them.
- If possible, provide a C header for the functions and the data types used in the DLL, even if the DLL is not written in C. This will make the DLL usable across a large variety of languages.
If you can, use packages, not DLLs
In Delphi, it makes no sense to export Delphi-specific types from a DLL, even if you are pretty sure that your DLL will only be used by a program written in Delphi. There will still be issues like memory management or RTTI differences.
Rather use packages (.BPL files). These contain a lot of metadata that makes them much more suitable to interface with Delphi, and you can pass around any datatype Delphi knows, without any memory management or RTTI issues. Packages can also export types and variables without the need to declare them and without an extra import unit.
In other words: Do not use DLLs if you can use packages.
Parameters
When interfacing with a DLL, parameters are one of the main problems to deal with.
Types
The main mistake people make when writing DLLs is to use language-specific types, like templates or classes in C++ or Delphi, or AnsiStrings, strings, dynamic arrays, sets, etc. in Delphi.Bad programmers worry about the code. Good programmers worry about data structures and their relationships.- — Linus Torvalds
I found that, for a DLL, the common denominator for the types used is the language C. Almost all languages on Windows are able to interface with a DLL that only exposes C Для просмотра ссылки Войди
A (non-exhaustive) list of the Windows types that can be safely passed across DLL borders are:
(Win32) | signed | unsigned | 32 bit | 64 bit |
---|---|---|---|---|
C or C++ type | Delphi types | Size (bytes) | ||
int | Integer | Cardinal | 4 | |
long (int) 1 | Longint | Longword | 4 | |
short (int) 1 | Smallint | Word | 2 | |
char | Shortint | Byte | 1 | |
char 2 | AnsiChar | 1 | ||
wchar_t | WideChar | 2 | ||
float | Single | 4 | ||
double | Double | 8 | ||
__int64 3 | Int64 | UInt64 | 8 | |
void | none | 0 | ||
void * | Pointer | 4 | 8 | |
char * | PAnsiChar | 4 | 8 | |
wchar_t * | PWideChar | 4 | 8 | |
int * | PInteger | 4 | 8 | |
1 the use of the keyword int together with signed, unsigned, long and short is optional. If no type is specified with these keywords, int is implied. 2 if used without signed or unsigned. 3 non-standard extension, but widely used in Win32. |
If you pass around compound types (structs, arrays), pass them around as pointers or references.
Bitfields
If you are a C or C++ programmer: although they are a valid C construct, do not use C bitfields. These are very awkward to use in any other language but C or C++.
Extended
Extended is an IEEE-754 type, and some compilers are able to handle it, but certainly not all. There could also be alignment issues around them, i.e. one compiler may expect them aligned on 16 bytes, while the other aligns them on 10 bytes (in a struct, or in an array). It is probably best to avoid Extended parameters (or return values) in DLLs.
COM Types
Another class of types that can usually be passed across DLL borders are the COM-compatible types, like (IUnknown-based) interfaces. Note that in C and C++, interfaces are declared as pointer types to structs, while in Delphi, they are reference types already, so the following Delphi declaration:
procedure SetInstanceExplorer(const punk: IUnknown); stdcall;
is equivalent to C++’s:
Код:
void __stdcall SetInstanceExplorer(IUnknown *punk);
References
Many languages handle references differently. C strictly uses pointers, and that is, in my opinion, the best way to handle them too, because almost all languages on Windows can deal with pointers (even so called “managed” languages like the .NET languages can deal with them, through marshalling). So instead of doing something like:
Код:
type
MyRecord =
...
end;
procedure SomeAPI(... var Rec: MyRecord; ...);
You should probably do something like:
Код:
type
PMyRecord = ^MyRecord;
MyRecord =
...
end;
procedure SomeAPI(... Rec: PMyRecord; ...);
Now, Embarcadero (and before, Borland) advise the use of var. I am a bit ambivalent about this. The simple code example above, with GetCurMoniker, is easier to read with var, but using var can make translating to and from C a little harder, since in C, you need one extra explicit level of indirection. In other words, if you use var, you are not translating as verbatim as possible anymore. Reading documentation, which often shows the C syntax, is also a little harder that way.
I think it is best to use what you prefer.
Arrays
Arrays are best passed around as pointers to the first element.
Because in C and in many other languages, arrays do not have an inherent length that can somehow be retrieved from such a pointer, you should have an extra parameter that passes the length (in elements) of that array.
An example:
Код:
// DLL function:
procedure DrawGraph(PlotPoints: PInteger; Count: Integer); stdcall;
// Usage:
Код:
var
Measurements: array[0..2999] of Integer;
begin
// Fill measurements array here, and then draw the graph:
DrawGraph(@Measurements[0], Length(Measurements));
Do not declare such array parameters as var parameters. This makes it a lot harder to use pointer arithmetic or to use casts to access the single elements of the array. How this can be done can be found in my conversion article
[/SHOWTOGROUPS]