Download demo project - 59 Kb
Introduction
How can we create a program that will be
able to work with objects that do not even exist at the time when our
executable is conceived? Would it not be nice if we could design a program,
which functionality we would be able to extend without altering the original
executable’s source (and even without recompiling): an application where we can
plug-in additional functionality on the fly.
This is possible if we use the proposed
development scheme of working with plug-in objects. What we need is a framework
that works for predefined base objects, but that is able to load DLLs
containing (new) child objects that extend, alter or specialize the behaviour.
It is even possible to manipulate different generations of these objects: They
co-exist peacefully together.
One of the difficulties of working with
DLLs and allocating and de-allocating (and thus exchanging) memory is the way
the heap manager works. Unless one uses GlobalAlloc, it is imperative that all
the memory allocated in a particular DLL, is also deleted or de-allocated by
the same DLL (read 'the same heap manager' instead of 'the same DLL'). If one
does not follow this rule, there is the distinct possibility that one will try
to delete memory that is unknown by the heap manager of the ‘called’ DLL. These
bugs are fatal and very difficult to find!
Let say you created an object 'A' inside
the DLL called 'A-DLL'. You pass this object to the executable ('Bogus.exe') or
another DLL called 'B-DLL' and you try to de-allocate the memory associated
with Object 'A'. The heap manager of 'Bogus.exe' or 'B-DLL' does not know about
the existence of the memory associated with object 'A' and could thus delete
invalid memory. If you would use the GlobalAlloc function and sorts, you do not
use the heap managers, but instead memory is directly obtained from the
operating system. This approach certainly works, but IMHO is very ugly and
hardly C++.
What is proposed here is a scheme for C++ Objects.
It is better to enforce the rule
"de-allocate where you allocated it". That's where the DLLProxy Class
comes into the picture. Only one instance of this DLLProxy exists per DLL. And
this instance is responsible for allocation and de-allocation of our (plug-in)
objects in the DLLs. In fact, these objects, handle the memory management for
objects created inside a DLL.
When the application is designed, one must
of course know what kind of functionality is going to be extended in the
future: communication protocols, MFC views or documents, windows, different
languages for code generation, support future hardware equipment or new
algorithms… Almost everything is possible if the function class interface is
properly defined.
The behaviour will be altered by the
virtual function mechanism later on. In the example, you can see that the
function DoSomething() is called, but what actually happens inside this
function is determined by the implementation in the Plug_Object_Child class.
The Plug_Object_Child class is defined inside the Plug-in DLL and was allocated
by a particular DLLProxyChild Object. This DLLProxyChild Object is a
specialization of the DLLProxy class because it has to know which
Plug_Object(_Child) objects to allocate and de-allocate!
Explicit linking is required, because the
plug-in enabled executable is not even aware of the existence of the particular
plug-in DLLs at compile and link time.
What kind of objects?
When we design our application, we must of course know what kind of functionality we are going to extend in the future. Do we want to extend communication protocols, MFC views or documents, windows, different languages for code generation, support future hardware equipment or new algorithms… you name it. Almost everything is possible.
We then create a virtual base class for our to-be-extended functionality. Our application will always work with this class definition. The behavior will be altered by the virtual function mechanism later on.
In our example, we will call DoSomething(), but what actually happens inside this function is determined by the implementation in the Plug_Object_Child class. The Plug_Object_Child class is defined inside a DLL and was allocated by a particular DLLProxyChild Object. This DLLProxyChild Object is a specialization of the DLLProxy class because it has to know which Plug_Object(_Child) objects to allocate and deallocate!
This is a simple explanation of how it works. Sounds simple for anyone familiar with DLLs and the virtual function mechanism, does it not?
Of course we need EXPLICIT linking, because we do not know whether the DLLs with extended functionality exist (or how many there exist in the future) when building our plug-in enabled application.
The Procedure
What we do is quite simple.
- First, one searches a particular directory
for the existence of DLLs, then their usability is determined: the DLLs must
contain the necessary exported functions to obtain a DLLProxy Object. (In other
words, it is a check whether the DLLs are really plug-in DLLs)
- Next, the DLLs are mapped in the
executable’s address space (Load the library) and
- the function pointers to the required
functions are stored. (Obtained by calling GetProcAddress)
- These FARPROC function pointers can be
caste to the correct function prototype, so that these functions can be called
to obtain the DLLProxy object.
- This DLLProxy Object is used to create and
delete Plug_Object instances.
- In the end, when it is certain that no more
DLLProxy objects exist, the DLLs can be mapped out (FreeLibrary).
- The DLLRTLoader (DLL Run-time Loader) class
automated this functionality.
- However, do not forget to define GetDLLProxy
function in every Plug-in DLL. It is a function with the following prototype:
typedef DLLProxy* (WINAPI *GETDLLPROXY)(void);
It is exported through the DLLs .def file when building the DLL project.
The Classes and their Interfaces
class DLLRTLoader
{
public:
DLLRTLoader();
virtual ~DLLRTLoader();
bool LookForDlls(const char* searchdirectory, bool recursive = true);
void FlushUnusedDLLs(void);
void AddFuncName(const char* theFN);
FileFinder* GetFileFinder(void);
DLLFileEntry* SearchDLLFileEntry(const char* completepath);
DynamicSortedArray<DLLFileEntry*>* GetMappedDLLList(void);
DynamicSortedArray<DLLFileEntry*>* GetDLLList(void);
unsigned GetNumberOfUsableDlls(void);
unsigned GetNumberOfMappedDLLs(void);
void Debug(ostream& theStream);
protected:
virtual void CheckForDLLFuncs(void);
FileFinder myFileFinder;
DynamicSortedArray<DLLFileEntry*> myDLLs;
DynamicArray<char*> myFuncNames;
};
class DLLFileEntry : public FileEntry
{
public:
DLLFileEntry(const char* path);
DLLFileEntry(const FileEntry&);
virtual ~DLLFileEntry();
inline void IncDllUsage(void)
{
_ASSERT(myMapped_In == true);
myRefCount++;
};
inline void DecDllUsage(void)
{
_ASSERT(myMapped_In == true);
_ASSERT(myRefCount > 0);
myRefCount--;
};
inline HINSTANCE GetModuleHandle(void)
{
return myLibHandle;
}
unsigned GetRefCount(void);
void SetRefCount(unsigned ref);
void SetProxy(DLLProxy* theProxy);
DLLProxy* GetProxy(void);
bool GetDllOK(void);
bool IsMappedIn(void);
virtual MapIn(void);
virtual bool CheckAndStoreFuncs(void);
virtual MapOut(void);
virtual FARPROC GetFuncAddress(const char* funcname);
void SetFuncNameList(DynamicArray<char*>* theFuncNames);
void Debug(ostream& theStream);
protected:
DLLFileEntry(const DLLFileEntry&);
DLLFileEntry& operator=(const DLLFileEntry&);
void SetDllOK(bool value);
unsigned myRefCount;
bool myMapped_In;
HINSTANCE myLibHandle;
Tree<FARPROC> myFuncAddresses;
DynamicArray<char*>* myFuncNames;
private:
bool myDLLisOK;
DLLProxy* myProxy;
};
class CLASS_DECL_DLL DLLProxy
{
public:
DLLProxy();
virtual ~DLLProxy();
virtual Plug_Object* CreateObject(void);
virtual void DeleteObject(ProxyInterface* theObject);
void SetDLLFE(DLLFileEntry* theDLLFE);
void DecDllUsage(void);
void IncDllUsage(void);
char* GetDLLRelativePath(void);
HMODULE GetSafeModuleHandle(void);
static char* GetRootModulePath(void);
static void SetRootModulePath(const char* modulerootpath);
protected:
DLLProxy(const DLLProxy&);
DLLProxy& operator=(const DLLProxy&);
DLLFileEntry* myDLLFE;
static char* myModuleRootPath;
};
class CLASS_DECL_DLL ProxyInterface
{
public:
ProxyInterface();
virtual ~ProxyInterface();
DLLProxy* GetProxy(void);
void SetProxy(DLLProxy* theProxy);
protected:
DLLProxy* myProxy;
};
class CLASS_DECL_DLL Plug_Object : public ProxyInterface
{
public:
virtual void DoSomething(void) = 0;
protected:
Plug_Object(DLLProxy* theProxy)
{
myProxy = theProxy;
}
virtual ~Plug_Object();
};
Plug_Object* DLLProxy::CreateObject(void)
{
_ASSERT(myDLLFE != NULL);
Plug_Object* toReturn = NULL;
if (toReturn != NULL)
{
myDLLFE->IncDllUsage();
toReturn->SetProxy(this);
}
else
{
_ASSERT(NULL);
}
return toReturn;
}
void DLLProxy::DeleteObject(ProxyInterface* theObject)
{
if (theObject != NULL && theObject->GetProxy() == this)
{
delete theObject;
theObject = NULL;
if (myDLLFE != NULL)
{
myDLLFE->DecDllUsage();
}
}
else
{
_ASSERT(NULL);
}
}
Create a new DLL Project or in other words what does a Plug-in DLL contain?
One Global pointer to a DLLProxy object
DLLProxyChild1 theProxy;
DLLProxy* theProxy = &theProxy;
1 exported function (put the name in the .DEF file) =
extern "C" DLLProxy* GetDLLProxy(void)
{
return theProxy;
}
Definitions of the specific specialization Plug_Object class and the specific specialization DLLProxy class:
class DLLProxyChild1 : public DLLProxy
{
public:
DLLProxyChild1 ();
virtual ~ DLLProxyChild1 ();
Plug_Object* CreateObject(void);
void DeleteObject(ProxyInterface* theObject);
};
class CLASS_DECL_DLL Plug_Object_Child1 : public Plug_Object
{
public:
Plug_Object_Child1(DLLProxy* theProxy);
virtual ~Plug_Object_Child1();
void DoSomething(void);
protected:
};
Plug_Object* DLLProxyChild1::CreateObject(void)
{
_ASSERT(myDLLFE != NULL);
Plug_Object_Child1* toReturn = new Plug_Object_Child1;
if (toReturn != NULL)
{
myDLLFE->IncDllUsage();
toReturn->SetProxy(this);
}
else
{
_ASSERT(NULL);
}
return toReturn;
}
void DLLProxyChild1::DeleteObject(ProxyInterface* theObject)
{
if (theObject != NULL && theObject->GetProxy() == this)
{
delete theObject;
theObject = NULL;
if (myDLLFE != NULL)
{
myDLLFE->DecDllUsage();
}
}
else
{
_ASSERT(NULL);
}
}
#pragma data_seg( ".GLOBALS")
int nProcessCount = 0;
int nThreadCount = 0;
#pragma data_seg()
DLLProxyChild1 theProxy;
DLLProxy* theProxy = &theProxy;
extern "C" BOOL APIENTRY
DllMain ( HANDLE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
UNREFERENCED_PARAMETER(lpReserved);
switch( ul_reason_for_call )
{
case DLL_PROCESS_ATTACH:
{
nProcessCount++;
break;
}
case DLL_THREAD_ATTACH:
{
nThreadCount++;
break;
}
case DLL_THREAD_DETACH:
{
nThreadCount--;
break;
}
case DLL_PROCESS_DETACH:
{
nProcessCount--;
break;
}
default:
{
break;
}
}
return TRUE;
}
Serialization of Plug-in Objects
Concerning serialization of Plug-in Objects, we can distinguish between projects or programs using MFC and projects without MFC.
A. With MFC
To Save
- Define a Serialize(CArchive& theArchive) function for every Plug_Object.
- As usual, call the Serialize function of the CDocument.
- The Plug_Objects store themselves to file through their Serialize function
To Load
- Before the Serialize function of the CDocument is performed, all the necessary run-able code must be present in memory. All the DLLs, present in the DLL root path are recursively mapped in.
- Load the file
- Flush the unused DLLs.
B. Without MFC
Remember we have the DLL root path and we also have the relative path of the DLLs to the DLL root path.
So write a function in every Plug_Object that does this:
BYTE* DeSerialize(const BYTE* pFileData);
BYTE* Serialize( unsigned int& serialization_byte_size );
The serialisation scheme then looks like this
To save:
While (still_objects_to_save)
{
Save relative plug-in DLL path size in 4 bytes (so you know which plug-in code to load when loading the file)
Save the relative plug-in DLL path itself (to identify the correct object)
Serialize() the object and save it
}
To load:
While (still_data_to_read)
{
Read byte stream
4 bytes (for the size) -> relative path string
Load Plug-in DLL if not already loaded (with relative path)
Construct object from Plug-in DLL
Call DeSerialize() on Plug-in object (in fact this is a 2 phase construction!)
}
Some more info...
More information about the difference between a similar scheme, which I discovered some time ago in MSDN, and COM can be found in the MSDN Article "From CPP to COM" by Markus Horstmann, where COM is presented as a superior (?) solution.
It has been pointed out to me that there is a more general solution to be found on Dynamic C++ Classes as "a lightweight mechanism to update code in a running program" at http://actcomm.dartmouth.edu/dynamic/
See the sample project for a demonstration of its usage.
I hope all things all clear.
If they are not: try stepping through the debugger, that sometimes helps.
If something is not clear in the above explanation, let me know and I will try to clarify things!
Updates
Now works with VC++ 6
Gert Boddaert is an experienced embedded software architect and driver developer who worked for companies such as Agfa Gevaert, KBC, Xircom, Intel, Niko, (Thomson) Technicolor, Punch Powertrain, Fifthplay, Cisco and Barco. For more obscure details, please take a look at his LinkedIn profile. Anyway, he started out as a Commercial Engineer – option “Management Informatics”, but was converted to the code-for-food religion by sheer luck. After writing higher level software for a few years, he descended to the lower levels of software, and eventually landed on the bottom of embedded hell… and apparently still likes it down there.
His favourite motto: “Think hard, experiment and prototype, think again, write (easy and maintainable) code”,
favourite quote: “If you think it’s expensive to hire a professional to do the job, wait until you hire an amateur.” – by Red Adair,
I can be contacted for real-time embedded software development projects via http://www.rtos.be and http://www.rtos.eu