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 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:
Set the 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.
#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
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.
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.
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".
Adding this file will "unmangle/undecorate" the exported function name.
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.
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
In order to access it, you may write:
HMODULE hLoadedLibrary = LoadLibrary("DemoLib4.dll");
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
.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 "C"
{
__declspec(dllexport) int __stdcall Mul(int a, int b);
}
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