Sistema de plugins con C#. Parte II. El código explicado

Antes de empezar

Como indiqué en la parte final del artículo anterior este artículo se ha retrasado tanto que he decidido sacar el código antes de estar acabado. Pese a que la parte fundamental del código es correcta, sobre todo a los efectos de ilustrar este artículo, no debe considerarse en producción y no recomiendo utilizarla para su uso en ninguna aplicación "real", al menos hasta que publique la versión completa.

La web del proyecto y su licencia

El código y otra información adicional está disponible en la página del proyecto Monet Plugins Library y tenéis disponible la última versión del código en el control de versiones de subversion http://svn.thealphasite.org/Monet.

Todo el proyecto responde a la licencia LGPL, lo cual implica, a grandes rasgos que el proyecto es esencialmente GPL (con todas las obligaciones que ello conlleva, como distribuir las modificaciones echas al código y mantener la misma licencia) pero permitiendo que el framework sea utilizado dese una aplicación o librería que no sea GPL. En general esta es una de las licencias que más "libre" me parece. Básicamente, si quieres modificarla y mejorarla tienes que "devolver" lo que has hecho y por tanto la librería va mejorando constantemente, pero si simplemente quieres usarla desde algún otro programa, entonces no encuentras restricciones.

La librería Plugins

Como ya comentamos en el artículo anterior la biblioteca plugins contiene una serie de clases de ayuda que nos serviran para definir todo el sistema.

En esta biblioteca encontraremos tanto la definición de los interfaces que definen un plugin, la definición de los atributos necesarios para declarar Hooks, el interfaz del plugin manager así como una clase que nos permitirá acceder, registrar y recuperar servicios.

Servicios

Para la definición de servicios existen dos clases auxiliares. La primera, CService, encapsula la información del propio servicio, datos como el nombre, el grupo o tipo en el que se engloba el servicio o la descripción del mismo. Así mismo guarda una referencia al objeto que realmente implementa dicho servicio. Está clase es usada por el Plugin Manager para encapsular la información recogida de los atributos de un plugin en cuanto a los servicios que implementa.

La segunda clase, Modules, es una clase estática que nos permite registrar y recuperar servicios del sistema. La clase es usada por el Plugin Manager para registrar los servicios según son declarados por los plugins y por cada plugin o parte del sistema que desee utilizar un servicio simplemente recuperandolo (pidiendoselo) a la clase.

La clase Modules

La implementación de la clase estática modules es relativamente sencilla. Si echamos un vistazo:

public static class Modules
{

  private static Dictionary<string, Dictionary<string, CService>> mServices = new Dictionary<string,  Dictionary<string, CService>>();

  private static List<string> mTypes = new List<string>();
       
  /// <summary>
  /// Devuelve un servicio
  /// </summary>
  public static CService ServiceEx(string serviceType)

  /// <summary>
  /// Devuelve un servicio dado su nombre y su tipo
  /// </summary>
  public static CService ServiceEx(string serviceName, string serviceType)

  /// <summary>
  /// Devuelve el objeto que implementa un servicio
  /// </summary>
  public static object Service(string serviceType)

  /// <summary>
  /// Devuelve el objeto que implementa un servicio
  /// </summary>
  public static object Service(string serviceName, string serviceType)        

  /// <summary>
  /// Devuelve la lista de tipos de servicios registrados
  /// </summary>
  public static List<string> ServicesTypes

  /// <summary>
  /// Devuelve todos los servicios de un determinado tipo
  /// </summary>
  public static List<CService> Services(string type)

  /// <summary>
  /// Registra un nuevo servicio en el sistema
  /// </summary>
  public static bool RegisterService(CService service)

  /// <summary>
  /// Desregistra un servicio del sistema
  /// </summary>
  public static void UnRegisterService(CService service)

  /// <summary>
  /// Carga el plugin especificado
  /// </summary>
  public static object LoadRunnablePlugin(string filename, string name)

Básicamente la clase contiene una lista de servicios y la funcionalidad básica para almacenar (registrar), eliminar (desregistrar) y obtener los distintos servicios. Puede obtenerse tanto el objeto CService en si mismo (con toda la información asociada que contiene) como el objeto que implementa el servicio (que deberá ser casteado al interfaz adecuado). El primer caso será más probablemente usado por el Plugin Manager, para mantener y mostrar la información relacionada con los servicios. El segundo caso probablemente se use mucho más por los plugins que desean acceder y usar el servicio.

Plugins

Dentro de la librería de plugins se definen tanto los interfaces como los atributos necesarios para mantener la información asociada.

El interfaz de plugin explicado

public interface IPlugin

    {

        /// <summary>

        /// Realiza la carga del plugin.

        /// </summary>        

        /// <returns>0 si todo fue correcto. Un numero negativo en cualquier otro caso.</returns>

        int Load();

        /// <summary>

        /// Descarga el mÛdulo de memoria

        /// </summary>

        /// <returns></returns>

        int UnLoad();

        /// <summary>

        /// Indica si un plugin debe ser cargado en un thread independiente

        /// </summary>        

        bool Threaded{ get; }

    }

Cada plugin del sistema debe implementar al menos 2 métodos básicos (carga y descarga) y una propiedad.

Los métodos realizan la carga y descarga del plugin. Esto implica, para el caso de la carga, realizar todas las acciones necesarias para dejar el plugin preparado para su ejecución, por ejemplo, crear un menú en el interface gráfico, realizar toda una serie de acciones en el registro o lanzar una tarea en segundo plano para, por ejemplo, quedarse escuchando de un determinado puerto TCP. La opción de descarga deberá deshacer dichas opciones de forma que el sistema vuelva a estar en el estado que estaba antes de que se cargara el plugin.

Aunque hemos visto que durante la fase de carga el plugin puede, si lo desea, crear una tarea que se encargue de realizar determinadas acciones en background (y pueden concebirse plugins con dicho planteamiento), esta no es la idea inicial. La fase de carga está orientada a preparar el plugin, fundamentalmente los datos que necesite tal como registrar manejadores de eventos en determinados servicios o configurar parte del registro. Si deseamos que nuestro plugin directamente no devuelva el control a la aplicación, sino que sencillamente se quede ejecutando en su propia tarea debemos marca la propiedad Threaded a true. Esto hará (suponiendo que tengamos un plugin manager que soporte esa opción) que el lanzador cree una tarea dedicada para dicho plugin y se la asigne a la ejecución del Load.

Si elegimos este modo de funcionamiento (por ejemplo levantando un servidor TCP en el método Load) deberemos hacer que el método Unload provoque la finalización del bucle de ejecución del método Load.

Definición de los atributos

Además de la definición del plugin existen toda una serie de atributos que nos permitirán definir la metainformación asociada a cada plugin. De está forma podremos marcar las dependencias que este tiene, los hooks que declara y los que intercepta.

No voy a especificar aqui la implementación de dichos atributos porque son bastante básicos de forma que me limitaré a hacer una pequeña descripción de cada uno de ellos:

  • RequiresPluginAttribute Sirve para indicar que un plugin requiere la presencia de otro plugin para funcionar, esto permitirá saber al plugin manager (o a quien desee saberlo en su caso) que no debe cargar el plugin sin la presencia de la dependencia.
  • RequiresServiceAttribute Sirve para indicar que un plugin requiere la presencia de un servicio, independientemente de quien lo implemente. Por ejemplo un plugin puede requerir la presencia de un servicio de comunicaciones independientemente de quien implemente dicho servicio (un plugin TCP, un plugin UDP, etc.
  • HookableAttribute Servirá para indicar al Plugin Manager que el evento declarado es interceptable por otros plugins.
  • HooksAttribute Sirve para declarar que un determinado método del plugin intercepta un evento hookable declarado en otro plugin. El attributo puede utilizarse con un nombre de plugin, de forma que intercepte solo en evento con el nombre dado para el plugin indicado o bien el nombre de un interface, de forma que intercepte el evento
  • PluginAttribute Identifica una clase como plugin y proporciona la información básica de dicho plugin (nombre, versión, descripción y grupo)
  • ServiceAttribute Identifica un servicio. Se define sobre una clase (que además debe ser un plugin) para indicar que dicha clase implementa dicho servicio. Pueden definirse varios servicios sobre la misma clase.
  • RunnableAttribute Identifica un tipo de plugin que se puede ejecutar directamente sin necesidad de ser cargado.

El plugin manager

El plugin manager es el encargado de manejar toda la información que hemos definido anteriormente y hacer buen uso de ella. Mediante el uso de atributos hemos creado un esqueleto de metainformación que nos permite marcar cada plugin así como almacenar información sobre sus características y las acciones que queremos que realice. El Plugin Manager debe encargarse de generar las estructuras de datos necesarias en memoria para mantener dicha información y proveer de los medios adecuados para ejecutar acciones sobre los plugins dados.

Examinando el interfaz del Plugin Manager vemos que tiene una serie de métodos que nos permitirán listar los plugins, verificar si pueden o no cargar (en función de las dependencias que tengan), cargarlos, descargarlos, etc. Por supuesto el propio Plugin Manager es en si mismo un plugin que implementa un servicio. De esta forma, cualquier otro plugin puede hacer referencia y acceder a la información del Plugin Manager.

Creo que el interfaz está suficientemente comentado y es bastante claro por lo que voy a pasar a centrarme en el como realiza las acciones necesarias el Plugin Manager.

Obteniendo la información de los plugins

La información de los plugins se obtiene mediante el método privado GetPluginInfo. Dicho plugin abre cada ensamblado que se le pasa y recorre cada tipo del sistema buscando el interfaz IPlugin. Una vez encontrado adquiere el atributo de plugin correspondiente (si no existe devuelve un error).

Para ello haremos uso de diversas funciones del framework de .NET, concretamente de la parte de reflection que nos permite obtener información de cualquier ensamblado en runtime. Una descripción del uso de Reflection queda fuera de los objetivos de este articulo, para más información sobre reflection visita el artículo Atributos en .NET. Leyendo los atributos con Reflection

De los propios atributos del plugin se obtienen los datos básicos y se almacenan en las estructuras que el plugin manager utilizar para retener esa información. A continuación se comprueba si el plugin es instalable (si implementa el interfaz IInstalablePlugin) y finalmente se comprueban las dependencias declaradas (RequiresPluginAttribute y RequiresServicesAttribute).

Detección de dependencias

Como hemos visto durante el paso anterior lo único que se hace es recopilar información sobre el plugin, incluyendo las dependencias que el plugin ha declarado. El Plugin Manager implementa un método que comprueba si un determinado plugin puede o no cargar dado el estado de sus dependencias.

// Comprobamos que cada uno de los plugins requeridos está cargado
for (int i = 0; i < info.PluginDependencies.Count; i++)
{
  if (!plugins.ContainsKey(info.PluginDependencies[i].Name))
  {                    
    info.PluginDependencies[i].Status = TDependencyStatus.dsNotFound;
    retval = false;
  }
  else if (plugins[info.PluginDependencies[i].Name].State != TPluginState.psLoaded)
  {                    
    info.PluginDependencies[i].Status = TDependencyStatus.dsUnloaded;
    retval = false;
  }
  else
    info.PluginDependencies[i].Status = TDependencyStatus.dsFullFilled;
}
// Comprobamos que cada uno de los servicios requeridos existe
for (int i = 0; i < info.ServiceDependencies.Count; i++)
{
  if (info.ServiceDependencies[i].TypeOnly && Modules.Service(info.ServiceDependencies[i].Type) == null)
  {
    info.ServiceDependencies[i].Status = TDependencyStatus.dsNotFound;
    retval = false;
  }
  else if (!info.ServiceDependencies[i].TypeOnly &&
               Modules.Service(info.ServiceDependencies[i].Type,
               info.ServiceDependencies[i].Name) == null)
  {
    info.ServiceDependencies[i].Status = TDependencyStatus.dsNotFound;
    retval = false;
  }
  else
    info.ServiceDependencies[i].Status = TDependencyStatus.dsFullFilled;
}

El proceso de comprobación es relativamente sencillo, consiste sencillamente en comprobar que, para cada una de las dependencias del plugin dado, estas se encuentran cargadas. Así comprobamos que los plugins necesarios (identificados en la lista de dependencias de plugins leida en el paso anterior) están cargados, así mismo comprobamos que, en caso de que sea un servicio lo que el plugin necesita para funcionar, este está disponible.

Cada "pedazo" de información se guarda en un diccionario (tabla hash). Hay una para las dependencias en otros plugins, y otra para las dependencias en servicios. Lo que hacemos es recorrer cada una de dichas dependencias y comprobar que se satisface.

El proceso de carga

Durante el proceso de carga debemos realizar diversas acciones.
  • En primer lugar comprobaremos que el plugin no está ya cargado.
  • En segndo lugar debemos comprobar que los requisitos del plugin se cumplen, de lo contrario este podría provocar un error.
  • En tercer lugar procesamos los hooks que van a ser interceptados por el plugin así como los hooks del plugin que son interceptados por otros plugins.
  • En cuarto lugar realizamos la carga del plugin propiamente dicha si todo lo demás ha ido bien.
  • Por último, registramos los servicios que implementa el plugin y los asociamos al mismo.

Intercepción de Hooks explicada

Uno de los puntos principales de este sistema de plugins es el mecanismo de hooks y su intercepción, es decir, la posibilidad de que un determinado plugin modifique el comportamiento de otro interceptando sus llamadas.

Para ello, como ya habíamos mencionado, se han definido dos atributos (Hooks y Hookable) utilizado para marcar los métodos que interceptan y los eventos que son interceptados.

El plugin manager es la pasta que se encarga de unir esos dos conceptos. Para ello nos apoyamos en tres métodos auxiliares: ProcessHooks, ProcessHookables y UpdateHookInfo

Los métodos ProcessHookables y ProcessHookInfo se limitan a actualizar la información almacenada sobre los propios hooks dentro de la estructura de información del plugin.

El método UpdateHookInfo se llama cada vez que cambia dicha información y se encarga de asociar los métodos con los eventos, si echamos un vistazo al código.

// Para cada plugin del sistema comprobamos si "hookea"
// alguno de los hooks del plugin que se activa y en la misma
// pasada comprobamos si el plugin activado hookea alguno
// de los metodos del plugin
foreach (TPluginInfo pi in plugins.Values)
{                
  if (pi.GetHashCode() != info.GetHashCode())
  {                        
    // Procesamos los hookables del plugin activado    
    foreach (THookInfo hi in pi.Hooks)
    {                                    
      if (info.Hookables.ContainsKey(hi.Attribute.Name))
      {
        EventInfo e = info.Hookables[hi.Attribute.Name];
                           
        // Obtenemos el delegado apropiado                            
        Delegate d = Delegate.CreateDelegate(e.EventHandlerType,
                                                                            pi.Instance,
                                                                            hi.Method.Name,false);
        if (d != null)
          if (activation)
            e.AddEventHandler(info.Instance,d);
          else
            e.RemoveEventHandler(info.Instance, d);
        }                            
      }

      // Procesamos los hooks del plugin activado
      foreach (THookInfo hi in info.Hooks)
      {
        if (pi.Hookables.ContainsKey(hi.Attribute.Name))
        {
          EventInfo e = pi.Hookables[hi.Attribute.Name];
          // Obtenemos el delegado apropiado                        
          Delegate d = Delegate.CreateDelegate(e.EventHandlerType,
                                                                              info.Instance,
                                                                              hi.Method, false);
          if (d != null)
            if (activation)
               e.AddEventHandler(pi.Instance, d);
            else
              e.RemoveEventHandler(pi.Instance, d);
        }                        
      }
   }                
}

En primer lugar recorremos todos los eventos declarados como hookables y comprobamos si alguno de los plugins ya activos en el sistema los intercepta. A la vez, para cada plugin que vamos mirando comprobamos si contiene algún hook que sea interceptado por el plugin que se activa.

El mecanismo de intercepción se basa en la creación de delegados y en su asignación a los eventos declarados. Para ello en primer lugar obtenemos un EventInfo, que es una estructura de .NET que encapsula la información asociada un evento y nos permite actuar sobre ella, asociado al plugin. Dicho EventInfo contiene dos métodos fundamentales, AddEventHandler y RemoveEventHandler, que nos permiten añadir un manejador al evento. Dichos métodos admiten varias sobrecargas, la que nosotros utilizaremos acepta como parametros una instancia de objeto y un delegado.

public void AddEventHandler(
    Object target,
    Delegate handler
)

Por otro lado el método CreateDelegate de la clase Delegate nos permite obtener un delegado adecuado en función del tipo de delegado, la instancia del objeto que provee el método así como el propio método. De esta forma obtenemos por un lado el evento y por otro lado una instancia válida del delegado (para que dicha instancia sea válida y se pueda construir, el método declarado como hook tiene que coincidir con la definición del delegado).

Otros detalles no explicado

IConfigurable

En ocasiones es posible que queramos permitir la configuración avanzada de un plugin. Para ello se ha definido el interfaz IConfigurable de forma que, para aquellos plugins que lo implementen pueda, el propio plugin, definir un formulario de configuración invocable por el Plugin Manager.

PluginManagerGUI

El Plugin Manager es, en si mismo, un plugin que además proporciona un servicio. La función principal del plugin manager es la carga, descarga y configuración de plugins, así como la obtención de información acerca del mismo.

El interfaz extendido IPluginManagerGUI proporciona un interfaz que define dos métodos (Show y ShowModal) que permiten presentar un interfaz de manejado de dicho funcionamiento. El plugin manager que acompaña el artículo proporciona una implementación de dicho interfaz aunque podría realizarse cualquier otra invocando a los métodos del plugin manager.

IMessageable

En general, siempre que desarrollemos un plugin que dependa otro plugin estaremos creando una dependencia real con él, es decir, si vamos a utilizarlo como servicio, debemos tener, al menos la descripción del interfaz de dicho servicio.

Esto, en general, es la forma habitual de trabajar y la que mayor flexibilidad nos permite, sin embargo en alguna ocasión es posible que no dispongamos de dicha implementación en código o que no queramos proporcionarla. Para este caso se define el interfaz IMessageable que imita el mecanismo de comunicación basado en mensajes de windows. Para ello tenemos la siguiente definición:

public interface IMessageable
{
  /// <summary>
  /// Envia un mensaje al servicio
  /// </summary>
  /// <param name="msg">El mensaje a enviar</param>
  /// <returns></returns>
  IPluginMessage SendMessage(IPluginMessage msg);
  /// <summary>
  /// Envia un comando a un servicio
  /// </summary>
  /// <param name="command">El nombre del comando</param>
  /// <param name="msg">El mensaje asociado al comando</param>
  /// <returns></returns>
  IPluginMessage SendCommand(string command, IPluginMessage msg);
    }

De esta forma podemos mandar mensajes a un determinado servicio. Evidentemente tendremos que conocer el tipo de mensajes y los argumentos que reciben (por mucho que queramos no hay forma de ir a ciegas), pero de esta forma, no necesitamos ningún tipo de definición de interfaz.

Lo ideal es que cada plugin definido, permita acceder de esta forma a cada uno de los servicios que proporciona, de forma que se pueda acceder a ellos de ambos modos...

Conclusión

Hemos visto una forma, de las muchas que hay, de realizar un sistema de plugins. Hay muchos aspectos que probablemente hayan quedado fuera de este artículo pero espero que la presencia del código ayude a solucionar la mayor parte de los problemas que puedan surgir. Para cualquier duda os podéis poner en contacto conmigo en la dirección de contacto o bien posteando los comentarios que queráis.
8.57143
Average: 8.6 (7 votes)
Your rating: None