Professional Documents
Culture Documents
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.
(continued)
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)
as i discussed in the last article, .dlls are a useful tool for any mfc programmer. they are
subject to a number of important limitations, however, and anyone who is making .dlls
should be aware of these.
(continued)
mfc issues
this was discussed in the last article, but is worth mentioning again briefly. an mfc
extension .dll can only be used if the client application dynamically links to the same
version of mfc, and the correct mfc code library .dll is available on the client computer. a
regular .dll which dynamically links to mfc will only work if the correct mfc code library
.dll is available.
compiler incompatibility issues
one of the biggest problems with c++ based .dlls arises when a .dll is built on one brand
of compiler and called by an application built on another brand of compiler. often it won't
work without a great deal of effort.
ansi sets the standards for the c and c++ languages. that is, it specifies the c and c++
functions and data types which should be supported by a compiler. it does not, however,
provide a complete standard as to how these functions and data types should be
implemented on a binary level. as a result, compiler vendors are free to implement
language features in their own proprietary ways.
most c / c++ programmers know that different compilers handle data types differently.
one compiler will allocate 2 bytes for an int and another will allocate 4 bytes. one will use
4 byte doubles and another will use 8 bytes. an even bigger difference with c++ compilers
arises from their implementation of function and operator overloading. the differences
between compilers go far beyond this, however. the same c or c++ code may be compiled
very differently by different compilers. these differences may keep your .dll from running
with someone else's application.
of course, if you are building an mfc extension .dll, this is not an issue for you. mfc
extension .dlls are made with a microsoft compiler. as discussed in the previous article,
they can only be used by applications that are dynamically linked to mfc. these
applications are also made with a microsoft compiler.
compiler incompatibility problems can be fixed by inserting pragmas and other
precompile instructions into your code, but this is hard to do and unreliable. there will
always be the chance that someone else is using a compiler that's still incompatible.
recompiling
let's say you built a .dll that exports a class called cmyclass. you provide a copy of the
header file for cmyclass to be used by the client application. suppose that a cmyclass
object is 30 bytes in size.
now let's suppose you modify the .dll to change cmyclass. it still has the same public
functions and member variables, but now cmyclass has an additional private member
variable, an int. so now, when you create an object of type cmyclass, it's 34 bytes in size.
you send this new .dll to your users and tell them to replace the old .dll. now you have a
problem. the client application is expecting a 30 byte object, but your new .dll is creating
a 34 byte object. the client application is going to get an error.
here's a similar problem. suppose that instead of exporting cmyclass your .dll exports
several functions which use cmyclass references or pointers in their parameters or return
values. you have provided a cmyclass header file which now resides in the client
application. again, if you change the size of the cmyclass object without rebuilding the
client application, you will have problems.
at this point, the only way to fix the problem is to replace the cmyclass header file in the
client application and recompile it. once recompiled, the client application will start
looking for a 34 byte object.
this is a serious problem. one of the goals of having a .dll is to be able to modify and
replace the .dll without modifying the client application. however, if your .dll is exporting
classes or class objects, this may not be possible. you may have to recompile the client
application. if you don't have the source code for the client application, you simply can't
use the new .dll.
solutions
if there were a perfect fix for these problems, we might not have com. here are a few
suggestions:
mfc extension .dlls don't have compiler incompatibility problems. they simply can't be
used by applications built on non-microsoft compilers. as for regular .dlls, you can avoid
many compiler problems by only exporting c-style functions and using the extern "c"
specifier. exporting c++ classes and overloaded c++ functions leaves you much more
vulnerable to compiler incompatibility.
as for having to recompile your client application when you modify the .dll, there are
relatively easy ways to avoid the problem. i will describe two of them: (1) an interface
class and (2) static functions used to create and destroy your exported class.
using an interface class
the goal of an interface class is to separate the class you want to export and the interface
to that class. the way to do this is to create a second class which will serve as the interface
for the class you want to export. then, even when the export class changes, you will not
need to recompile the client application, because the interface class remains the same.
here's an example of how that would work. suppose you want to export cmyclass.
cmyclass has two public functions, int functiona(int) and int functionb(int). if i simply
export cmyclass, i'll have to recompile the client application every time i add a new
variable. instead, i'll create and export an interface class, cmyinterface. cmyinterface will
have a pointer to a cmyclass object. here's the header file for cmyinterface as it looks
inside the .dll:
#include "myclass.h"
cmyinterface( );
~cmyinterface( );
public:
int functiona(int);
int functionb(int);
};
inside the client application, the header file will look slightly different. the #include will
be gone. after all, you can't include myclass.h, because the client application doesn't have
a copy of it. instead, you will use a forward declaration of cmyclass. this will allow you
to compile even without the cmyclass header file:
class __declspec(dllimport) cmyinterface
{
//forward declaration of cmyclass
class cmyclass;
cmyclass *m_pmyclass;
cmyinterface( );
~cmyinterface( );
public:
int functiona(int);
int functionb(int);
};
inside the .dll, you implement cmyinterface as follows:
cmyinterface::cmyinterface( )
{
m_pmyclass = new cmyclass;
}
cmyinterface::~cmyinterface( )
{
delete m_pmyclass;
}
int cmyinterface::functiona( )
{
return m_pmyclass->functiona( );
}
int cmyinterface::functionb( )
{
return m_pmyclass->functionb( );
}
thus, for every public function in cmyclass, cmyinterface will provide its own
corresponding function. the client application has no contact with cmyclass. if it wants to
call cmyclass::functiona, it instead calls cmyinterface::functiona. the interface class then
uses its pointer to call cmyclass. with this arrangement, you're free to modify with
cmyclass. it doesn't matter if the size of the cmyclass object changes. the size of the
cmyinterface object will remain the same. if you add a private member variable to
cmyclass, the size of cmyinterface will remain unchanged. if you add a public member
variable to cmyclass, you can add "getter" and "setter" functions for the new variable in
cmyinterface without fear. adding new functions to cmyinterface will not cause recompile
problems.
creating a separate interface class avoids some compiler incompatibility problems and
most recompile problems. as long as the interface class doesn't change, there should be
no need to recompile. there are still two relatively minor problems with this solution.
first, for every public function and member variable in cmyclass, you must create a
corresponding function or variable in cmyinterface. in the example there are only two
functions, so that's easy. if cmyclass had hundreds of functions and variables, this would
be a more tedious, error-prone process. second, you are increasing the amount of
processing that must be done. the client application no longer calls cmyclass directly.
instead, it calls a cmyinterface function which calls cmyclass. if this is a function that will
be called thousands of times by the client application, the extra processing time may
begin to add up.
static functions
a different way to avoid having to recompile uses static functions to create and destroy
the exported class. this solution was sent to me by ran wainstain in a comment to my
previous article.
when you are creating a class which you intend to export, you add two public, static
functions, createme( ) and destroyme( ):
class __declspec(dllexport) cmyclass
{
cmyclass( );
~cmyclass( );
public:
static cmyclass* createme( );
static void destroyme(cmyclass *ptr);
}
createme( ) and destroyme( ) are implemented as follows:
cmyclass* cmyclass::createme( )
{
return new cmyclass;
}
the code of the ontimer function is rather straightforward. every time a timer is called in
this class, it will run through this function. therefore, the value passed through the
function automatically houses an unsigned integer corresponding to the timer you create
manually. using a select statement, i created the case for the timer you will render with.
the basic "draw then swap buffers" routine is now placed inside this timer. in the first
steps of this tutorial, though, you won't actually draw images to the screen, but only
correctly swap the buffers to the clear color.
openglcontrol.cpp:
void copenglcontrol::ontimer(uint nidevent)
{
switch (nidevent)
{
case 1:
{
// clear color and depth buffer bits
glclear(gl_color_buffer_bit | gl_depth_buffer_bit);
// swap buffers
swapbuffers(hdc);
break;
}
default:
break;
}
cwnd::ontimer(nidevent);
}
step 12: adding the onsize function
the last message function you need to add will be the onsize function. like you've done
before, select the wm_size message and select <add> onsize from the dropdown to
create the function.
the onsize function is called when the window is resized. to prevent the opengl rendering
from getting all wacky from resizing the window, a few things such as the perspective
and viewport need to be tweaked. in the tutorial, you won't actually resize the control
according to the window, but that could be achieved in this function if the need arises.
openglcontrol.cpp:
void copenglcontrol::onsize(uint ntype, int cx, int cy)
{
cwnd::onsize(ntype, cx, cy);
// projection view
glmatrixmode(gl_projection);
glloadidentity();
// model view
glmatrixmode(gl_modelview);
}
you are now done with the copenglcontrol class (the initial setup at least). so, the next
step is to integrate the class into your main mfc window, the coglmfcdialogdlg class.
step 13: customizing the main mfc dialog class
your last step is to integrate your opengl class into the main mfc dialog. this is done rather
painlessly. first, inside the oglmfcdialogdlg.h file you will need to #include the created
opengl class header file and also create an instance of the copenglcontrol class as a local
variable to this class.
oglmfcdialogdlg.h:
#include "openglcontrol.h"
glbegin(gl_quads);
// top side
glvertex3f( 1.0f, 1.0f, 1.0f);
glvertex3f( 1.0f, 1.0f, -1.0f);
glvertex3f(-1.0f, 1.0f, -1.0f);
glvertex3f(-1.0f, 1.0f, 1.0f);
// bottom side
glvertex3f(-1.0f, -1.0f, -1.0f);
glvertex3f( 1.0f, -1.0f, -1.0f);
glvertex3f( 1.0f, -1.0f, 1.0f);
glvertex3f(-1.0f, -1.0f, 1.0f);
// front side
glvertex3f( 1.0f, 1.0f, 1.0f);
glvertex3f(-1.0f, 1.0f, 1.0f);
glvertex3f(-1.0f, -1.0f, 1.0f);
glvertex3f( 1.0f, -1.0f, 1.0f);
// back side
glvertex3f(-1.0f, -1.0f, -1.0f);
glvertex3f(-1.0f, 1.0f, -1.0f);
glvertex3f( 1.0f, 1.0f, -1.0f);
glvertex3f( 1.0f, -1.0f, -1.0f);
// left side
glvertex3f(-1.0f, -1.0f, -1.0f);
glvertex3f(-1.0f, -1.0f, 1.0f);
glvertex3f(-1.0f, 1.0f, 1.0f);
glvertex3f(-1.0f, 1.0f, -1.0f);
// right side
glvertex3f( 1.0f, 1.0f, 1.0f);
glvertex3f( 1.0f, -1.0f, 1.0f);
glvertex3f( 1.0f, -1.0f, -1.0f);
glvertex3f( 1.0f, 1.0f, -1.0f);
glend();
}
now, one last thing you'll need to do is uncomment the commented-out call to the
ogldrawscene function in the ontimer function.
openglcontrol.cpp::ontimer:
.
.
.
// clear color and depth buffer bits
glclear(gl_color_buffer_bit | gl_depth_buffer_bit);
// swap buffers
swapbuffers(hdc);
.
.
.
step 16: setting up a maya-style camera
the maya-style camera refers to the camera system used in alias|wavefront maya in
which the mouse button->drag performs a certain operation: left: rotate, right: zoom, and
middle: translate. to do this, you'll need to make an onmousemove function call:
select the wm_mousemove message and select from the dropdown, <add>
onmousemove to create the function. inside this newly created function, you'll need to
add the code to perform each of the buttons' given operations. if you recall from early on
in the tutorial, you created some member variables for rotate, zoom, and position. you'll
use these variables and set their new values according to the position variables passed
into the function by windows.
openglcontrol.h:
afx_msg void onmousemove(uint nflags, cpoint point);
openglcontrol.cpp:
void copenglcontrol::onmousemove(uint nflags, cpoint point)
{
int diffx = (int)(point.x - m_flastx);
int diffy = (int)(point.y - m_flasty);
m_flastx = (float)point.x;
m_flasty = (float)point.y;
cwnd::onmousemove(nflags, point);
}
furthermore, i have put a few initial variable settings into the constructor of the
copenglcontrol class, which help the camera work properly.
openglcontrol.cpp:
copenglcontrol::copenglcontrol(void)
{
m_fposx = 0.0f; // x position of model in camera view
m_fposy = 0.0f; // y position of model in camera view
m_fzoom = 10.0f; // zoom on model in camera view
m_frotx = 0.0f; // rotation on model in camera view
m_froty = 0.0f; // rotation on model in camera view
}
now, the last step to do is, if you recall from when you first created the ondraw function,
to change the commented todo of the camera controls to the following:
openglcontrol.cpp::ondraw:
.
.
.
glloadidentity();
gltranslatef(0.0f, 0.0f, -m_fzoom);
gltranslatef(m_fposx, m_fposy, 0.0f);
glrotatef(m_frotx, 1.0f, 0.0f, 0.0f);
glrotatef(m_froty, 0.0f, 1.0f, 0.0f);
.
.
.
(full size image)
and that's about it. quite simple, no? feel free to post any comments whatsoever on this
article. thanks for viewing; hope it helps!