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++ class from a VB application
In Part 2, I talked about calling a function in a C++ DLL from a VB application. The nice thing about using DLLs in this way is that they encapsulate functions, and what you see from the outside is only the interface. In DLL2.cpp, there are actually two functions. But since one is declared static and does not appear in the DLL2.def file, an external application has no idea it even exists. Thus, there are no side effects and the DLL will be reusable in many projects.
It is the same way with C++ classes. Encapsulating them in DLLs is very useful - especially because this gives us an opportunity to use them in VB applications too. Here's how to do this:
Step 1
I start with the code from DLL2.cpp and add the CDLL3
class:
#include "stdafx.h"
#define DLL3_EXPORTS
#include "DLL3.h"
BOOL APIENTRY DllMain( HANDLE ,
DWORD ul_reason_for_call,
LPVOID
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
static inline unsigned __int64 GetCycleCount()
{
unsigned int timehi, timelo;
__asm
{
rdtsc
mov timehi, edx;
mov timelo, eax;
}
return ((unsigned __int64)timehi << 32) + (unsigned __int64)timelo;
}
CDLL3::CDLL3()
{
}
int CDLL3::GetCpuSpeed()
{
const unsigned __int64 ui64StartCycle = GetCycleCount();
Sleep(1000);
return static_cast<int>((GetCycleCount() - ui64StartCycle) / 1000000);
}
DLL2.h looks like:
#ifndef DLL3_H
#define DLL3_H
#ifdef DLL3_EXPORTS
#define DLL3_API __declspec(dllexport)
#else
#pragma message("automatic link to DLL3.LIB")
#pragma comment(lib, "DLL3.lib")
#define DLL3_API __declspec(dllimport)
#endif
class DLL3_API CDLL3
{
public:
CDLL3();
int GetCpuSpeed();
};
#endif
Note that I have gone back to the use of __declspec
because later on I want to test the DLL with a VC++ application. As the code stands right now, there's not much point in trying it with VB, because the only thing that is exported is the C++ class CDLL3
, and VB cannot deal with that.
Step 2
When a C++ program wants to use a C++ class, it must first create an instance of that class - either on the stack or on the heap. When the member functions of a class are then called, there is an implicit "this
" pointer that gets passed as the first parameter. Because VB doesn't understand C++ classes or "this
" pointers, there is no way for VB programs to use C++ classes directly.
What I can do, however, is call the C++ class methods from functions internal to the DLL. But first I must do one thing: I must get access to the class and deal the problem of the "this
" pointer. Here is the trick: I will implement functions in the DLL that can be called by a VB program. For each class method, there will be a corresponding VB wrapper function. In addition, there will be two more functions that will create and destroy an instance of the class, which will take care of the problem with the "this
" pointer.
The create and destroy functions are prototyped as follows:
void * __stdcall CreateDll3();
void __stdcall DestroyDll3(void * objptr);
Each of the wrapper functions includes the class object pointer as its first parameter:
int __stdcall GetCpuSpeedDll3(void * objptr);
TIP: I have named each of these functions with a "Dll3" suffix, to give a hint as to where these functions reside - you may use whatever naming conventions you wish.
To start using a C++ class, the VB program first calls CreateDLL3()
, which creates an instance of the class on the heap (via new
) and returns the pointer to the class object. The VB program passes the object pointer to each of the class wrapper functions (which correspond to the class methods). Inside the DLL, the class wrapper functions use the object pointer to access the class methods. Finally, the VB program calls DestroyDLL3()
when it is finished with the C++ class.
It sounds complicated, but it really isn't. Here's the new DLL3.cpp:
#include "stdafx.h"
#define DLL3_EXPORTS
#include "DLL3.h"
BOOL APIENTRY DllMain( HANDLE ,
DWORD ul_reason_for_call,
LPVOID
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
static inline unsigned __int64 GetCycleCount()
{
unsigned int timehi, timelo;
__asm
{
rdtsc
mov timehi, edx;
mov timelo, eax;
}
return ((unsigned __int64)timehi << 32) + (unsigned __int64)timelo;
}
CDLL3::CDLL3()
{
}
int CDLL3::GetCpuSpeed()
{
const unsigned __int64 ui64StartCycle = GetCycleCount();
Sleep(1000);
return static_cast<int>((GetCycleCount() - ui64StartCycle) / 1000000);
}
void * __stdcall CreateDll3()
{
return new CDLL3;
}
void __stdcall DestroyDll3(void * objptr)
{
CDLL3 *dll3 = (CDLL3 *) objptr;
if (dll3)
delete dll3;
}
int __stdcall GetCpuSpeedDll3(void * objptr)
{
CDLL3 *dll3 = (CDLL3 *) objptr;
if (dll3)
return dll3->GetCpuSpeed();
else
return 0;
}
Here's the new DLL3.h:
#ifndef DLL3_H
#define DLL3_H
#ifdef DLL3_EXPORTS
#define DLL3_API __declspec(dllexport)
#else
#pragma message("automatic link to DLL3.LIB")
#pragma comment(lib, "DLL3.lib")
#define DLL3_API __declspec(dllimport)
#endif
class DLL3_API CDLL3
{
public:
CDLL3();
int GetCpuSpeed();
};
void * __stdcall CreateDll3();
void __stdcall DestroyDll3(void * objptr);
int __stdcall GetCpuSpeedDll3(void * objptr);
#endif
Step 3
To use the wrapper functions in a VB program, I need to export them with the correct names. Once again, I will do this with a module definition (.DEF) file:
; DLL3.def - defines the exports for DLL3.dll
LIBRARY DLL3
DESCRIPTION 'A C++ dll that can be called from VB'
EXPORTS
GetCpuSpeedDll3
CreateDll3
DestroyDll3
Now DLL3 can be compiled.
Step 4
With the DLL3 wrapper functions defined, I can edit the VB3 program:
Private Declare Function CreateDll3 Lib "DLL3.dll" () As Long
Private Declare Sub DestroyDll3 Lib "DLL3.dll" (ByVal objptr As Long)
Private Declare Function GetCpuSpeedDll3 Lib "DLL3.dll" _
(ByVal objptr As Long) As Integer
Private Declare Sub InitCommonControls Lib "comctl32.dll" ()
Private Sub Form_Initialize()
InitCommonControls
ChDir App.Path
End Sub
Private Sub Command1_Click()
Dim nSpeed As Integer
Dim s As String
Dim objptr As Long
Screen.MousePointer = vbHourglass
objptr = CreateDll3()
nSpeed = GetCpuSpeedDll3(objptr)
DestroyDll3(objptr)
Screen.MousePointer = 0
s = nSpeed
Form1.Text1.Text = "GetCpuSpeedDll3() returned " + s
End Sub
Private Sub Form_Load()
Form1.Text1.Text = ""
End Sub
I run this program, click the button, and this is what I see:
OK! I've got a C++ class in a DLL, and I can call the class methods from VB by using wrapper functions. There's one question left: can I also call this DLL from a C++ program?
Step 5
I modify the EXE2 program from Part 2 and add code for two buttons; the first button will call the GetCpuSpeedDll3()
wrapper function, and the second button will call the class method CDLL3::GetCpuSpeed()
directly:
void CEXE3Dlg::OnButton1()
{
CWaitCursor wait;
void * objptr = CreateDll3();
int nSpeed = GetCpuSpeedDll3(objptr);
CString s;
s.Format(_T("GetCpuSpeedDll3() returned %d"), nSpeed);
m_Speed1.SetWindowText(s);
DestroyDll3(objptr);
}
void CEXE3Dlg::OnButton2()
{
CWaitCursor wait;
CDLL3 dll3;
int nSpeed = dll3.GetCpuSpeed();
CString s;
s.Format(_T("CDLL3::GetCpuSpeed() returned %d"), nSpeed);
m_Speed2.SetWindowText(s);
}
I compile this code and try out the buttons:
So, now I have one DLL that can be called from VC++ and VB programs; the VB program calls wrapper functions to access the C++ class methods, and the VC++ program can call either the wrapper functions or the class methods directly.
Step 6
With this DLL, I can now begin writing VC++ and VB programs to call its functions and class methods. But I would never put this DLL in a production environment or use it in a commercial application. Reason? I have no way of tracking this DLL. The file timestamp can be modified, and so is useless. What I need is a way to determine the DLL's version number, which can be tied in to a version control or bug anomaly tracking system.
Applications typically use an embedded resource based on the VS_VERSION_INFO
resource type. But how can I add this to DLL3? Here is the simplest way I have found to add a version resource to a DLL: find an application (EXE) with a version resource. Copy the application's resource (.RC) file to the DLL's directory, and rename it to DLL3.rc. Open the DLL3.rc file with your favorite text editor, and remove everything except what you see below - while you're at it, you can update the version information, too:
#include "afxres.h"
#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU)
#ifdef _WIN32
LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
#pragma code_page(1252)
#endif
VS_VERSION_INFO VERSIONINFO
FILEVERSION 1,0,0,1
PRODUCTVERSION 1,0,0,1
FILEFLAGSMASK 0x3fL
#ifdef _DEBUG
FILEFLAGS 0x1L
#else
FILEFLAGS 0x0L
#endif
FILEOS 0x4L
FILETYPE 0x1L
FILESUBTYPE 0x0L
BEGIN
BLOCK "StringFileInfo"
BEGIN
BLOCK "040904B0"
BEGIN
VALUE "CompanyName", "\0"
VALUE "FileDescription", "DLL3 Dynamic Link Library\0"
VALUE "FileVersion", "1, 0, 0, 1\0"
VALUE "InternalName", "DLL3\0"
VALUE "LegalCopyright", "Copyright (C) 2004 by Hans Dietrich\0"
VALUE "ProductName", "DLL3 Dynamic Link Library\0"
VALUE "ProductVersion", "1, 0, 0, 1\0"
END
END
BLOCK "VarFileInfo"
BEGIN
VALUE "Translation", 0x409, 1200
END
END
#endif
Save the changes, and open the Visual Studio DLL project. In File View, right-click on DLL3 files, select Add Files to Project..., and select the DLL3.rc file. Rebuild the DLL. In Windows Explorer, right-click on the DLL3.dll file, select Properties, and you will see a Version tab that looks like:
NOTE: Even though the DLL3.rc file includes AFXRES.H, it does not mean that the DLL is linked to any of the MFC libraries, or is calling any of the MFC code. AFXRES.H is necessary only to resolve some symbol definitions.
Key Concepts
- Use
__declspec(dllexport)
and __declspec(dllimport)
for exporting classes, to be able to use the DLL with a VC++ application.
- Use wrapper functions defined with
__stdcall
to allow VB access to class methods.
- Export the wrapper functions via a module definition (.DEF) file.
- Include a version resource in your DLL to keep track of changes.
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.