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

Step by Step: Calling C++ DLLs from VC++ and VB - Part 1

0.00/5 (No votes)
29 Feb 2004 7  
This series of articles is a step-by-step guide to constructing C++ DLLs that include C++ functions and C++ classes, and then calling the DLL functions and classes from VC++ and VB programs.

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:

// Test.cpp : Defines the entry point for the DLL application.
//

#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;
}


// This is an example of an exported variable
TEST_API int nTest=0;

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

// This is the constructor of a class that has been exported.
// see Test.h for the class definition
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:

// 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 TEST_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 
// TEST_API functions as being imported from a DLL, whereas this DLL
// sees symbols defined with this macro as being exported.
#ifdef TEST_EXPORTS
#define TEST_API __declspec(dllexport)
#else
#define TEST_API __declspec(dllimport)
#endif

// This class is exported from the Test.dll
class TEST_API CTest 
{
public:
    CTest(void);
    // TODO: add your methods here.
};

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:

// Test.cpp : Defines the entry point for the DLL application.
//

#include "stdafx.h"
#define TEST_EXPORTS                     // <===  ADD THIS LINE
#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:

    .
    .
    .
    // Set the icon for this dialog.  The framework does this automatically
    //  when the application's main window is not a dialog
    SetIcon(m_hIcon, TRUE);            // Set big icon
    SetIcon(m_hIcon, FALSE);        // Set small icon
    
    // code to test Test.dll function:
    int n = fnTest();                    // <=== ADD THIS LINE

    // code to test the CTest class:
    CTest test;                          // <=== ADD THIS LINE
    
    return TRUE;  // return TRUE  unless you set the focus to a control
}

Step 5

This code won't compile until I include the Test.h file from the DLL:

// TestExeDlg.cpp : implementation file
//

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

  1. 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.

  2. 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")   // <== add this line
    #pragma comment(lib, "Test.lib")                // <== add this line
#endif

// This class is exported from the Test.dll
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

  • Initial public release

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.

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