DLLs and how to create them with C++

DLL (Dynamic Link Library) files are an essential part of the Windows operating system and many software applications. Although they are pervasive, their functioning may seem a bit mysterious to many users. In this article, we will explore the world of DLL files, discovering what they are, how they work, and attempting to create one.

What is a DLL file?

A DLL file is a Dynamic Link Library that contains code and data that can be used by more than one program simultaneously. Essentially, DLLs allow the sharing of common functionalities among different applications without duplicating the code.

How do DLLs work?

DLLs contain functions and procedures that can be called by other programs. When an application needs a particular functionality, instead of including the code directly in its executable, it can reference the appropriate DLL. This dynamic linking process occurs at runtime, meaning the program can access the DLL's functions only when needed.

This approach has several advantages, including:

  • Space savings: Since multiple programs can share the same DLL file, code duplication is avoided, saving disk space.

  • Simplified updates: If changes need to be made to a common function, updating the DLL is sufficient, and all programs referencing it will automatically benefit from the changes.

  • Memory efficiency: DLLs can be loaded into memory only when required, contributing to a more efficient use of system resources.

How do you create a DLL file?

As mentioned, Dynamic Link Libraries (DLLs) are crucial components in building efficient and modular software on the Windows platform. Creating a DLL file requires a deep understanding of programming and development best practices. In this article, we will discuss how to create a simple DLL using the C++ language and the Microsoft Visual Studio development environment.

In general

In general, creating a Dynamic Link Library (DLL) involves generating executable code meant for "exporting" or sharing resources. To export a resource, such as a function or class, one typically adds the __declspec(dllexport) keyword to its declaration. For example:

#include <windows.h>

extern "C" __declspec(dllexport) void HelloWorld() {
    MessageBox(nullptr, "Hello World!", "DLL Message", MB_OK);
}

This code defines and exports a function named HelloWorld(), which displays a window with a greeting message. In the declaration, the extern "C" specifier prevents name "mangling" of the function, forcing the compiler to treat the function as if it were declared in the C language, without any additional decoration. This allows calling the function from code written in C or other languages following the C calling convention.

Name "mangling" is a technique used by the compiler to manage the unique signature of functions, considering the types of their parameters and return types. While useful to support function overloading, it might pose issues when interfacing with other languages that might not understand this "mangling."

This becomes particularly relevant when working with Application Programming Interfaces (APIs) shared between C++ code and code written in other languages. In our next example, we will use only the C++ language, so the extern "C" specifier will not be necessary.

Practical Example

Step 1: Creating a DLL Project

Open Visual Studio and create a new project by selecting the "File" -> "New Project..." menu and choosing "Dynamic-Link Library (DLL)" as the project type.

Click the Next button and fill in the required information.

Step 2: Creating a Class

To add a new class to the project, right-click on the project folder, select "Add" -> "Class."

In the window that appears, specify the class name, header file (.h) name, implementation file (.cpp) name, and click OK.

Define the Class

At this point, all that's left is to define the class, i.e., declare it with all its attributes and methods in the newly created header file (the file with the .h extension). For example:

Counter.h

#pragma once
class Counter
{
protected:
    double m_value;
public:
    Counter(double initialValue = 0.0);
    double Add(double value = 1.0);
    double Sub(double value = 1.0);
    double GetValue();
    void SetValue(double value);
};

Implementing the Class

Once the class has been defined, the next step is to implement it, which involves writing the body of the functions declared in the above-mentioned header file, typically in a file with a .cpp extension, usually with the same name. For example:

Counter.cpp

#include "pch.h"
#include "Counter.h"

Counter::Counter(double initialValue) {
    m_value = initialValue;
}

double Counter::Add(double value) {
    m_value += value;
    return m_value;
}

double Counter::Sub(double value) {
    m_value -= value;
    return m_value;
}

double Counter::GetValue() {
    return m_value;
}

void Counter::SetValue(double value) {
    m_value = value;
}

Step 3: Configuring the DLL to Export the Class

As seen before, to export the Counter class, theoretically, we only need to add the __declspec(dllexport) keyword to the declaration in our header file (counter.h):

class __declspec(dllexport) Counter {...}

However, to use the DLL in another project, the declaration needs to be modified with the __declspec(dllimport) keyword:

class __declspec(dllimport) Counter {...}

To avoid creating two different files, the export macro is inserted at the top of the header file, defined as follows:

#ifdef COUNTER_EXPORTS
#define DLLIMPEXP __declspec(dllexport)
#else
#define DLLIMPEXP __declspec(dllimport)
#endif

And it should be added to the class declaration like this:

class DLLIMPEXP Counter {...}

What is the export macro and how does it work?

The export macro is a preprocessor directive in C++ that allows managing the visibility of symbols in a dynamic-link library (DLL) or a static library. Its primary utility is to provide a consistent way to declare which part of the code should be made accessible to other parts of the program or other DLLs.

In the context of a DLL, when declaring a class or a function to be exported, it is necessary to specify how these symbols should be treated during compilation and linking. The export macro is a means to achieve this portably, regardless of the compiler or operating system used.

When creating a DLL project, Visual Studio automatically defines a symbol with the format DLLNAME_EXPORTS (in this case COUNTER_EXPORT).

When the DLL is compiled, the presence of the COUNTER_EXPORT symbol definition causes the DLLIMPEXP export macro to be resolved as __declspec(dllexport). This instructs the compiler to export the class. On the other hand, when the "counter.h" file is included in another file of a different project using the #include directive, the DLLIMPEXP export macro is resolved as __declspec(dllimport). This informs the compiler that the Counter class is not defined in the current project but is imported from the external DLL..

Step 4: Compile and Distribute the DLL

At this point, proceed with the compilation of the project, which will generate our DLL. It is important to keep in mind that, to distribute the DLL and include it in another project, it will be necessary to provide not only the DLL file but also the "counter.h" and "counter.lib" files. The latter is generated during compilation and can be found in the output folder.

How to Use the DLL in Another Project

But how do we use our DLL in another C++ project? Let's create a simple example by making a console application.

Step 1: Create a New Test Project

Open Visual Studio and create a new project by selecting the "File" -> "New Project..." menu and choosing "Console App" as the project type.

Click the Next button and fill in the required information.

Press OK and then modify the ApplicationTest.cpp file as follows:

#include <iostream>
#include "Counter.h"

int main()
{
    Counter counter(1);

    std::cout << "Initial value!\n";
    std::cout << counter.GetValue() << "\n";

    std::cout << "We add 5 to the counter value\n";
    counter.Add(5);
    std::cout << counter.GetValue() << "\n";

    std::cout << "We subtract 2 from the counter value\n";
    counter.Sub(2);
    std::cout << counter.GetValue() << "\n";

    std::cout << "The final value is " << counter.GetValue() << "\n";

    std::cout << "We set the value to 10\n";
    counter.SetValue(10);
    std::cout << "Now the value is " << counter.GetValue() << "\n";
}

Observing the code, you can note that the definition of the Counter class has been included using the directive:

#include "Counter.h"

Then, an instance of the Counter object has been added and initialized to the value 1:

Counter counter(1);

Afterward, various methods were called that modified the value of the counter and displayed the result:

counter.Add()
counter.GetValue()
// and so on...

However, we cannot yet compile the project. It needs to be configured first.

Step 2: Configure the Project

Before being able to compile the project, we need to add the path to the header files of our DLL to the search paths in the compiler, and we need to import the .lib file for the linker.

To do this, right-click on the test project folder in the Solution Explorer and select Properties from the context menu. Choose General under the C++ branch and add the path to the folder containing the "counter.h" file in the definition of Additional Include directories.

Now, select General under the Linker branch and add the path to our counter.lib file in the Additional Library Directories definition.

Next, go to the Input branch under Linker, add Counter.lib to Additional Dependencies,

and click the OK button.

Step 3: Compile and Run the Application

At this point, we can compile the application without errors and run it. The result will be as follows.

Conclusion

In conclusion, Dynamic Link Libraries (DLLs) are a fundamental element in the software development ecosystem, providing modularity and efficiency. Throughout this article, we explored the concept of DLLs and delved into the creation process using the C++ programming language. From class declarations to the use of export macros, we outlined step by step the path to generate a functional DLL. The importance of correctly configuring the project, managing function name mangling, and considering the import declaration are crucial aspects to ensure seamless integration of the DLL into other projects.

Creating DLLs in C++ not only offers an efficient solution for organizing and sharing code but also represents a key practice in facilitating the development of robust and scalable applications. By maintaining code flexibility and modularity through DLLs, developers can create more manageable solutions and promote smoother software maintenance in the long run.