Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / Win32

Introduction to Native DLLs - Part 2: Exporting Functions

5.00/5 (8 votes)
25 Jan 2024CPOL8 min read 6.1K   131  
An introduction to exporting functions from DLLs
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.

Image 1

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.

PowerShell
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:

C++
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:

C++
//DynamicLibrary.h
__declspec(dllexport) int getAnswerToLife(void);

//DynamicLibrary.cpp
__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:

C++
//DynamicLibrary.h
#ifdef DYNAMICLIBRARY_EXPORTS
#define DYNAMICLIBRARY_API __declspec(dllexport)
#else
#define DYNAMICLIBRARY_API __declspec(dllimport)
#endif

DYNAMICLIBRARY_API int getAnswerToLife(void);

//DynamicLibrary.cpp
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.

Image 2

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.

Image 3

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:

PowerShell
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:

Image 4

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.

C++
//DynamicLibrary.h
#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

//DynamicLibrary.cpp
#ifdef __cplusplus
extern "C" {
#endif

// This is an example of an exported function.
DYNAMICLIBRARY_API int getAnswerToLife(void)
{
    return 42;
}

#ifdef __cplusplus
}
#endif

If we compile that and run the script, we get the expected result:

Image 5

If we now look at the DLL dependencies, we see this:

Image 6

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:

C++
//DynamicLibrary.h
#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

//DynamicLibrary.cpp
#ifdef __cplusplus
extern "C" {
#endif

// This is an example of an exported function.
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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)