DLL d'extensions : écriture d'un plug-in de classe - Club des décideurs et professionnels en Informatique

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

// Cible de la commande CPlugImportTarif

class CPlugImportTarif : public CObject
{
public:
	CPlugImportTarif();
	virtual ~CPlugImportTarif();
};
Je vais modifier la classe pour qu'elle soit exportable dans faq une DLL d'extensions et surtout définir mon interface de traitement.

#pragma once

// Cible de la commande CPlugImportTarif

class AFX_EXT_CLASS CPlugImportTarif : public CObject
{
public:
	virtual bool PreFiltre()=0;	// traitement de filtrage du fichier fixé par SetFile.
	virtual void SetFile(LPCTSTR szFileName=_T(""))=0; // fixe le fichier a traiter.
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"

// boîte de dialogue CImportTarifDlg
class CImportTarifDlg : public CDialog
{
// Construction
public:
	CImportTarifDlg(CWnd* pParent = NULL);	// constructeur standard

// Données de boîte de dialogue
	enum { IDD = IDD_IMPORTTARIF_DIALOG };

	protected:
	virtual void DoDataExchange(CDataExchange* pDX);	// Prise en charge de DDX/DDV


// Implémentation
protected:
	HICON m_hIcon;
	CPlugImportTarif *m_pImportTarif; // pointeur sur ma classe de traitement externe.

	// Fonctions générées de la table des messages
	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


// plugimpDupont.h
#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 :

// PlugImpTarDupont.cpp : définit les fonctions d'initialisation pour la DLL.
//

#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)
{
	// Supprimez cet élément si vous utilisez lpReserved
	UNREFERENCED_PARAMETER(lpReserved);
	if (dwReason == DLL_PROCESS_ATTACH)
	{
		TRACE0("Initialisation de PlugImpTarDupont.DLL !\n");
		// Initialisation unique de la DLL d'extension
		if (!AfxInitExtensionModule(PlugImpTarDupontDLL, hInstance))
			return 0;
		new CDynLinkLibrary(PlugImpTarDupontDLL);

	}
	else if (dwReason == DLL_PROCESS_DETACH)
	{
		TRACE0("Arrêt de PlugImpTarDupont.DLL !\n");
		// Terminez la bibliothèque avant que les destructeurs soient appelés
		AfxTermExtensionModule(PlugImpTarDupontDLL);
	}
	return 1;   // ok
}
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.

// ImportTarifDlg.h : fichier d'en-tête
//

#pragma once
#include <stdexcept>
#include <string>
#include "plugimportTarif.h"
 
// la classe de gestion du plugin.
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() // libération du plugin
	{		
		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)// lecture plugin
	{
		// 
		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()// recuperation d'une instance de la classe du plugin.
	{
		// 
		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;
};

// boîte de dialogue CImportTarifDlg
class CImportTarifDlg : public CDialog
{
// Construction
public:
	CImportTarifDlg(CWnd* pParent = NULL);	// constructeur standard

// Données de boîte de dialogue
	enum { IDD = IDD_IMPORTTARIF_DIALOG };

	protected:
	virtual void DoDataExchange(CDataExchange* pDX);	// Prise en charge de DDX/DDV


// Implémentation
protected:
	HICON m_hIcon;
	CPlug<CPlugImportTarif> m_Plug;// la variable de gestion du plugin pour la classe abstraite CPlugImportTarif.
	
	// Fonctions générées de la table des messages
	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()
{
	// TODO : ajoutez ici le code de votre gestionnaire de notification de contrôle
	CString OpenFilter;
	OpenFilter = _T("Plugin File (*.dll)|*.dll||");

	CFileDialog FileOpenDialog(
					  TRUE,
					  NULL,
					  NULL,
					  OFN_FILEMUSTEXIST|OFN_HIDEREADONLY|OFN_PATHMUSTEXIST,
					  OpenFilter,                       // filter
					  AfxGetMainWnd());               // the parent window 
    if(FileOpenDialog.DoModal()==IDOK)
    {
        CFile File;
        VERIFY(File.Open(FileOpenDialog.GetPathName(),CFile::modeRead));
		m_strPath=FileOpenDialog.GetPathName();
		UpdateData(FALSE);
    }
}
void CImportTarifDlg::OnBnClickedButtontest()
{
	// TODO : ajoutez ici le code de votre gestionnaire de notification de contrôle
	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.

Le programme de tests avec deux plug-in: ImportTarif



Valid XHTML 1.1!Valid CSS!

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.

Vos questions techniques : forum d'entraide C++ - Publiez vos articles, tutoriels et cours
et rejoignez-nous dans l'équipe de rédaction du club d'entraide des développeurs francophones
Nous contacter - Hébergement - Participez - Copyright © 2000-2010 www.developpez.com - Legal informations.