This article briefly explains what a native
Windows Dynamic Link Library (DLL) is, shows how to create a DLL using C++, how to consume it in C# and then explains how DLLs work. There are
many types of DLLs but all of them are native DLLs, except the term
native should only be used for DLLs that do not have a Type Library (COM) or CLI
Metadata (.Net Class Library). If it is possible to create a reference to a DLL using the Visual
Studio Add References window then the DLL should not be called a
native DLL.
When a C# program calls a native DLL it must use the DllImportAttribute class
(DllImport for short), as described here. Use of DllImportAttribute/DllImport is
also called Pinvoke.
A native Windows DLL is not a .Net Class Library.
A .Net Class Library is a DLL with CLI Metadata and other additional things. A
native DLL can only have unmanaged code; in other words, a compiled native DLL
has native (x86) machine instructions but cannot have Common Intermediate Language (CIL)
virtual machine instructions (CIL virtual machine instructions are also called Microsoft Intermediate Language
(MSIL) instructions). There are many other articles
explaining .Net Class Libraries using C# and other .Net languages. Unless a .Net
Class Library is built using C++ it cannot have native (x86) machine
instructions in it. To create
a .Net Class Library using C++ see my article
Managed C++ Wrapper For Unmanaged Code.
Understanding native DLLs
is not a requirement for C# and Common Language Infrastructure programming. (CLI
is often called .Net.) This article is intended for C# programmers unfamiliar with
C++ that want to understand native DLLs and are brave enough to explore them. This article assumes you at least
know how to create and use a C# Console Application project. I intend to explain
in this article how to create a C++ DLL project you can use for educational purposes even if you
are unfamiliar with C++.
What a Dynamic Link Library Is
A DLL file is separate from other executable files. The "Dynamic Link" portion of
Dynamic Link Library refers to the fact that the functions and variables are
linked dynamically. In contrast, a static library contains unmanaged code that
is combined with other unmanaged code into a single executable. Programmers
unfamiliar with C++ and unmanaged code might have difficulty understanding the
concept of static libraries and static linking. When code that is statically
linked is changed, every executable that uses that code must be updated. The
majority of the Windows API exist in native Windows DLLs. Imagine how
complicated it would be to update Windows if every Windows
program (executable) had to be updated for every patch or fix for Windows.
Note that when a DLL project is built, there is a file with a "lib" extension
created that is used by C/C++ projects but not by C#.
A DLL is a library of:
- an optional entry-point function (DllMain) for the DLL
- exported functions and data (variables)
- internal functions
and data
The DllMain entry-point function is called once when each of the following
happens to the DLL:
- a process loads the the DLL
- a process unloads the the DLL
- a thread loads the DLL
- a thread unloads the DLL
This provides the DLL the opportuinty to initialize and uninitialize itself as
needed. Note that there are limits to what can be or should be done in the
DllMain function; Windows API functions cannot be called in the DllMain function
except for those in Kernel32.dll. These limitations only apply to the DllMain
function. The DllMain function must return TRUE unless it intends to indicate
that the DLL has failed to load properly.
Creating the DLL
We will begin by creating the C++ DLL project. In Visual Studio start with:
"File" -> "New" -> "Project..."
You will then get the familiar "New Project" window as in:
Then in the "New Project" window:
- In the left pane under "Installed Templates" (the default) expand "Visual C++".
- In the middle pane select "Win32 Project".
- Near the bottom give the project a name: I used "SampleNativeDLL".
- Change the Location if you need to or want to.
- Click "OK".
You will then get the "Welcome to the Win32 Application Wizard" window as in:
Just click "Next >" in the "Welcome to the Win32 Application Wizard" window.
The next window will be the "Application Settings" window as in:
In it:
- For "Application type" select "DLL". The "Additional options" will change.
- For "Additional options" select "Export symbols". You will get very little
sample code without this.
- Leave all the other settings as the default setting.
- Click "Finish".
The project will be generated and since we selected "Export symbols" the
generated code will include additional sample code. The sample code will show
how to define:
- a variable (nSampleNativeDLL)
- a function that is outside of a class (fnSampleNativeDLL)
- a class (CSampleNativeDLL)
And how to export them for use by other applications.
A class cannot be exported from a DLL if the DLL is to be used by any language
other than C++. So remove the CSampleNativeDLL class. To remove the
CSampleNativeDLL class, in the "SampleNativeDLL.h" file remove:
// This class is exported from the SampleNativeDLL.dll
class SAMPLENATIVEDLL_API CSampleNativeDLL {
public:
CSampleNativeDLL(void);
// TODO: add your methods here.
};
In the "SampleNativeDLL.cpp" file remove:
// This is the constructor of a class that has been exported.
// see SampleNativeDLL.h for the class definition
CSampleNativeDLL::CSampleNativeDLL()
{
return;
}
We can now tell C++ that we want the exported function and variable to be
accessible to the C language, because then they will be accessible to other
languages too. To do that, near the top of the
"SampleNativeDLL.cpp" file, after the two #include statements, add the following line:
extern "C" {
Then at the bottom of the file add a line with "}".
In other words, the top of the "SampleNativeDLL.cpp" file should look
like the following:
// SampleNativeDLL.cpp : Defines the exported functions for the DLL application.
//
#include "stdafx.h"
#include "SampleNativeDLL.h"
extern "C" {
Be sure to put the closing "}" at the bottom. Then do the same for the
"SampleNativeDLL.h" file except since there are no #include statements in the
SampleNativeDLL.h file, the extern "C" can go just below the comments at the top
of the file. Then put the closing "}" at the bottom.
Creating the Console Application
Next we will create a C# application to use the DLL. We will keep it very simple
so we will make it a Console applicaiton. I will assume you know how to do that.
Be sure to change the language in the left (templates) pane back
to C# from C++.
After creating the Console project, add the following two lines to the "Program"
class:
[System.Runtime.InteropServices.DllImport("SampleNativeDLL.dll")]
public static extern int fnSampleNativeDLL();
Then add the following line to the "Main" nethod:
Console.WriteLine(fnSampleNativeDLL());
If you build and run the program now then during execution you will get the error
that the DLL could not be loaded. That is because the Console Application does
not know where the DLL is at. There are many possible solutions but for this
article we will simply copy the DLL. There are many ways to do that but here is
one. In Windows Explorer go to the directory where the DLL project is at. There
will be one subdirectory called "Debug"; go to it and you will see the DLL; copy
it. Then go to the directory where the Console Application project is at. Go to
the "bin" subdirectory and then the "Debug" subdirectory of that. Paste the DLL
there. Now when you execute the Console Application you should get "42" written
to the console. That will happen because the Console Application is calling the
fnSampleNativeDLL function in the DLL and that function is returning 42.
Explanation of the DLL Code
If you look at the generated DLL project then it will look as in the following:
The "ReadMe.txt" file is created by Visual Studio when the project is generated to
describe the generated files. You can ignore the "External Dependencies". The
remaining two folders are "Header Files" and "Source Files".
For the purposes of this article we will ignore the files "targetver.h",
"stdafx.h" and "stdafx.cpp".
The following is the contents of the "SampleNativeDLL.h" file
after the modifications I described previously:
extern "C" {
// The following ifdef block is the standard way of creating macros which make exporting
// from a DLL simpler. All files within this DLL are compiled with the SAMPLENATIVEDLL_EXPORTS
// symbol defined on the command line. This symbol should not be defined on any project
// that uses this DLL. This way any other project whose source files include this file see
// SAMPLENATIVEDLL_API functions as being imported from a DLL, whereas this DLL sees symbols
// defined with this macro as being exported.
#ifdef SAMPLENATIVEDLL_EXPORTS
#define SAMPLENATIVEDLL_API __declspec(dllexport)
#else
#define SAMPLENATIVEDLL_API __declspec(dllimport)
#endif
extern SAMPLENATIVEDLL_API int nSampleNativeDLL;
SAMPLENATIVEDLL_API int fnSampleNativeDLL(void);
}
Originally the primary need for header files is the result of the C language
requiring that functions and variables be defined (in the file) before use. So a header
file has the signatures (prototypes) of functions. Header files have what is
required by any C/C++ file to use the functions defined in the header. The explanation of header files is not directly relevant to DLLs. What is relevant is that the
"SampleNativeDLL.h" file has the following in it:
SAMPLENATIVEDLL_API int fnSampleNativeDLL(void);
That is the signature (prototype) of the fnSampleNativeDLL function. Note the use
of the "SAMPLENATIVEDLL_API" macro. After the compiler processes the "SAMPLENATIVEDLL_API"
macro the fnSampleNativeDLL function is defined as:
__declspec(dllexport) int fnSampleNativeDLL(void);
The purpose of "__declspec(dllexport)" is to export the function for use by other
programs. A "__declspec(dllimport)" would be used to import
a function that is exported from a DLL. In C#, DllImportAttribute
(DllImport for short) essentially serves the same purpose as
"__declspec(dllimport)" in C/C++.
The following is the contents of the "SampleNativeDLL.cpp" file after the
modifications I described previously:
// SampleNativeDLL.cpp : Defines the exported functions for the DLL application.
//
#include "stdafx.h"
#include "SampleNativeDLL.h"
extern "C" {
// This is an example of an exported variable
SAMPLENATIVEDLL_API int nSampleNativeDLL=0;
// This is an example of an exported function.
SAMPLENATIVEDLL_API int fnSampleNativeDLL(void)
{
return 42;
}
}
Note that that implements the "fnSampleNativeDLL" function. The function just
returns the number "42".
The only file remainng to be described is "dllmain.cpp". The following is the
contents of 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;
}
It has one function called DllMain that has previously been
described.
The contents of the SampleNativeDLL.cpp and dllmain.cpp files could be combined
into one file; they are made separate for our convenience.