Tutoriel – Implémenter un système de plugin (Framework .NET – C#)

Intégrer un système de plugin vous permettra d’acquérir quelques notions de programmation orientée composant (nommé par son acronyme POC [en]).

Dans le cas présenté, il s’agit de définir une application noyau qui interagit avec des modules externes : les plugins. Ces modules externes seront ici des librairies dynamiques (dll) qui seront chargés à la volée, lors de l’exécution du programme principal.

Tout au long de ce tutoriel, les plugins seront nommés modules.

Pourquoi créer un système de plugins ?

Les avantages d’un tel système sont nombreux :

  • Extensibilité d’une application qui ne nécessite aucune re-compilation
  • Permettre à des développeurs tiers d’implémenter des modules sans avoir accès au code source de l’application noyau.
  • Ré-utilisabilité des modules dans d’autres projets

Cependant il n’est pas nécessaire et même non recommandé de l’implémenter dans tout vos projets. Il faut que ce besoin soit clairement défini et porte un intérêt. En effet, ce système pose une contrainte à ne pas négliger : dans le cas d’une modification importante des interfaces permettant l’ancrage des modules au noyau, l’ensemble des modules écrits sont impactés.

Exemple d’application fonctionnant avec ce système

Nombreuses sont les applications qui proposent l’intégration de modules externes.

A titre d’exemple :

  • Photoshop (Plugins d’effet, etc)
  • Ableton live : VSTi (Instruments de musique virtuels)
  • Apache (module pour exécuter python par exemple)
  • Sublime text
  • MySQL Workbench
  • etc…

Ces plugins sont écrits dans divers langages.

Schéma descriptif

Pré-requis

Avant de commencer l’implémentation d’un projet en POO [en] suivant le paradigme POC, il est vivement recommandé de comprendre les notions de polymorphisme [en], d’interface et de classe abstraite [en]. Il est aussi recommandé de connaître, du moins en surface, les principes de l’introspection [en] (réflexivité).

Les interfaces et les classes abstraites vont permettre de décrire la structure des modules et l’introspection permettra à l’application noyau de charger ceux-ci à la volée.

Description des projets

Le langage C# sera utilisé. Les modules seront donc des « Assembly », par conséquent des DLL (Dynamic link library conformes au code managé .NET).

La création de deux projets au minimum vont être nécessaires pour le fonctionnement de ce système.

1. Le projet général

Il s’agit de l’application noyau. Elle comportera les fonctionnalités de base et doit être capable d’interagir avec un module (le charger, appeler des fonctions, etc).

2. Une librairie d’interface

La librairie d’interface permet de créer le lien entre l’application noyau et ses modules via ses classes, et ses interfaces.
Elle permet notamment à l’application noyau de connaitre les types des objets d’un module.

Pour résumer, cette librairie doit permettre

  • Au projet général de connaître les signatures des fonctions des modules à charger.
  • Au module de connaître les signatures des fonctions à implémenter.

Remarque :

la librairie d’interface est une obligation dans les langages statiquement typés [en] et au typage nominatif [en], sans quoi les types ne peuvent être déterminés comme identiques entre deux composants (modules).

Cette librairie d’interface n’est cependant pas nécessaire dans les langages à typage dynamique [en] tel que javascript, lua, etc.
En effet ceux-ci bénéficient du Duck typing [en] et l’écriture d’un plugin en devient grandement simplifié (voir en complément de l’article).

Implémentation

Dans cet exemple, nous allons implémenter une librairie d’interface, une application et deux modules différents. L’application noyau chargera les modules (dll) contenues dans son dossier « Modules » et exécutera quelques fonctionnalités.

Structure de la solution

La solution d’exemple contiendra la structure de projet suivante

structure-solution
Structure de la solution

POCSample contient donc les projets :

  • POCSample – Il s’agit de l’application noyau
  • ModuleInterface – Il s’agit de la librairie d’interface
  • ModuleA – Un module (plugin)
  • ModuleB – Un autre module

Il faut savoir cependant que rien n’empêche de séparer l’ensemble des projets dans des solutions différentes. C’est d’ailleurs généralement le cas pour une implémentation de modules, car chaque modules sera à priori défini dans des solutions distinctes s’ils sont créé par différent développeurs ne se connaissant pas par exemple.

Librairie d’interface

La librairie d’interface implémente une ou plusieurs interfaces contenant les signatures des fonctions qui correspondent aux fonctionnalités que ces modules doivent implémenter.

structure-module-interface
Projet d’interface

Ci-dessus, nous pouvons voir qu’il a été choisi d’implémenter dans ce cas une seule interface nommée IModule

public interface IModule 
{ 
    string MainMessage { get; } 
    double Compute(double a, double b);
}

L’interface IModule comporte une propriété MainMessage qui permettra d’afficher un petit message concernant le module, et une fonction Compute qui va permettre d’effectuer un calcul défini selon le module. Ce seront les deux fonctionnalités de ce type de module.

Application

Un projet nommé « POCSample » a été créé. C’est l’application noyau. Elle doit référencer l’assembly « ModuleInterface ». Cela lui permettra de connaitre le type « IModule » et donc ses signatures – nous pourrons ainsi disposer de l’auto-complétion grâce à l’inférence de type.

structure-application
Projet « POCSample »

Dans la structure du projet, il y’ a un dossier « Modules ». C’est dans ce dossier que nos modules vont être placés afin de pouvoir être chargés à la volée par l’application principale.

structure-directory-modules
Dossier « Modules » comprenant deux modules

Notez qu’il est important de créer un fichier lambda afin de pouvoir y ajouter l’option « Copier si plus récent » – option présente dans l’onglet Propriété. Cela permettra de copier le dossier « Modules » dans le répertoire de sortie de l’application. Ici nous avons ajouté un fichier read.txt.

Voici le code de l’application – nommée ici POCSample – qui permet le chargement des modules contenus dans le dossier « Modules » et d’en exécuter les fonctionnalités :

using System; 
using System.Linq; 
using System.IO; 
using ModuleInterface; 
using System.Reflection; 

namespace POCSample 
{ 
    class MainClass 
    { 
        public static void Main (string[] args) 
        { 
            //Récupération des fichiers de modules 
            string[] files = Directory.GetFiles ("Modules", "*.dll"); 
            //On parcours les fichiers de modules et on les charges 
            foreach (string file in files) 
            { 
                string absolutePath = String.Format ("{0}{1}{2}", Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location), Path.DirectorySeparatorChar, file); 
                
                //Charge l'assembly 
                Assembly assembly = Assembly.LoadFile (absolutePath); 
                
                //Créé les classes module de type IModule de l'assembly à la volée 
                //Ici nous utilisons SingleOrDefault car l'on a qu'une seule classe IModule par Assembly 
                Type modules = assembly.GetTypes ().SingleOrDefault(x => x.GetInterfaces().Contains(typeof(IModule))); 
                //On instancie 
                IModule module = (IModule)Activator.CreateInstance (modules); 

                //On peut le charger aussi de cette manière 
                //Cependant cette ligne de code est dépendante du nom du namespace et de la classe 
                //IModule module = (IModule)assembly.CreateInstance ("Module.Module"); 

                //Affiche le message principal du module en cours 
                Console.WriteLine (module.MainMessage); 

                //Effectue le calcul du module en cours pour les paramètres donnés 
                double resultat = module.Compute (10, 5); 

                //Affiche le résultat de l'opération 
                Console.WriteLine ("Résultat de l'opération : {0}", resultat); 
            }

            Console.ReadKey (); 
        } 
    } 
}
  • Tout d’abord les fichiers dll contenus dans le dossier « Modules » sont énumérés.
  • Ensuite ils sont parcourus afin de les charger grâce à la classe Assembly contenu dans le namespace System.Reflection.
  • Une fois l’assembly chargé, il est nécessaire de retrouver les classes du module qui implémentent IModule.
  • Ces classes sont instanciées par réflexion en utilisant ici la classe Activator du namepace System.Reflection.
  • Après toutes ces étapes, il est désormais possible d’utiliser la classe chargé du module comme n’importe quel objet. On peut donc en exécuter toutes les fonctionnalités.

Remarque : Il faut une étape supplémentaire qui consiste à vérifier la validité des assembly chargés.

Modules

En dernier lieu, nous allons voir l’implémentation d’un module.

Un projet nommé ModuleA a été créé. Celui-ci, une fois compilé, pourra être placé dans le dossier Modules de l’application principale.

Module A :

using System; 
using ModuleInterface; 

namespace Module 
{ 
    public class Module : IModule 
    { 
        
        public string MainMessage => "Module A Chargé, il permet de multiplier deux nombres";
        public double Compute (double a, double b) => a * b;

    } 
}

Le module référence la librairie d’interface ModuleInterface et la classe implémente les méthodes de IModule.
Ainsi, le module ModuleA est prêt à fonctionner.

Ci-dessous, on comprends maintenant aisément que lors du chargement de ce module à la volée dans notre application POCSample, l’appel de la propriété MainMessage de IModule, fera ici référence à l’implémentation du module chargé. Donc pour le module ModuleA, le message affiché sera : « Module A Chargé, il permet de multiplier deux nombres ».

//On instancie 
IModule module = (IModule)Activator.CreateInstance (modules); 
//Affiche le message principal du module en cours 
Console.WriteLine (module.MainMessage);

Pour finir, nous dirons que la librairie d’interface sert au programme POCSample à comprendre que le ModuleA implémentant IModule et le ModuleB ou ModuleC, etc, implémentent la même interface IModule.


A partir de ce tutoriel vous devriez être capable de dériver la méthode afin d’obtenir le système de plugins que vous désirez.

Pour ma part j’ai créé un système de plugins dans un de mes projets personnel qui utilise des parsers de fichiers. Mon application qui ne lit par défaut que les fichiers au format Xml peut ainsi être étendue pour lire d’autres formats (Json, Texte, …) par la simple écriture d’un nouveau parser. Ceux-ci peuvent être développés par n’importe qui, téléchargés à part, sans modifier l’application noyau. Pratique pour les mises à jours !

Je vous remercie d’avoir pris le temps de lire ce tutoriel et j’espère qu’il aura été clair et compréhensible. Ci-dessous vous trouverez le lien de téléchargement du projet d’exemple présenté.


Télécharger le projet (Xamarin Studio) – 16.1 Ko


4 thoughts on “Tutoriel – Implémenter un système de plugin (Framework .NET – C#)

Laisser un commentaire