Environment:
Visual C++
At one point
in time, before COM,
before ATL,
programmers used ordinary .DLLs
instead. You could do a lot with a .DLL.
If you had several programs that used the same functions
or other resources, you could save space by putting those
resources in a .DLL.
Putting code used by multiple programs in a single .DLL
often saved maintenance time because the code was all in
one place. Fixes and other modifications would only have
to be done one time. If you had a program which needed to
run different routines at different times, you could put
those routines into .DLLs and have the application
load the appropriate .DLL
when it was needed. There were lots of good reasons to use
.DLLs.
There are
still a lot of good reasons to use .DLLs.
They haven't gone away. Sure, whatever you can do with a .DLL,
you can probably do with a COM
object. Granted, there are a number of
shortcomings to .DLLs,
some of them serious, which is why we ended up with COM
in the first place. Still, .DLLs
remain a very useful tool. Compared to COM
and ATL, they are much
easier to make. Learning COM
or ATL requires a
serious investment of time and effort. Making a .DLL
is relatively easy. Modifying one is easy too. If you know
some C++ and MFC,
you could be making .DLLs
today.
This article
will review the types of .DLLs
you can make with MFC,
including when to use each type and how to make them. In
the next article there will be a discussion of the
limitations of .DLLs
(which led to the rise of COM
and ATL), and how
these can be partially avoided. In the third article,
there will be more coding details and examples.
Different
types of .DLLs
There are
two kinds of .DLLs you can make using MFC:
an MFC extension .DLL
or a regular .DLL. Regular
.DLLs in turn come in
two varieties: dynamically linked
or statically linked.
Visual C++ also allows you to make a generic
Win32 .DLL, but in this article I'm only going to discuss
the MFC-based .DLL
types.
MFC
extension .DLLs
Every .DLL
has some kind of interface. The interface is the set of
the variables, pointers, functions or classes provided by
the .DLL which you can
access from the client program. They are the things that
allow the client program to use the
.DLL. An MFC extension
.DLL can have a C++ style interface. That is,
it can provide ("export")
C++ functions and
entire C++ classes to
be used by the client application. The functions it
exports can use C++ or
MFC data types as
parameters or as return values. When it exports
a class, the client will be able to create objects of that
class or derive new classes from it. Inside the .DLL,
you can also use MFC
and C++.
The MFC
code library used by Visual C++
is stored in a .DLL.
An MFC extension .DLL
dynamically links to the MFC
code library .DLL. The
client application must also dynamically link to the MFC
code library .DLL. As the years have gone by
the MFC library has grown. As a result, there are a few
different versions of the MFC code library .DLL out there.
Both the client program and the extension .DLL must be
built using the same version of MFC. Therefore, for an MFC
extension .DLL to work, both the extension .DLL and the
client program must dynamically link to the same MFC code
library .DLL, and this .DLL must be available on the
computer where the application is running.
Note: If you
have an application which is statically linked to MFC, and
you wish to modify it so that it can access functions from
an extension .DLL, you can change the application to
dynamically link to MFC.
In Visual C++, select
"Project | Settings" from the menu. On the
"General" settings tab you can change your
application to dynamically link to MFC.
MFC
extension .DLLs are very small. You can build an extension
.DLL which exports a few functions or small classes and
has a size of 10-15 KB. Obviously, the size of your .DLL
depends on how much code you store in it, but in general MFC
extension .DLLs are relatively small and quick
to load.
Regular
.DLLs
The MFC
extension .DLL only works with MFC client applications. If
you need a .DLL that can be loaded and run by a wider
range of Win32 programs, you should use a regular .DLL.
The downside is that your .DLL and your client application
cannot send each other pointers or references to MFC-derived
classes and objects. If you export a function, it cannot
use MFC data types in its parameters or return values. If
you export a C++ class, it cannot be derived from MFC. You
can still use MFC inside your .DLL, but not in your
interface.
Your regular
.DLL still needs to have access to the code in the MFC
code library .DLL. You can dynamically link to this code
or statically link. If you dynamically link, that means
the MFC code your .DLL needs in order to function is not
built into your .DLL. Your .DLL will get the code it needs
from the MFC code library .DLL found on the client
application's computer. If the right version of the MFC
code library .DLL is not there, your .DLL won't run. Like
the MFC extension .DLL, you get a small .DLL (because the
.DLL doesn't include the MFC code), but you can only run
if the client computer has the MFC code library .DLL.
If you
statically link to the MFC code library, your .DLL will
incorporate within itself all the MFC code it needs. Thus,
it will be a larger .DLL, but it won't be dependent on the
client computer having the proper MFC code library .DLL.
If you can't rely on the host computer having the right
version of MFC available, this is the way to go. If your
application users are all within your own company, and you
have control over what versions of the MFC .DLLs are
lurking on their computers, or if your installation
program also loads the right MFC .DLL, this might not be
an issue.
Building
a .DLL
You can make
an MFC-based .DLL with the App Wizard. Select "File |
New" from the menu. On the "Projects" tab,
select "MFC AppWizard (.DLL)." Pick a name for
your new project and click "OK." On the next
screen, you will have the choice to create an MFC
extension .DLL, a regular .DLL "using
shared MFC .DLL" (i.e., a regular .DLL dynamically
linked to MFC), or a regular .DLL statically linked to MFC.
Pick the one you want and click "Finish."
App Wizard
builds a .DLL which doesn't do anything. The new .DLL will
compile, but since it doesn't export any classes or
functions yet, it is still essentially useless. You now
have two jobs: (1) add functionality to make your .DLL
useful; and (2) modify your client application to use your
.DLL.
Export
a class
Once you're
done with the App Wizard, you can add classes to your .DLL
by adding the .cpp and .h files from another project, or
you can create them from scratch within your current
project. To export a class, you add "__declspec(dllexport)"
to the class declaration so it looks like this:
class __declspec(dllexport) CMyClass
{
};
If you are
making an MFC extension .DLL,
you can instead use the AFX_EXT_CLASS
macro:
class AFX_EXT_CLASS CMyClass
{
};
There are
other ways to export a class, but this is the easiest. If
your exported class requires a resource which is located
in the .DLL, for example a class derived from CDialog, the
process is more involved. I'll cover this subject in
tutorial #3. Below I'll discuss what to do to the client
application so that it can use your exported class.
Export
variables, constants and objects
Instead of
exporting a whole class, you can have your .DLL export a
variable, constant or object. To export a variable or
constant, you simply declare it like this:
__declspec(dllexport) int MyInt;
__declspec(dllexport) extern const COLORREF MyColor =
RGB(50,50,50);
When you
want to export a constant, you must use the
"extern" specifier. Otherwise you will get a
link error.
You can
declare and export a class object in the exact same
manner:
__declspec(dllexport) CRect MyRect(30, 30, 300, 300);
Note that
you can only export a class object if the client
application recognizes the class and has its header file.
If you make a new class inside your .DLL, the client
application won't recognize it without the header file.
When you
export a variable or object, each client application which
loads the .DLL will get its own copy. Thus, if two
different applications are using the same .DLL, changes
made by one application will not affect the other
application.
It's
important to remember that you can only export objects and
variables which are of global scope within your
.DLL. Local objects and variables cease to exist when they
go out of scope. Thus, if your .DLL included the
following, it wouldn't work.
MyFunction( )
{
__declspec(dllexport) CSomeClass SomeObject;
__declspec(dllexport) int SomeInt;
}
As soon as
the object and variable go out of scope, they will cease
to exist.
Export
a function
Exporting
functions is similar to exporting objects or variables.
You simply tack "_declspec(dllexport)"
onto the beginning of your function prototype:
__declspec(dllexport) int SomeFunction(int);
If you are
making an MFC regular .DLL which will be used by a client
application written in C, your function declaration should
look like this:
extern "C" __declspec(dllexport) int SomeFunction(int);
and your
function definition should look like this:
extern "C" __declspec(dllexport) int SomeFunction(int x)
{
}
If you are
building a regular .DLL which is dynamically linked
to the MFC code library .DLL, you must insert the
AFX_MANAGE_STATE macro as the first line of any exported
function. Thus, your function definition would look like
this:
extern "C" __declspec(dllexport) int AddFive(int x)
{
AFX_MANAGE_STATE(AfxGetStaticModuleState( ));
return x + 5;
}
It doesn't
hurt to do this in every regular .DLL. If you you switch
your .DLL to static linking, the macro will simply have no
effect.
That's all
there is to exporting functions. Remember, only an
MFC extension .DLL can export functions with
MFC data types in the parameters or return value.
Export
a pointer
Exporting an
uninitialized pointer is simple. You do it the same way
you export a variable or object:
__declspec(dllexport) int* SomeInt;
You can also
export an initialized object this way:
__declspec(dllexport) CSomeClass* SomePointer =
new CSomeClass;
Of course,
if you declare and initialize your pointer you need to
find a place to delete it.
In an extension
.DLL, you will find a function called DllMain(
). This function gets called when the client program
attaches your .DLL and
again when it detaches. So here's one possible way to
handle your pointers in an extension
.DLL:
#include "SomeClass.h"
_declspec(dllexport) CSomeClass* SomePointer = new CSomeClass;
DllMain(HINSTANCE hInstance, DWORD dwReason, LPVOID lpReserved)
{
if (dwReason == DLL_PROCESS_ATTACH)
{
}
else if (dwReason == DLL_PROCESS_DETACH)
{
delete SomePointer;
}
}
A regular
.DLL looks more like an ordinary MFC
executable. It has an object derived from CWinApp to
handle opening and closing your .DLL.
You can use the class wizard to add an InitInstance( )
function and an ExitInstance( ) function.
int CMyDllApp::ExitInstance()
{
delete SomePointer;
return CWinApp::ExitInstance();
}
Using
the .DLL in a client application
A .DLL can't
run on its own. It requires a client application to load
it and use its interface. Making a client application that
can do so is not difficult.
When you
compile your .DLL, the compiler creates two important
files: the .DLL file and the .lib file. Your client
application needs both of these. You must copy them into
the project folder of your client application. Note that
the .DLL and .lib files that are created when you build in
Debug are different that those built when you build in
Release. When you are building your client application in
Debug, you need the Debug versions of the .DLL and .lib
files, and when you are building in Release you need the
Release .DLL and .lib. The easiest way to handle this is
to put the Debug .DLL and .lib files in your client
application's Debug folder and the Release .DLL and .lib
in the Release folder.
The next
step is to go into your client project settings and tell
the linker to look for your .lib file. You must tell the
linker the name of your .lib file and where it can be
found. To do this, open the project settings, go to the
"Link" tab and enter your file name and path in
the "Object/library modules" box.
In addition
to the .DLL and .lib files, your client application needs
a header file for the imported classes, functions, objects
and variables. When we were exporting, we added "__declspec(dllexport)"
to our declarations. Now when we are importing, we will
add "__declspec(dllimport)."
So if we wanted to import the variable, object and
function used in our previous examples, our header file
would contain the following:
__declspec(dllimport) int SomeFunction(int);
__declspec(dllexport) CSomeClass SomeObject;
__declspec(dllexport) int SomeInt;
Remember, if
you used the extern "C" specifier in the
.DLL, you must also use it in the client application:
extern "C" __declspec(dllimport) int SomeFunction(int);
To make
things more readable, we might write it like this instead:
#define DLLIMPORT __declspec(dllimport)
DLLIMPORT int SomeFunction(int);
DLLIMPORT CSomeClass SomeObject;
DLLIMPORT int SomeInt;
Now that you
have declared your object, variable and function in a
header file inside your client application, they are
available for use.
To import an
entire class, you must copy the entire .h header file into
the client application. The .DLL and the client
application will thus have identical header files for the
exported class, except one will say "class __declspec(dllexport)
CMyClass" and one will say "class __declspec(dllimport)
CMyClass". If you are making an MFC
extension .DLL, you could instead say
"class AFX_EXT_CLASS
CMyClass" in both places.
Once you are
done building your client application and you're ready to
turn it over to the actual users, you should give them
your Release executable and the Release .DLL. You do not
need to give the users the .lib file. The .DLL can go in
the same directory as the executable, or it can go in the
Windows System directory. As discussed above, you may also
have to provide your users with the correct MFC
code library .DLL.
This .DLL was loaded onto your computer when you installed
Visual C++. Your
users, however, may not have it. It does not come standard
with Windows.
A
Word of Caution
This article
should provide you with enough information to start building
your own .DLLs. A word of caution is needed,
however. As mentioned at the beginning of this article,
there are several serious shortcomings to .DLLs.
These shortcomings are the reason that we now have COM
and ATL. There are two
main problems. First, a .DLL built with one brand of
compiler may not be compatible with a client application
built with a different compiler. Second, when you modify
the .DLL, you may have to recompile the client
application, even though you aren't changing any code in
the client application. You may still have to copy in a
new .DLL and .lib
file and recompile.
There are
ways to avoid this problem under some circumstances. I'll
discuss the problem in more detail in the next article.