Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

DllExports - Common Problems and Solutions

0.00/5 (No votes)
8 Aug 2014 1  
Learn how to export functions from a DLL, and obtain full language independence

Screenshot of DemoAppNative.exe

Introduction

There are numerous guides on how to export functions from DLLs, but somehow when you start doing DLLs yourself, you run into problems. If you create a DLL in one language and want to use it from another, there are some pitfalls to be aware of. We will look at some of them.

The key to solving problems is really understanding what is going on. I am going to naively start with a simple C++ DLL, and try to lead us into some of the most common problems, Then we will make modifications to the DLL to remedy the problems.

Background

Sooner or later, you will find the need to use a DLL with exported functions. Either using a third party library or exposing a public API yourself to a third party.

Let's go back a few years. Back to where VB6 was popular. VB6 was a fantastic language for doing rapid prototyping in. You would learn how to work with GUI code in just a couple of days. VB6 was initially compiled into Pcode, which was interpreted at runtime. Later on, the possibility to compile this code into an EXE was added.

VB6 was quite slow, and lacked elementary things like unsigned integers and bit shifting, and I think without being able to use DLLs made in other languages, VB6 would never have had as much success as it did.

When speed really was needed someone just made a DLL in C or C++, or in any other language. Creating GUI in VB6 was a pleasure compared to doing it with GDI or MFC. You could have the best of both worlds.

How could VB6 call C++ DLLs? Well. Let's consider an assembly example where we add 2 numbers, stored in registers AX and BX.

int Add(int a, int b)
{
__asm
  {
    ADD AX, BX     // AX = AX + BX
    RET
  }
}

To call this from VB6 any other language. The only thing we need to do is jump to the code location and set AX and BX. The way parameters are passed to a function is called "Calling convention". Some conventions pass parameters via the stack, others through registers, or a combination of both. VB6 uses a calling convention called __stdcall. It is also the standard win32 convention. Parameters are pushed on the stack from right to left, the return value is stored in AX, and the function is responsible for balancing the stack. C and C++ compilers uses by default __cdecl calling convention, which is similar to __stdcall, but the caller of the function is responsible for balancing the stack. In order to make VB6 able to call exported function, we need to change the calling convention to __stdcall.

I use VB6 as example here, but I would recommend to always use the __stdcall convention in order to be more language independent, and conform to the operating systems convention.

Creating a DLL Project with a Minimal Stub

This article is primarily about managing the exported names and ordinals. I will be very brief, and just quickly show how the demo DLLs were created.

Select a new C++ win32 Console Application:

Selecting a console project

Set the type to "DLL":

Changing type to DLL

DLLMain

Not only programs have a Main function, but also DLLs. Here, you typically allocate and initialize resources and free resources. The one below is autogenerated, and I did no modifications to it.

// dllmain.cpp : Defines the entry point for the DLL application.
#include "stdafx.h"

BOOL APIENTRY DllMain( HMODULE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved
                     )
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}

__declspec(export)

There is very little you need to do to export a function. You only need to decorate it with __declspec(dllexport). I will combine it with the __stdcall attribute, to change the calling convention.

__declspec(dllexport) int __stdcall Add(int a, int b)
{
  return a + b;
}

When you compile, a DLL and a .lib file will be created. If you want the DLL to be automatically loaded into your application at run-time, you need to add the .lib file when you link your application.

From an application, you need to add a reference to the .lib file either in the project settings or programmatically via a pragma link directive. I personally prefer the pragma directive, because project settings are per build configuration, and you have to remember to update all configurations.

#include "../DemoLib1/DemoLib1.h" //  __declspec(dllimport) int __stdcall Add(int a, int b);

#ifdef _DEBUG
#pragma comment(lib, "../Debug/SimpleMath.lib")
#else
#pragma comment(lib, "../Release/SimpleMath.lib")
#endif

int main()
{
  int result = Add(1,2);
}

This is the most basic form of function export/import via a DLL, but I wouldn't qualify it as a good DLL. This DLL has very poor interoperability with other compilers and languages.

One Step Towards Interoperability

If our intention was to use the DLL from C, Pascal, VB6, C# or some other language we will be dissappointed. This DLL is only useable from C++.

Let's use bindump.exe, a tool part of the Windows SDK and Visual Studio, to see why other languages don't like the DLL.

bindump.exe /EXPORTS 

Output from dumpbin on DemoLib1.dll

The name Foo is C++ mangled. Type information has been encoded into the function name by the C++ linker. This is ok, as long as your application also is written in C++ and you compile it with the same compiler and linker. The C language does not mangle the names the same way C++ does.

Remember the extern "C" keyword for making C/C++ interoperate.

extern "C"
{
  __declspec(dllexport) int __stdcall Foo(int a, int b);
}

Now the exported names are not C++ mangled anymore.

Output from dumpbin on DemoLib2.dll

Now the name is "_Foo@8". Why is the name _Foo@8 instead of just Foo? Well, now it is C language mangled. C decorates its functions with a leading underscore, followed by the name of the function, and it ends the name with the size in bytes of the parameters. We are passing 2 integers (4 bytes). So the total size is 8. You can now use the DLL from both C and C++, instead of just C++ in the previous example. This is an improvement, but still it is not generic enough.

The calling convention is strictly specified, but the decorated name is more loosely specified. There are languages that don't decorate the name at all. We might be able to call this method using the decorated name instead, by running dumpbin.exe on the DLL and lookup the decorated name, but that name may change if you change compiler, or add/modify any of the parameters. This is a huge drawback.

If you look closely, the mangled name "_Foo@8" is repeated twice. _Foo@8 = @ILT+235(_Foo@8) The one of the left is the exported name, and the one on the right is the internal name. At this moment, they are the same. In the next section, we will learn how to change the exported name.

One Step Back, Two Steps Forward - Use a .def File

.def files gives you more control of how functions are exported. I don't know how some people writing guides on DLLs fail to mention them. I always add one.

Add a file preferably ending with .def to your project. The name doesn't matter. I named my file exports.def, because that is what I always do.

Adding a .def file to the project

The export file should have the following format:

LIBRARY DemoLib3
EXPORTS
   Foo   @1

Go into the property page for the project, and add it under "linker input".

Updating project linker settings to use the .def file

Adding this file will "unmangle/undecorate" the exported function name.

Output from dumpbin where function name is not decorated

What More Can a .def File Do

Functions that are exported from a DLL are assigned a number and optionally a name. For public functions, it makes sense to add a name. For internal functions that are not intended for public use, you can assign them a number, but leave out the name. Let's have a look at the exports from USER32.dll.

Mangeled names

There are 1062 exported functions, but only 894 named functions. That means that you can only access them by Ordinal. The Ordinal is usually a number starting from 1. But it may optionally start from a different number, like 1502.

Let's create a DLL ourselves. One that exports two functions. The public function Foo, which is exported by name and ordinal. We will also add a private function Foo, that can only be accessed by ordinal. To make it more interesting, we will make the ordinals start at 1502, and we will have a hole in the number series, and give Bar the ordinal 1505.

In order to hide the name, we will use the NONAME directive.

LIBRARY DemoLib4
  EXPORTS
     Foo   @1502
     Bar   @1505  NONAME

Mangeled names

In order to access it, you may write:

HMODULE hLoadedLibrary = LoadLibrary("DemoLib4.dll");

// Notice __stdcall on function pointer typedef
typedef int (__stdcall* BarFunc)(int, int); 
   
BarFunc Bar = (BarFunc) GetProcAddress(hLoadedLibrary, (LPCSTR)1505);
int result = Bar(6,3);
printf("#1505(6,3) = %i\n", result);
FreeLibrary(hLoadedLibrary);

It looks strange, but you simply pass the Ordinal number instead of the exported name.

Interoperability with .NET and Other Languages

In the DemoAppManaged.exe project, I will import and use all three of the demo libraries:

  • DemoLib1 has a C++ mangled function name
  • DemoLib2 has a C mangled name
  • DemoLib3 has no mangling at all

Mangeled names

.NET is flexible, it seems to accept the name "Foo" also for the C mangled name, but other languages may not be so forgiving. So my recommendation is to export the names unmangled/undecorated. This is also how all the functions in the win32 libraries are exported.

VB6 Interoperability

At request, I will also mention interoperability with VB6.

The declaration is straight-forward, with a few pitfalls to lookout for. Below you will See the C++ exported function, followed by the same declaration in VB6.

// extern is used in order to export it as a C function
// __stdcall to is used to conform to calling convention
extern "C"
{
  __declspec(dllexport) int __stdcall Mul(int a, int b);
}

// VB6 
Private Declare Function MyMul Lib "MoreMath.dll" Alias "Mul" _
(ByVal op1 As Long, ByVal op2 As Long) As Long

The VB6 code looks very similar to PInvoke declarations in .Net. You define a name for the function "MyMul", and specify both the DLL name, and the real exported name(called alias) "Mul".

The hard part is how to declare the parameters. The function is declared with Integer in C++, but why didn't I use Integer in VB6? The reason is simple. The two languages don't define Integer the same way. So we need to choose a datatype that is its equivalent. In VB6, Integers are 16 bits, and Longs are 32 bits. In C++ the general rule is that the size follows the size of the registers of the cpu. So Integers are 32 bits for the x86 architecture.

For further reading regarding VB6 declarations, please look at the following link Declare Statement

History

  • 25th May, 2014 - First version
  • 17thJune, 2014 - Second version
  • 8th Aug, 2014 - Third version

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here