DLL d'extensions : écriture d'un plug-in de classe
Date de publication : 26/04/2007 , Date de mise à jour : 26/04/2007
Par
Patrick OTTAVI MVP Visual C++ (Site) (Blog)
Les MFC permettent la génération de DLL d'extensions permettant l'export de classes complètes qui peuvent être instanciées et même dérivées.
Pour information c'est ce type de DLL que nous utilisons quand nos projets MFC sont liés avec l'option DLL MFC partagées.
Ce que je vous propose à travers cet article, c'est d'utiliser cette fonctionnalité pour réaliser un plug-in de classe, c'est-à-dire de charger dynamiquement une DLL donnant accès à une classe permettant un traitement spécifique.
I. Définition du problème
A. La classe de traitement
II. Génération du projet de tests
III. Définition de la classe abstraite de traitement
IV. Création de la DLL d'extensions
V. Définition de la classe plug-in
VI. Gestion du Plug-in
A. Fonctionnement de la classe CPlug
VII. Utilisation du Plug-in
VIII. Conclusion
I. Définition du problème
Je souhaite donner la possibilité à mon programme de charger une DLL avec sa classe de traitement à la demande.
Le but étant que mon programme ne soit pas tributaire de cette classe et qu'il ne soit pas alourdi par du code supplémentaire.
Mais pour clarifier tout ça prenons un exemple :
Imaginons que je travaille sur un programme avec un module d'intégration de données tarifaires de fournisseurs divers dans ma base de données articles.
Chaque fournisseur mettant à disposition un fichier texte avec ses propres particularités.
Pour traiter ces données dans mon programme je vais construire un modèle de traitement, idéalement une classe pour réaliser l'intégration.
Mais voila les fichiers à traiter sont très différents, des prétraitements sont à réaliser dessus, pour les remettre à ma norme d'intégration.
Dans un cadre classique j'écrirais une classe de base pour modéliser les traitements à réaliser sur ces fichiers, puis au fil des différentes intégrations je créerai une autre classe héritée de la première pour tenir compte des particularités du fichier à intégrer.
Au bout du compte mon traitement s'appuiera sur une classe de prétraitement de fichier qui sera dérivée autant de fois que je dispose de type de fichiers fournisseurs à traiter.
Tout cela est satisfaisant dans un contexte simple, mais qu'arrivera t-il si mon programme et son module d'intégration sont destinés à être utilisés pour plusieurs clients avec chacun leurs fournisseurs spécifiques ?
Mon code va enfler et se retrouver avec du code inutile d'un client à l'autre.
D'où l'idée : si je pouvais externaliser mon traitement dans une classe qui serait chargée dynamiquement, mon programme ne contiendrai que l'essentiel, libre à moi de distribuer mes DLL de prétraitements suivants mes clients.
Il me suffira ensuite de fournir une nouvelle DLL de traitement pour un nouveau fournisseur sans toucher à mon programme principal.
Facile ?
Pas tout à fait, le fait de définir l'utilisation de ma classe de traitement dans mon programme m'oblige à en définir le contenu et donc à lier mon programme à une DLL.
Sauf que je veux que mon programme puisse fonctionner sans DLL préalablement chargée, et surtout je veux continuer à travailler en C++ avec une classe.
Je précise ce dernier point, car rien de plus facile que de mettre un traitement dans une DLL et de le rendre accessible par une fonction « extern » en C dont on établira le lien dynamique avec l'api32 GetProcAddress.
A. La classe de traitement
Je vais commencer par définir ma classe de base de traitement sous forme d'une classe interface, en fait une classe abstraite qui ne pourra donc avoir d'instances directes.
Rappel : une classe est abstraite dès qu'elle possède une fonction virtuelle pure ou qu'elle hérite d'une fonction virtuelle pure sans la redéfinir.
Le fait que ma classe soit abstraite résout mon problème d'édition des liens.
Voyons maintenant comment procéder.
II. Génération du projet de tests
Pour les besoins de l'exemple je vais créer un projet MFC boîte de dialogue nommé ImportTarif .
Vous pouvez faire de même avec Visual Studio 2005 en suivant les étapes ci-dessous :
Cliquez ensuite sur le bouton « Terminer », notre projet de tests est généré.
III. Définition de la classe abstraite de traitement
Passons maintenant à la définition de notre classe abstraite :
Utilisez l'assistant pour créer une nouvelle classe :
Choisissez le modèle classe MFC, et cliquez sur le bouton ajoutez.
J'ai nommé ma classe de base CPlugImportTarif elle hérite de la classe de base MFC CObject.
Dans le cas présent l'assistant me génère plus d'éléments que j'en ai besoin, seul le .h m'intéresse, on supprimera du projet le fichier source généré.
Le .h généré donne ceci :
#pragma once
class CPlugImportTarif : public CObject
{
public:
CPlugImportTarif();
virtual ~CPlugImportTarif();
};
|
Je vais modifier la classe pour qu'elle soit exportable dans
une DLL d'extensions et surtout définir mon interface de traitement.
#pragma once
class AFX_EXT_CLASS CPlugImportTarif : public CObject
{
public:
virtual bool PreFiltre()=0;
virtual void SetFile(LPCTSTR szFileName=_T(""))=0;
public:
CString m_strFile;
};
|
Ma classe est très simple volontairement pour illustrer la méthode.
A ce stade la compilation et l'édition des liens ne causent pas de probléme.
Je peux aussi déclarer sans probléme un pointeur sur ma classe abstraite dans classe boite de dialogue sans plus de probléme.
|
#pragma once
#include "plugimportTarif.h"
class CImportTarifDlg : public CDialog
{
public:
CImportTarifDlg(CWnd* pParent = NULL);
enum { IDD = IDD_IMPORTTARIF_DIALOG };
protected:
virtual void DoDataExchange(CDataExchange* pDX);
protected:
HICON m_hIcon;
CPlugImportTarif *m_pImportTarif;
virtual BOOL OnInitDialog();
afx_msg void OnSysCommand(UINT nID, LPARAM lParam);
afx_msg void OnPaint();
afx_msg HCURSOR OnQueryDragIcon();
DECLARE_MESSAGE_MAP()
};
|
Passons maintenant à la réalisation de la DLL d'extensions.
IV. Création de la DLL d'extensions
A partir de l'explorateur de solution au sommet du projet faites clic droit : ajouter un nouveau projet :
Je crée mon premier plugin pour le fournisseur imaginaire Dupont :
Dans les paramètres du projet je prends bien garde de sélectionner le modèle DLL d'extensions comme ci-dessous :
Je vais maintenant définir ma classe de traitement qui hérite bien sûr de ma classe abstraite :
V. Définition de la classe plug-in
|
#pragma once
#include "../ImportTarif/plugimportTarif.h"
class CPlugImpDupont :public CPlugImportTarif
{
DECLARE_SERIAL(CPlugImpDupont)
public:
CPlugImpDupont(LPCTSTR szFileName=_T(""))
{
SetFile(szFileName);
}
virtual void SetFile(LPCTSTR szFileName=_T(""))
{
CString str;
if(szFileName!=_T(""))
{
str=_T("Dupont:Affection du nom:");
str+=szFileName;
AfxMessageBox(str);
}
m_strFile=szFileName;
}
virtual bool PreFiltre()
{
AfxMessageBox(_T("\nDupont:Traitement de Filtrage"));
return true;
}
public:
};
IMPLEMENT_SERIAL(CPlugImpDupont,CObject,0)
|
Ma classe de traitement se nomme CPlugImpDupont, elle hérite bien de ma classe base abstraite CPlugImportTarif définie dans mon projet principal.
Maintenant que mes classes sont définies, la question que vous devez certainement vous posez est : comment faire pour créer une instance de cette classe et surtout l'affecter au pointeur sur ma classe abstraite dans mon projet de tests?
En fait c'est la que réside toute l'astuce du procédé, je vais utiliser la capacité des MFC à créer une instance à partir d'une signature de classe.
Cette propriété est assumée par la présence des macros DECLARE_SERIAL et IMPLEMENT_SERIAL dans ma classe.
Enfin je vais rajouter dans le source principal de ma DLL d'extension la fonction PlugInit définie ci-dessous :
#include "stdafx.h"
#include <afxdllx.h>
#include "plugimpDupont.h"
#ifdef _MANAGED
#endif
#ifdef _DEBUG
#define new DEBUG_NEW
#endif
static AFX_EXTENSION_MODULE PlugImpTarDupontDLL = { NULL, NULL };
#ifdef _MANAGED
#pragma managed(push, off)
#endif
extern "C" int APIENTRY
DllMain(HINSTANCE hInstance, DWORD dwReason, LPVOID lpReserved)
{
UNREFERENCED_PARAMETER(lpReserved);
if (dwReason == DLL_PROCESS_ATTACH)
{
TRACE0("Initialisation de PlugImpTarDupont.DLL !\n");
if (!AfxInitExtensionModule(PlugImpTarDupontDLL, hInstance))
return 0;
new CDynLinkLibrary(PlugImpTarDupontDLL);
}
else if (dwReason == DLL_PROCESS_DETACH)
{
TRACE0("Arrêt de PlugImpTarDupont.DLL !\n");
AfxTermExtensionModule(PlugImpTarDupontDLL);
}
return 1;
}
extern "C" AFX_EXT_API void PlugInit(CRuntimeClass** pClass)
{
*pClass = RUNTIME_CLASS(CPlugImpDupont);
}
#ifdef _MANAGED
#pragma managed(pop)
#endif
|
C'est une simple fonction C ,qui me renverra la signature de ma classe de traitement.
Ceci fait, voyons maintenant comment utiliser notre plugin .
VI. Gestion du Plug-in
Pour cela je vais écrire une petite classe template de gestion de plugin, la classe CPlug qui prend comme paramètre la classe de base abstraite de votre plugin.
#pragma once
#include <stdexcept>
#include <string>
#include "plugimportTarif.h"
template <class Plug>
class CPlug
{
private:
CPlug(const CPlug &rPlug);
void operator=(const CPlug&);
public:
CPlug():m_pPlug(NULL),m_hDLL(NULL){}
CPlug(LPCTSTR szFileName):m_pPlug(NULL),m_hDLL(NULL),m_bLoad(false)
{
if(!LoadPlugin(szFileName))
throw std::invalid_argument("Nom de DLl Incorrect");
}
~CPlug()
{
FreePlugin();
}
void FreePlugin()
{
if(m_bLoad)
{
delete m_pPlug;
FreeLibrary(m_hDLL);
TRACE(_T("\nLibération ancien plug-in"));
m_hDLL=NULL;
m_pPlug=NULL;
}
m_bLoad=false;
}
bool LoadPlugin(LPCTSTR szFileName)
{
FreePlugin();
m_hDLL = LoadLibrary(szFileName);
if(m_hDLL<0x20)
{
TRACE(_T("\nDLL %s non trouvée ou Erreur consultez GetLastError() !!!"),szFileName);
return false;
}
TRACE(_T("\nPlug-in :%s chargé"),szFileName);
m_bLoad=true;
return true;
}
Plug *GetPlugIn()
{
if(m_pPlug) return m_pPlug;
if(!m_bLoad) return NULL;
typedef UINT ( * LPDLLFUNC)(CRuntimeClass**);
LPDLLFUNC lpfnDllFunc = NULL;
lpfnDllFunc = (LPDLLFUNC)::GetProcAddress(m_hDLL,"PlugInit");
if (!lpfnDllFunc)
{
TRACE(_T("\nPas de Fonction PlugInit dans la DLL!!"));
FreePlugin();
return NULL;
}
CRuntimeClass* pNewClass;
lpfnDllFunc(&pNewClass);
ASSERT(pNewClass);
m_pPlug= static_cast<Plug *>(pNewClass->CreateObject());
return m_pPlug;
}
protected:
Plug *m_pPlug;
HINSTANCE m_hDLL;
bool m_bLoad;
};
class CImportTarifDlg : public CDialog
{
public:
CImportTarifDlg(CWnd* pParent = NULL);
enum { IDD = IDD_IMPORTTARIF_DIALOG };
protected:
virtual void DoDataExchange(CDataExchange* pDX);
protected:
HICON m_hIcon;
CPlug<CPlugImportTarif> m_Plug;
virtual BOOL OnInitDialog();
afx_msg void OnSysCommand(UINT nID, LPARAM lParam);
afx_msg void OnPaint();
afx_msg HCURSOR OnQueryDragIcon();
DECLARE_MESSAGE_MAP()
public:
afx_msg void OnBnClickedButtonplug();
CString m_strPath;
afx_msg void OnBnClickedButtontest();
};
|
A. Fonctionnement de la classe CPlug
La DLL de plug-in est lue avec l'api32 LoadLibrary.
Pour retourner une instance de notre classe plug-in on associe un pointeur de fonction à notre fonction d'initialisation PlugInit qui sera présente dans chaque plugin.
On appelle PlugInit pour récupérer la signature de la classe à instancier.
Enfin on créé l'instance grâce à la méthode CreateObject() de la classe CRuntimeClass.
Il ne reste plus qu'a utiliser notre objet.
VII. Utilisation du Plug-in
Dans ma classe CDialog de traitement j'ai rajouté deux fonctions :
Une pour permettre à l'utilisateur de sélectionner la DLL de plug-in.
Et une autre pour tester la classe plug-in
void CImportTarifDlg::OnBnClickedButtonplug()
{
CString OpenFilter;
OpenFilter = _T("Plugin File (*.dll)|*.dll||");
CFileDialog FileOpenDialog(
TRUE,
NULL,
NULL,
OFN_FILEMUSTEXIST|OFN_HIDEREADONLY|OFN_PATHMUSTEXIST,
OpenFilter,
AfxGetMainWnd());
if(FileOpenDialog.DoModal()==IDOK)
{
CFile File;
VERIFY(File.Open(FileOpenDialog.GetPathName(),CFile::modeRead));
m_strPath=FileOpenDialog.GetPathName();
UpdateData(FALSE);
}
}
void CImportTarifDlg::OnBnClickedButtontest()
{
UpdateData(TRUE);
if(m_Plug.LoadPlugin(m_strPath))
{
CPlugImportTarif *p=m_Plug.GetPlugIn();
p->SetFile(_T("essai.txt"));
p->PreFiltre();
m_Plug.FreePlugin();
}
}
|
L'utilisation est très simple, après sélection de la DLL par l'utilisateur il ne me reste plus qu'à lire le plug-in, récupérer une instance de ma classe et d'appeler les différentes fonctions.
VIII. Conclusion
La mise en place et l'utilisation de plug-in de classe en utilisant les DLL d'extensions est relativement facile à mettre en œuvre,
Il ne vous reste plus qu'a trouver votre terrain d'application.


Copyright © Farscape. Aucune reproduction, même partielle, ne peut être faite
de ce site et de l'ensemble de son contenu : textes, documents, images, etc
sans l'autorisation expresse de l'auteur.
Sinon vous encourez selon la loi jusqu'à 3 ans de prison et jusqu'à 300 000 E
de dommages et intérêts.
Cette page est déposée à la
SACD.