Introduction
This series of articles discusses four common situations when working with DLLs:
Part 1 |
Calling a DLL C++ function from a VC++ application |
Calling a DLL C++ class from a VC++ application |
Part 2 |
Calling a DLL C++ function from a VB application |
Part 3 |
Calling a DLL C++ class from a VB application |
Part 4 |
Loading a C++ DLL dynamically from a VC++ application |
Calling a DLL C++ function or class from a VC++ application
Visual Studio 6 makes it very easy to create C++ DLLs that contain functions or C++ classes.
Step 1
Open Visual Studio and go to File | New:
Select Win32 Dynamic Link Library, enter a project name, and hit OK.
Select A DLL that exports some symbols and hit Finish. You will see the following files in the File View:
Step 2
Inside Test.cpp, you will see this code:
#include "stdafx.h"
#include "Test.h"
BOOL APIENTRY DllMain( HANDLE 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;
}
TEST_API int nTest=0;
TEST_API int fnTest(void)
{
return 42;
}
CTest::CTest()
{
return;
}
Test.cpp contains code for fnTest
and CTest::CTest
. If you compiled Test.dll at this point, you would have a DLL that could be called directly by other VC++ applications. The key mechanism that allows other VC++ applications to call Test.dll is contained in Test.h:
#ifdef TEST_EXPORTS
#define TEST_API __declspec(dllexport)
#else
#define TEST_API __declspec(dllimport)
#endif
class TEST_API CTest
{
public:
CTest(void);
};
extern TEST_API int nTest;
TEST_API int fnTest(void);
What is going on here? What does #ifdef TEST_EXPORTS
mean, and where is TEST_EXPORTS
defined?
What is happening is that, depending on whether TEST_EXPORTS
is defined, TEST_API
will be defined to export or import the DLL symbols. The export process results in the DLL's symbols being defined in Test.lib, which can be linked to any VC++ application that wants to access the DLL. When compiling Test.dll, I want to export, which means I want TEST_EXPORTS
to be defined.
Step 3
Where is TEST_EXPORTS
defined? This is one of the things I don't like about the DLL wizard - it puts the definition of TEST_EXPORTS
on the command line. Go to Project | Settings | C/C++ | General, and you will see the Project Options:
While this method certainly works, it is obscure, and may cause maintenance problems later on. I prefer to make explicit the definition of TEST_EXPORTS
. I do this by deleting /D "TEST_EXPORTS" from the Project Options, and then changing Test.cpp to define TEST_EXPORTS
:
#include "stdafx.h"
#define TEST_EXPORTS
#include "Test.h"
BOOL APIENTRY DllMain( HANDLE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved)
{
.
.
.
Notice that the #define TEST_EXPORTS
is before the line #include "Test.h"
, so that the definition will be seen in the include file. Now Test.dll compiles just as before, and you will have a DLL that can be called from other VC++ applications.
Step 4
How can I call the functions in this DLL? To illustrate this, I create a demo app using Visual Studio. Select MFC AppWizard (exe), enter a project name, and click OK. Select Dialog based, then click Finish. Open the file XXXDlg.cpp, where XXX is the project name. Go to the OnInitDialog()
method, and enter the following code:
.
.
.
SetIcon(m_hIcon, TRUE);
SetIcon(m_hIcon, FALSE);
int n = fnTest();
CTest test;
return TRUE;
}
Step 5
This code won't compile until I include the Test.h file from the DLL:
#include "stdafx.h"
#include "TestExe.h"
#include "TestExeDlg.h"
#include "Test.h" // <=== ADD THIS LINE
.
.
.
Step 6
If you are doing a quickie demo, you will probably be tempted just to copy the DLL's Test.h into your exe project directory, so the compiler will be able to find it. For larger projects, this is not a good idea, because there is too much risk that you will update the DLL file, but forget to copy it into your exe directory. There is a simple way to solve this: go to Project | Settings | C/C++ | Settings | Preprocessor, and add to the Additional include directories field:
Note: This assumes that the DLL project and the EXE project have the same parent directory.
Now when I compile, I get ... linker errors!
Deleting intermediate files and output files for project 'TestExe - Win32 Debug'.
--------------------Configuration: TestExe - Win32 Debug--------------------
Compiling resources...
Compiling...
StdAfx.cpp
Compiling...
TestExe.cpp
TestExeDlg.cpp
Generating Code...
Linking...
TestExeDlg.obj : error LNK2001: unresolved external symbol "__declspec(dllimport)
public: __thiscall CTest::CTest(void)" (__imp_??0CTest@@QAE@XZ)
TestExeDlg.obj : error LNK2001: unresolved external symbol "__declspec(dllimport)
int __cdecl fnTest(void)" (__imp_?fnTest@@YAHXZ)
Debug/TestExe.exe : fatal error LNK1120: 2 unresolved externals
Error executing link.exe.
TestExe.exe - 3 error(s), 0 warning(s)
Step 7
Although I told the compiler about the DLL symbols, I also must tell the linker. Go to Project | Settings | Link and add the DLL's LIB file to the Object/library modules field:
Step 8
Now I get a clean compile. Before running the application, there is one more thing to do: copy Test.dll to the EXE's directory.
Step 9
I can then put a breakpoint in the OnInitDialog()
method, and hit Go (F5):
and I see that fnTest
has returned 42 as expected. The CTest
class can be tested in a similar way.
Key Concepts
- The Visual Studio project wizard provides a good starting point for creating a VC++ DLL.
- Functions, classes, and variables can be exported from the DLL.
- Using
#define
preprocessor definitions, one include file can be used by both the DLL and the application.
- The DLL exports its symbols, and the application imports the DLL symbols. When compiling the application, the compiler sees the DLL symbols via an include file (Test.h). When linking the application, the linker sees the DLL symbols via the import library (Test.lib).
- The DLL must be in the same directory as the EXE when running the application. In early versions of Windows, it was acceptable practice to put application DLLs in the Windows or System directories, but this is now recognized to cause problems, and should not be done.
Comments
In practice, I never use the approach described in Step 7. For large projects, the number of DLLs and LIB files quickly becomes unmanageable if handled in this way. What you want to do is set up project lib and bin directories, where you can keep copies of all current LIB, DLL, and EXE files. How do you tell the linker where to find these LIB files? There are two ways to do this in Visual Studio:
- Go to Tools | Options | Directories and set Show directories for to "Library files". Now enter the path to where your project's LIB files are.
- Alternatively, go to Project | Settings | Link, select the Input category, and enter the Additional library path for your project's LIB files.
Which method is better? That depends on the situation in your development environment. Method 1 means all projects on a machine will share the same set of directory paths, but each developer's Visual Studio must be set up with those paths.
Method 2 means that the paths can be customized for each project, and will be stored inside the project, so any developer can check out the project and begin development - provided the same paths exist on the developer's machine.
It is common to establish standard drive+directory mappings, to eliminate endlessly tweaking the directory paths on each developer's machine.
I still haven't discussed how to specify what LIB files to use. Here is my trick: In the DLL's Test.h, add two lines. The DLL1.h file now looks like:
#ifdef TEST_EXPORTS
#define TEST_API __declspec(dllexport)
#else
#define TEST_API __declspec(dllimport)
#pragma message("automatic link to Test.lib")
#pragma comment(lib, "Test.lib")
#endif
class TEST_API CTest
{
public:
CTest(void);
};
extern TEST_API int nTest;
TEST_API int fnTest(void);
This ensures that when a project includes the DLL's header file, it will also automatically link to the DLL's LIB file. The developer is notified of this by the #pragma message
, which appears in the output window of Visual Studio.
Demo
The EXE1.exe demo tests the GetCpuSpeed()
function and CDLL1::GetCpuSpeed()
class method in DLL1.dll:
Revision History
Version 1.0 - 2004 February 29
Usage
This software is released into the public domain. You are free to use it in any way you like, except that you may not sell this source code. If you modify it or extend it, please to consider posting new code here for everyone to share. This software is provided "as is" with no expressed or implied warranty. I accept no liability for any damage or loss of business that this software may cause.