by Jesper K. Pedersen <blackie@kde.org>

traducción al español por Duncan Mac-Vicar P. <duncan@kde.org>

¿Cómo crear una infraestructura de plugins para una aplicación KDE?

Los Plugins le dan a una aplicación la capacidad de agregar funcionalidades nuevas en tiempo de ejecución. Aparte de que esta muy de moda decir que tu aplicación tiene soporte para plugins, esto también tiene varias ventajas reales: Este documento te llevará de la mano por todos los pasos requeridos para crear una infraestructura para una aplicación basada en KDE. Vale la pena leerlo incluso si no estás desarrollando una infraestructura de plugins sino que un plugin en si mismo.

Existen dos tipos de plugins:

El principal foco de este documento esta en los plugins del segundo grupo, pero se añadirán los comentarios necesarios para destacar la diferencia con los primeros.

La aplicación de ejemplo

Junto con este documento se incluye una aplicación de ejemplo con una infraestructura de plugins. La aplicación puede ser descargada aquí, y durante el tutorial nos referiremos a esta aplicación como ejemplo.

Los archivos están separados en tres carpetas (directorios):

Los Pasos

Los pasos para construir una infraestructura de plugins para una aplicación son: El orden en que se describen los pasos en este documento es ligeramente diferente al orden anterior, simplemente para facilitar la comprensión.

Partiremos mirando como todo lo anterior se lleva a cabo desde un punto de vista C++, y luego veremos que archivos auxiliares necesitaremos. Ej: Makefile.am's y archivos .desktop

Clase base para los plugins

Bajo los cimientos de KDE, lo siguiente es lo que sucede cuando un plugin es cargado: la aplicación encuentra la biblioteca dinámica la cual es en si misma el plugin, la bliblioteca es abierta mediante la función dlopen, y se busca una función específica escrita en C y ejecutada si es encontrada. Esta funcion de C debe retornar algo valioso para el programa cargando el plugin. En KDE, debe retornar un puntero a la clase base definida por la función que realiza la llamada.

En otras palabras, el resultado de cargar un plugin es un puntero a una clase conocida por la aplicación que carga el plugin. Por lo tanto nuestra primera tarea es crear una clase virtual base que los plugins puedan implementar.

En nuestra aplicación de ejemplo, esta clase es interfaces/plugindemo/plugin.h. La clase esta, así como todos los archivos de la carpeta template, en el espacio de nombres PluginDemo. Esto no es un requisito, pero sirve para evitar colisiones de nombres, y para hacer mas legible de donde viene cada cosa. De la misma manera, el archivo esta en la subcarpeta plugindemo, lo que significa que incluir el archivo se realiza mediante una instrucción include como #include <plugindemo/plugin>, esto también para evitar colisiones de nombres.

Es importante que el lector note que esta clase hereda de KXMLGUIClient. Esto tiene como consecuencia que la aplicación padre podría mezclar sus acciones en la barra de menú o de herramientas con las acciones y funcionalidad agregadas por el plugin. Discutiremos como se hace estodespués.

Dos métodos están definidos en esta clase: editorInterface(), y selectionInterface(). Para que los plugins sean realmente útiles, deben ser capaces de acceder a las partes internas de la aplicación a la cual están siendo enchufados (cargados). Estos dos métodos retornan una interfaz para acceder a la aplicación. Esta interfaz es implementada como veremos después, por la aplicación padre. Por ahora solo debemos tener en mente que los plugins pueden accesar la aplicación a través de dos clases que se encuentran en: interfaces/plugindemo/editorinterface.h y interfaces/plugindemo/selectioninterface.h.

Si nuestro plugin hubiese sido del primer tipo (módulo cargable), el lector habría observado las siguientes diferencias:

Desarrollando plugins

Vamos directo a los plugins y veamos como se implementan. Un plugin de ejemplo se puede encontrar en plugindemo-capitalize/capitalizeplugin.h y plugindemo-capitalize/capitalizeplugin.cpp. Este plugin utiliza la interfaz de selección para transformar a mayúsculas la primera letra de cada palabra en la selección.

La primera cosa que debemos notar es que esta clase hereda de PluginDemo::Plugin, esto es un requisito para que sea un plugin después de todo. Desde un punto de vista C++, esta es simplemente una clase de C++, que puede por supuesto implementar slots, tener variables de instancia, conectar señales, etc. La unica cosa truculenta acerca de esta clase es la manera en que se construye. Esta clase es construida por la aplicación padre cargando la biblioteca que la define en en forma dinámica, y mediante una serie de indirecciones, creando una instancia de dicha clase.

Entendiendo el mecanismo de carga

Para entender como funciona esto, mira la implementación (plugindemo-capitalize/capitalizeplugin.cpp). La parte más importante del código se encuentra al comienzo del archivo, donde verás las siguientes líneas:
  K_EXPORT_COMPONENT_FACTORY( plugindemo_capitalize,
                              KGenericFactory<CapitalizePlugin>( "plugindemo_capitalize" ) );
K_EXPORT_COMPONENT_FACTORY es una macro definida por KDE, pero para entender realmente que hace es más util mirar su definición:
  #define K_EXPORT_COMPONENT_FACTORY( libname, factory ) \
    extern "C" { void *init_##libname() { return new factory; } }
Esto nos muestra que la macro se expande a una definición de una función de C [2], y luego retorna una instancia de la clase definida por el segundo argumento que se le pasa a la macro.

El segundo argumento ya inclye un poco de magia, este dice:

  KGenericFactory<CapitalizePlugin>( "plugindemo_capitalize" )
KGenericFactory es un template (plantilla), que toma a otra clase para instanciar el template. KGenericFactory tiene un método necesario para que todo esto junto funcione, llamado create(). Este método será llamado desde el subsistema de KDE, creando una instancia de la clase dada como parámetro durante la instanciación de dicho template.

Si todo esto te parece magia negra, no te preocupes, no es importante a la hora de desarrollar un plugin y fue incluido en este tutorial para que conozcas todos los detalles. Todo lo que necesitas recordar son los pasos siguientes:

Mezclando la interfaz de usuario con la aplicación padre

Mirando el cuerpo del constructor, verás las siguientes dos líneas:
    setInstance(KGenericFactory::instance());
    setXMLFile("plugindemo_capitalizeui.rc");
Lo que hacen básicamente es cargar la interfaz de usuario del plugin, y mezclarla con la de la aplicación padre. Esto significa que el plugin puede añadir ítemes a la barra de menú y de herramientas de la aplicación padre que contiene al plugin.

El nombre especificado como argumento a la función setXMLFile() es un archivo de recursos, el que debe ser instalado por el archivo Makefile.am del plugin. Los detalles de Makefile.am serán explicados después. El contenido de este arhivo es descrito en este tutorial.

El archivo de recursos especifica que se debe añadir una acción en el ítem Edit de la barra de menús. Estas acciones se deben iniciar en el constructor. En ese momento, una instancia de KApplication esta siendo creada.

Comunicandose con la aplicación padre

La comunicación con el plugin sucede a través de las interfaces disponibles en la superclase (interfaces/plugindemo/plugin.h). Un ejemplo de esto puede ser observado en el método slotCapitalize(), el cual es invocado cuando el usuario selecciona nuestra acción desde la barra de menús. En este método, se le consulta a la interfaz de selección por el texto seleccionado, el cual luego transformamos cada primera letra de una palabra a mayusculas, y volvemos a asignar el texto, esta vez con la versión transformada.

Cargando plugins

A llegado la hora de mirar a la aplicación padre que se encuentra en application/mainwindow.cpp, y ver que tan fácil es realmente cargar los plugins.

La primera cosa que debemos notar en la aplicación padre es que su ventana principal hereda KMainWindow. Esto es necesario para la mezcla de las barras de herramientas y los ítemes del menú.

La carga de los plugins se realiza en la función loadPlugins(). La primera línea de esa función es:

    KTrader::OfferList offers = KTrader::self()->query("PluginDemo/Plugin");
Lo que esto hace es básicamente preguntarle al sistema KDE por la lista de plugins que calzan con el tipo PluginDemo/Plugin. Cada plugin debe ser acompañado de un archivo .desktop que especifica su tipo. Discutiremos los detalles de los archivos .desktop después.

Ahora iteramos sobre la lista de plugins, y los cargamos uno a uno. Esto se hace con el siguiente código:

    for(iter = offers.begin(); iter != offers.end(); ++iter) {
        KService::Ptr service = *iter;
        int errCode = 0;
        PluginDemo::Plugin* plugin =
            KParts::ComponentFactory::createInstanceFromService
            ( service, _pluginInterface, 0, QStringList(), &errCode);
Toda la programación dura relacionada con abrir el archivo de biblioteca y llamar a la función de inicialización es realizada por la función createInstanceFromService.

Cuando se carga un plugin pueden suceder muchas cosas. Podría suceder que ni siquiera exista un plugin para nuestra aplicación, el usuario podria haber escrito incorrectamente algo en el archivo .desktop, etc. Por lo tanto es muy importante cersiorarse de que realmente obtuvimos un puntero de regreso desde el método createInstanceFromService(). Debemos avisar al usuario si la carga de un plugin no se completó con éxito, pero por simplicidad nos saltaremos esa parte ahora.

Quizás habrás notado que el segundo parámetro al método createInstanceFromService() es _pluginInterface, este es al puntero padre, pero en este framework necesita tener algunas características especiales descritas en la próxima sección.

Una vez que el plugin ha sido cargado, es el momento de añadirlo a KXML factory, de esta manera, la interfaz de usuario del plugin es mezclada con la de la aplicación padre. Esto se hace con la siguiente llamada:

  guiFactory()->addClient(plugin);

La comunicación entre los plugins y las aplicaciones padres.

Hasta ahora hemos aceptado que la comunicación entre el plugin y la aplicación que los contiene es a traves de los métodos de la interfaz que es retornada por la clase Plugin en interfaces/plugindemo/plugin.h.

Estas funciones necesitan de alguna manera obtener información acerca de la aplicación que los contiene y es esto mismo lo que discutiremos en seguida.

Mirando la implementación de selectionInterface() podremos deducir como sucede esto:

PluginDemo::SelectionInterface* PluginDemo::Plugin::selectionInterface()
{
    return static_cast
           ( parent()->child( 0, "PluginDemo::SelectionInterface" ) );
}
El plugin simplemente pregunta a su padre por el hijo que hereda la clase selectionInterface. En otras palabras, el padre necesita obtener una instancia de la clase que hereda SelectionInterface. Cómo obtener esta instancia se puede ver en el archivo application/mainwindow.cpp
    _pluginInterface = new QObject( this, "_pluginInterface" );
    new MySelectionInterface( _editor, _pluginInterface, "selection interface" );
En lo anterior, creamos el objeto _pluginInterface, y le damos un puntero a la clase MySelectionInterface. El puntero _pluginInterface es dado después como el argumento "padre" cuando construimos los plugins (como podemos ver en el párrafo anterior):
   KParts::ComponentFactory::createInstanceFromService
     ( service, _pluginInterface, 0, QStringList(), &errCode);
La implementación actual de la interfaz SelectionInterface está en application/myselectioninterface.h y en application/myselectioninterface.cpp.

En el código anterior quizás te diste cuenta de que nadie implementó la interfaz EditorInterface. Esto es para ilustrar como una aplicación contenedora puede implementar solo alguna de las interfaces. Imaginemos que los plugins son compartidos por varias aplicaciones. No todas las aplicaciones podrían ser capaces de ofrecer todos los servicios de la interfaz (por lo tanto no podrían ofrecer dicha interfaz). Los plugins que requieran dicha interfaz implementada no funcionarán cuando esa interfaz no esté implementada por la aplicación contenedora. Por lo tanto cuando un plugin necesite acceso a cierta interfaz, deberá primero corroborar que dicha interfaz esté disponible, y si no lo está, desactivarse a si mismo.

En este punto, estoy casi seguro de que te habrás preguntado ¿Por qué diablos se implementó la comunicación entre los plugins y la aplicación contenedora de esa manera?. para ser honesto contigo, esta no era mi idea inicialmente. Mi idea original iba por el lado de una clase virtual PluginInterface, donde los plugins recibían un puntero a esta en el momento de su creación, y que la aplicación padre (contenedora) tenía que implementar.

Esto era mucho más claro, y tenía un diseño mucho mejor desde el paradigma de objetos, pero tiene una desventaja terrible, es imposible agregar nuevas interfaces de una manera compatible a nivel binario. Por lo tanto la compatibilidad a nivel binario se rompe cuando una interfaz nueva es añadida, y todos los plugins deben ser recompilados.

Con la solución actual, una nueva interfaz se reduce a añadir unos cuantos métodos no-virtuales al archivo plugin.h. Un ejemplo podría ser una interfaz de párrafos, la cual podria resultar en un método que retorne un puntero a dicha clase ParapgrahInterface. Añadir métodos no-virtuales a una clase se puede hacer sin romper la compatibilidad binaria.

Archivos auxiliares necesarios para que todo funcione

Ahora que ya conoces cómo todas las piezas encajan para desarrollar una estructura de plugins para tu aplicación, aún no es suficiente. Aún queda por decirle a KDE que el código que compilas como un plugin es realmente un plugin, y debes decirle a KDE de que tipo es este plugin, información que será usada por KTrader en la sección anterior. De esto es lo que ahblaremos en esta sección.

El directorio interface

El archivo Makefile.am en interfaces/plugindemo necesita una añadidura comparado con un Makefile.am de una biblioteca, la línea es la siguiente:
  kde_servicetypes_DATA = plugindemoplugin.desktop
Lo que esta línea dice, es que el archivo plugindemoplugin.desktop debe ser instalado en el sistema como un tipo de servicios.

Recuerda, cuando cargamos los plugins, no especificamos ningún directorio o archivo a KDE para su búsqueda. Todo lo que especificamos era que queríamos cargar un plugin del tipo (respecto a los servicios que ofrece) PluginDemo/Plugin. El archivo interfaces/plugindemo/plugindemoplugin.desktop describe el tipo de servicios:

[Desktop Entry]
Type=ServiceType
X-KDE-ServiceType=PluginDemo/Plugin
Comment=A Demo Plugin
El punto importante en lo anterior es la línea X-KDE-ServiceType=PluginDemo/Plugin, donde podrás reconocer el tipo que solicitamos al cargar los plugins.

Este archivo contiene también algunos atributos, los cuales pueden ser consultados al cargar los plugins, esto esta de todos modos fuera del alcance de este documento.

El directorio plugins

El archivo Makefile.am en el directorio plugindemo-capitalize también necesita de algunas líneas especiales:
kde_module_LTLIBRARIES = plugindemo_capitalize.la
Para que el plugin sea instalado en el lugar correcto, necesitamos especificarlo usandokde_module_LTLIBRARIES.
plugindemo_capitalize_la_LDFLAGS = -module $(KDE_PLUGIN) $(all_libraries)
Los plugins son diferentes de las bibliotecas en el sentido de que no contienen información respecto de su versión, por ejemplo, si tuvieras que cargar Konqueror, pero no te interesa especificar que quieres cargar Konqueror versión x.y, sino simplemente Konqueror. De la misma manera tu nunca especificas el deseo de cargar una versión específica de un plugin [3]. Por esta razón es el parámetro -module a LDFLAGS.
pluginsdir = $(kde_datadir)/plugindemo_capitalize
plugins_DATA = plugindemo_capitalizeui.rc
Como vimos más arriba, El plugin tiene un archivo de recursos que especifica la interfaz gráfica que ofrece el plugin, y este archivo también debe ser instalado en el sistema. De esto se hacen cargo las líneas anteriores.

Atención, el nombre del archivo dado como argumento a setXMLFile() debe calzar con el nombre del archivo en la línea plugins_DATA, y el nombre que das como argumento a KGenericFactory como parte de la macro K_EXPORT_COMPONENT_FACTORY, debe coincidir con el nombre del archivo en la sección plugins_DATA.

kde_services_DATA = plugindemo_capitalize.desktop
Finalmente, el plugin necesita de un archivo .desktop, el cual especificamos en la línea anterior. El archivo .desktop , se ve así:
[Desktop Entry]
Name=Capitalize
Comment=Capitalize word plugin for PluginDemo
ServiceTypes=PluginDemo/Plugin
Type=Service
X-KDE-Library=plugindemo_capitalize
Debemos darnos cuenta de tres cosas importantes en este archivo. Primero la línea ServiceTypes debe coincidir con el tipo de servicios especificado en el archivo .dekstop en el directorio de interfaces. Segundo, la sección X-KDE-Library debe especificar el nombre de la biblioteca donde fue compilado el plugin, sin el sufijo .la. Finalmente, no es coincidencia que el nombre del archivo tenga como prefijo plugindemo_. Si dos aplicaciones instalan un archivo .desktop con el mismo nombre, pasarán cosas bastante desagradables. Por lo tanto siempre debes reservar un espacio de nombres añadiendo el prefijo como el ejemplo.

Conclusiones

Has visto ya un gran ejemplo de como construir una infraestructura de plugins para tu aplicación KDE. Hemos discutido las ventajas de los plugins, pero no hemos discutido como diseñar tus interfaces. Esto es muy similar a diseñar la interfaz de una biblioteca. Debes ser cuidadoso acerca de partes de la aplicación expones a los plugins. Exponer muy poco hará la interfaz inutil para desarrollar plugins que valgan la pena. Exponer demasiado hará imposible cambiar algo en la aplicación contenedora sin romper la compatibilidad con los plugins.

Notas

Créditos


Jesper Kjær Pedersen <blackie@blackie.dk>
Last modified: Mon Feb 23 20:54:03 2004