En français : Créer une librairie (DLL) en C ou C++ : quelques erreurs à éviter.
It is common to provide a software library to allow customers to use a product. For instance, if you're selling a scientific computation software, the customers need to read the output files or they need to integrate some functions of your program in their software. If you're selling carbon dioxide sensors, you can provide a DLL (Dynamic Link Library) to allow customers to read measured values.
Regardless of the programming language of your software, providing a C interface is convenient because it enables an integration in nearly every language.
On Windows, you provide a .dll file containing functions useful for the customer, a .lib file listing the functions and indicating how to find them in the .dll file and a .h file giving human-readable signatures of functions. On Linux, the DLL is called .so and is only provided with a .h file.
Should I provide a C or C++ interface?
If you're developing your program in C++ and your customers are also using C++, providing a DLL for this language gives a clear interface which benefits from classes and references. This is what Qt does for instance. The main disadvantage is that you need to compile the library with several compilers. Worse, sometimes you need to support several versions of the same compiler : GCC, Clang, MSVC (Visual Studio 2015, 2022, etc).
The C++ standard library changes between the compiler versions. For instance, between two versions of Visual Studio, std::string can change and leads to runtime errors that are difficult to understand.
To sum up, a C++ library has some advantages but also a cost. A solution is to make the library opensource and make the customers to compile it themself. Another solution is to develop an header-only C++ interface (only implemented in .h files) to hide the C interface.
Avoid frequent mistakes
malloc and free or new and delete must be in the same compilation unit, i.e., same program or same DLL. Let's have a look at the following functions:
DLL_EXPORT char* GetProjectName(Project_t id);
GetProjectName returns a string allocated by the DLL. If the customer's compiler is different from the compiler that produced the DLL, unallocating the string with a free can lead to a runtime error. The reason is that the allocator can be implemented differently. For instance in debug, malloc and free can contain special code to detect allocation problems.
The classical solution:
DLL_EXPORT void GetProjectName(Project_t id, char* name);
In this case, the customer is responsible for allocating an array to store the string. We must provide the maximum size of the string or a GetProjectNameLength function. An alternative solution is to define a type which contains the string:
typedef struct MyString_ { const char * const str; } MyString_t;
.. and some functions to manipulate MyString_t :
DLL_EXPORT MyString_t* CreateMyString(const str* ms); DLL_EXPORT void DeleteMyString(MyString_t* ms); DLL_EXPORT MyString_t* GetProjectName(Project_t id);
Fonctions CreateMyString and GetProjectName do malloc and the function DeleteMyString does a free. Hence, the DLL is responsible for the both allocating and freeing the memory.
This solution also reduces potential errors because the customer doesn't need to allocate an array with the correct size. The keywords consts prevent str from being modified by the customer and prevent DeleteMyString from calling free on a user-allocated memory.
Use types to improve the interface. Let's have a look at the following functions:
DLL_EXPORT int GetCurrentProjectId(); DLL_EXPORT void RemoveFile(int projectId, int fileId);
This code can be made a lot clearer with types.
typedef int ProjectId_t; typedef int FileId_t; DLL_EXPORT ProjectId_t GetCurrentProjectId(); DLL_EXPORT void RemoveFile(ProjectId_t projectId, FileId_t fileId);
Unfortunately, the compiler doesn't throw warning if ProjectId_t is used instead of FieldId_t, but the code is a lot easier to read.
Const correctness. Please declare pointers or reference as const when possible.
DLL_EXPORT char* SetProjectName(Project_t id, const char* str);
str is of type const char*. This is mandatory to allow the user to use constant string. The customer will not be able to write code observing const correctness if your library doesn't observe it neither.
No more than 4 parameters for fonctions. Use a structure if they are to many inputs. To return several parameters, use structure. Never use arrays to return data of different natures. For instance, returning an array with [x y z] can be dangerous because the order is confusing. In C++, avoid the std::pair which is also confusing.
Use opaque pointers to identify instances of your library., this allows you to hide the data. In C++, the PIMPL design pattern has this purpose. In previous examples, I used integers (for instance Project_t) which is less efficient because the mapping must done explicitly (with a std::map or an array) but integers are sometimes easier than pointers for customers.
Document the data type, format and unit. GetPressure, GetTemperature or GetDistance don't mean anything if we don't know if they are returning bars, N/m², °C, °F, meters or mm.
Last but not least, the lib is the interface between you and your customers. The documentation and function names must use the customer vocabulary, not an internal jargon!
To conclude, put yourself in your customer's shoes and try to figure out what they will understand from your interface and which mistake they will do. Make their job easier by making mistakes impossible or a least difficult to do.