This article is the second part in my DLL series, and shows the techniques and considerations involved in exporting functions from a native DLL.
Introduction
In my previous article, I explained the DllMain
boilerplate and the various restrictions that apply to native DLL projects. In this article, I explore the actual techniques for exporting functions.
It is possible to export classes as well as variables, but for the reasons I explained in my previous article, we are going to restrict ourselves to plain functions.
Background
As I mentioned earlier, a DLL is a library of exported functions that can be called by a client application or by a higher level DLL that itself depends on your DLL, just like your DLL will depend on others, such as
kernel32.dll or (for example) vcruntimexxx.dll. This is what gave rise to the term 'DLL Hell' which is a mess of dependencies with DLLs that get loaded from God knows where, based on environment variables and relative locations.
Dependencies
When working with DLL projects, it is important to have a good tool for viewing those dependencies. In the olden days, Visual Studio used to come with depends.exe. These days, there is a superior tool called Dependencies. I will be using it in this article.
On the left side is a hierarchical tree that lists DLL dependencies, the dependencies of those DLLs, and so on. You can opt to see only the DLL name or the complete path. This is incredibly useful to evaluate the hierarchy and find out where those files are loaded from. The bottom lists file info about those DLLs.
If you select a DLL in the left hand pane, on the left side, you first see the functions that are imported by that DLL, and below that, the functions that are exported by that DLL.
Calling Functions
There are countless scripting languages and development environments which can load a native DLL and execute exported functions. For the purpose of this article, I've made a small Powershell script you can use. This way, you don't need anything for the purpose of this article that you don't already have on your system.
In Powershell, you can call native DLL function with a small workaround by going through C#. You basically define an empty C# class that uses DllImport
to load the DLL and map the function to a method.
For the purpose of this article, I made it so that you can put the script in the same location as the DLL and execute it from there. If we don't specify the DLL location explicitly, it will be located using the normal search order.
Add-Type -TypeDefinition @"
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
public static class fun
{
[DllImport("$($(Get-Location).Path.Replace('\','\\'))\\DynamicLibrary.dll", CharSet=CharSet.Auto)]
public static extern int getAnswerToLife();
}
"@
[fun]::getAnswerToLife()
Building a Simple DLL
Suppose we have a useful function and we want to present it to as many client applications as possible, and we decide to build a DLL and start a win32 C++ project in Visual Studio. This is the function we want to expose:
int getAnswerToLife(void)
{
return 42;
}
By default, all functions stay inside the module they're compiled it. In order to export it, we have to change the declaration as follows:
__declspec(dllexport) int getAnswerToLife(void);
__declspec(dllexport) int getAnswerToLife(void)
{
return 42;
}
The __declspec
keyword is used to give the compiler additional information that is not available through the language itself. If we compile and build the DLL project, we get a DLL which exports this function. Of course, a large potential user base for our DLL are other C++ projects who need to add the DynamicLibrary.h file to their project. Since they need to import the DLL, we don't use the __declspec
keyword like that. Instead, we do the following:
#ifdef DYNAMICLIBRARY_EXPORTS
#define DYNAMICLIBRARY_API __declspec(dllexport)
#else
#define DYNAMICLIBRARY_API __declspec(dllimport)
#endif
DYNAMICLIBRARY_API int getAnswerToLife(void);
DYNAMICLIBRARY_API int getAnswerToLife(void)
{
return 42;
}
When you build your DLL, you add DYNAMICLIBRARY_EXPORTS
to the C++ -> Preprocessor -> Preprocessor definitions
field. That way, your project will export the function, and client projects which include the header file import it.
We can now build the DLL, open it in DependenciesGui.exe and look at the fruit of our labor.
Not quite what we expected. The function is there, but there's a question mark in front of the name, and some weird characters are tacked on at the end. If we right-click the function and select 'Undecorate C++ name', we get this.
That looks more like it. So what is going on here?
Name Decoration
We built our DLL in C++. C++ has a concept known as Name Decoration, also known as Name Mangling. C++ allows function overloading, meaning you can have multiple getAnswerToLife
functions with different parameter lists. In order for the compiler to know which exact one it needs, it needs to have a way to distinguish them, which is why it mangles the source name into something more specific and unique.
The problem is that unless the client application expects this, it cannot call the function. For example, in Powershell, you can call native DLL function like this:
Add-Type -TypeDefinition @"
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
public static class fun
{
[DllImport("$($(Get-Location).Path.Replace('\','\\'))\\DynamicLibrary.dll",
CharSet=CharSet.Auto)]
public static extern int getAnswerToLife();
}
"@
[fun]::getAnswerToLife()
And this will happen:
The reason is Powershell tries to locate a function with the name getAnswerToLife
which it cannot find. We could explicitly define the names of the mangled functions. DllImport
allows it. Even so, it is obvious that using mangled names is not very userfriendly.
To solve this, we have to tell the compiler to use C style linkage which causes the compiler to skip name mangling.
#ifdef DYNAMICLIBRARY_EXPORTS
#define DYNAMICLIBRARY_API __declspec(dllexport)
#else
#define DYNAMICLIBRARY_API __declspec(dllimport)
#endif
#ifdef __cplusplus
extern "C" {
#endif
DYNAMICLIBRARY_API int getAnswerToLife(void);
#ifdef __cplusplus
}
#endif
#ifdef __cplusplus
extern "C" {
#endif
DYNAMICLIBRARY_API int getAnswerToLife(void)
{
return 42;
}
#ifdef __cplusplus
}
#endif
If we compile that and run the script, we get the expected result:
If we now look at the DLL dependencies, we see this:
If you compare that with the undecorated C++ name we had in the previous chapter, you'll notice a minor difference: there is no function parameter list, no return type, and no calling convention. This is because that information is no longer encoded in the function name. Now, the function name is nothing more than a label, instead of a label that contains meta info.
As a result, C linkage also makes it impossible to overload exported functions because the name mangling is what is used by the callers to distinguish between them.
Deciding whether this is something you want or not is of course a project specific decision you have to make but in general, this is the way to go in order to make your DLL convenient to use.
Calling Convention
At this point, we're most of the way done. There is one more thing we need to cover. If you look back to the screenshot of the 'undecorated' C++ name of our original exported function, you'll see __cdecl
in between the return type and the function name.
This is the calling convention of the function. The calling convention is literally a convention (agreement) between a function caller, and the function being called, about how the function arguments are being passed to the stack, who is responsible for cleaning up that stack, which registers are preserved in between function calls, etc.
There are many different conventions. Every different architecture has at least 1. On 32 bit Windows, there are several, due to historic reasons. The two most common ones are __cdecl
(C calling convention) and __stdcall
(Standard calling convention).
For practical purposes, there is not a whole lot of difference except that in C calling convention, it is the caller who needs to preserve and restore the stack between function calls. Since the caller doesn't know which registers are used by the callee, it needs to preserve and restore everything, which leads to slightly bigger executables.
On 32 bit builds, both conventions are used, which is annoying because you need to be aware of it for every external interface you use. The win32 subsystem uses __stdcall
so whenever you pass a function pointer (for example, when scheduling an APC) you have to make sure the function uses the correct calling convention or your application will crash. If your DLL's exported functions need to be passed to another subsystem as function pointers (for example, if they are used to pass to the win32 api, then you have to make the choice for __stdcall
accordingly. Otherwise, it doesn't matter a whole lot.
On 64 bit builds, only 1 calling convention is used. You can specify a calling convention, but it is ignored by the compiler.
In order to add this final piece of declaration, we modify our declaration one more time:
#ifdef DYNAMICLIBRARY_EXPORTS
#define DYNAMICLIBRARY_API __declspec(dllexport)
#else
#define DYNAMICLIBRARY_API __declspec(dllimport)
#endif
#define DYNAMICLIBRARY_CALLING __stdcall
#ifdef __cplusplus
extern "C" {
#endif
DYNAMICLIBRARY_API int DYNAMICLIBRARY_CALLING getAnswerToLife(void);
#ifdef __cplusplus
}
#endif
#ifdef __cplusplus
extern "C" {
#endif
DYNAMICLIBRARY_API int DYNAMICLIBRARY_CALLING getAnswerToLife(void)
{
return 42;
}
#ifdef __cplusplus
}
#endif
With that, we have done all we need to unambiguously export our functions.
Module Definition Files
There exists a second way to export functions. This is by using a Module Definition File aka DEF file. A DEF file can be used to make the linker do certain things such as export functions or configure the ordinal (exact location in the exported function table) of each exported function.
If you want to, you can export functions by ordinal only, instead of name. This can theoretically save memory / disk space because now, the function names are not embedded in the file. In a DLL with many functions, this can be a measurable difference. But in practical terms, this has become pointless. Storage and memory are measured in gigabytes. Worrying about saving individual bytes creates more problem than it solves, because if you're using ordinals only, many environments cannot call the exported functions since they require a name.
DEF files also allow you to do some trickery with function name aliasing, and exporting existing functions under another name or changing the ordinal. Unless you have to provide a DLL for extremely legacy purposes that relies on such things, I would not advise anyone to use DEF files.
Your header file is going to have to include the dllimport
directives anyway, and specify a calling convention and define the naming convention. You gain nothing by not having the dllexport
directive, but you have the added complexity of having to maintain DLL export related information in more than 1 location.
Conclusion
With the techniques described here, you can build DLLs that export functions. These DLLs can be used by client applications.
For the purpose of this discussion, I limited the article to the process of actually exporting functions. In my next article, I will talk about topics such as distribution of the DLL, the runtime, parameter passing and other topics that are important and related to this topic.
History
- 25th January, 2024: First version
- 3th March, 2024: corrected bad link for dependencies.