Publicidad:
Terra
La Coctelera

un rato de sol

i will work harder, ja ja ja, no ahora en serio.

4 Septiembre 2008

¿Cómo hacer una extensión para firefox usando JS y C++?

Escribir una extensión para firefox que utilice un comonente XPCOM implementado en C++ es mucho más sencillo que lo que muestra el caótico estado de la documentación en Mozilla Developers. Por eso, he pensado que sería interesante hacer un minitutorial que recoja en un solo sitio lo que me ha tocado buscar en esta documentación, foros, etc...



Lo primero que hay que dejar claro, el entorno para el que se va a construir el plugin:

  1. Firefox 3.0 (Gecko SDK 1.9)
  2. Mac OS X 10.5

El componente XPCOM que vamos a construir se distribuye como una biblioteca compartida con la extensión, por lo que se debe recompilar contra la versión del SDK de Gecko para la plataforma objetivo, de tal forma que se obtendrá un fichero .dylib en Mac OS X, un fichero .so en Linux o un fichero .dll en Windows. Los makefile y el flags que voy a utilizar son los necesarios para construir la biblioteca en Mac.

¿Qué pinta tiene el plugin que vamos a construir? pues será un plugin de test, para demostrar la arquitectura de los plugin de Mozilla, pero sin mucha funcionalidad. Lo único que va a hacer es sumar dos números.

Las partes de una extensión firefox son las siguientes:

  • Un componente XPCOM: un conjunto de funciones que ofrecen una funcionalidad determinada con el objetivo de ser reutilizados fácilmente. Esta interfaz se expresa en un lenguaje de definición de interfaces (IDL) independiente del lenguaje de implementación. Luego, esta lógica la implementaremos en C++ y podrá ser reutilizada por código escrito en cualquier otro lenguaje, en nuestro caso javascript.
  • Una interfaz gráfica de usuario descrita en el lenguaje XUL: XUL es un lenguaje basado en XML que permite describir una interfaz gráfica de usuario mediante widgets: botones, ventanas, etc. Luego estos ficheros XML son convertidos en widgets en tiempo de ejecución por el motor de renderizado Gecko, del mismo modo en que se renderiza el HTML de una página web. El código XUL es accesible a través de DOM como cualquier otro documento XML. Por ejemplo, toda la interfaz de firefox está descrita en un solo fichero XUL. Además utilizaremos otra funcionalidad intersante los "overlays" porciones de interfaz descritas en XUL que pueden modificar otra interfaz XUL, por ejemplo, usaremos overlays para insertar un botón dentro de la barra de estado del navegador firefox.
  • Código de pegamento escrito en javascript: por ejemplo para asignar un callback que se ejecutará cuando el usuario haga click en el botón XUL anteriormente comentado y mostrar un nuevo diálogo, o acceder al componente XPCOM que vamos a escribir en C++. Básicamente usaremos javascript para pegar la otras piezas que construyamos.
  • Código de empaquetado, registro y distribución de la extensión: todo lo necesario para empaquetar la extensión en un formato de fichero especial llamado XPI (lease zippy) que se puede colgar en un servidor web y hacer que se instale automáticamente mediante el instalador de plugins del navegador firefox con una tecnología denominada XPInstall.

Antes de empezar una precisión, la extensión que vamos a construir es para firefox, pero el componente XPCOM se puede utilizar desde cualquier plataforma mozilla (thunderbird, sunbird, etc). Hay que pensar que firefox es una aplicación que se ha construido sobre una plataforma mucho más amplia y general, con la que se han construido diferentes productos.

Empezamos. Primero necesitamos las herramientas de desarrollo, en Mac, hay que tener instalado las herramientas para desarrolladores que vienen con XCode: compilador, enlazador, autotools, etc, si no lo tienes instalado, buscalo en el segundo disco de instalación de Mac OS X o bájalo del Apple Developer Connection.

Lo siguiente es conseguir el SDK de Gecko, aquí empiezan los problemas, porque el nombre de la plataforma y su organización interna ha cambiado numerosas veces. Incluso hay gente que descargan directamente el código fuente de Firefox (>4*10^6 líneas de código) para obtener una versión de la plataforma de desarrollo. En cualquier caso, la versión actual del SDK se puede descargar desde esta página: http://developer.mozilla.org/index.php?title=En/Gecko_SDK&highlight=Gecko+SDK La versión para firefox 3 es la 1.9. Deberemos descargar el SDK y descomprimirlo en algún directorio de nuestro sistema de ficheros. (El fichero descomprimido se llama xulrunner-sdk).

Ahora podemos empezar a construir nuestro componente. El primer paso es definir la interfaz a través de la que se va a poder acceder a nuestra lógica de negocio. Para ello necesitamos dos cosas: un identificador único para nuestra interfaz y una definición de la misma en IDL (¿alguien dijo CORBA?).

El identificador único, se puede conseguir en UNIX usando el comando uuidgen, que generara por nosotros el identificador, por ejemplo:

 $ uuidgen 
 7F5D8FC8-CF63-474D-8C70-31B6CAEC3201
 

A continuación escribiremos la definición de la interfaz, en nuestro caso tendrá una sola función que sumará dos números y además indicaremos en el mismo fichero el identificador que acabamos de generar:

fichero IMyComponent.idl

 #include "nsISupports.idl"
 
 [scriptable, uuid(7F5D8FC8-CF63-474D-8C70-31B6CAEC3201)]
 interface IMyComponent : nsISupports
 {
   long Add(in long a, in long b);
 };
 

El lenguaje en el que describimos la interfaz es parecido al IDL del OMG, la referencia completa se puede obtener en: http://www.mozilla.org/scriptable/xpidl/idl-authors-guide/index.html Recuerda que debes sustituir el UUID por el que has generad con uuidgen.

Con la interfaz descrita, el siguiente paso es compilarla para genera un fichero de tipo de datos binarios, que permitirá la interoperabilidad con otros lenguajes y una plantilla con el fichero de cabecera C para implementar la interfaz en este lenguaje. Para realizar esta tarea usamos la herramienta xpidl que viene incluida en la distribución del Gecko SDK por ejemplo:

 $PATH_TO/xulrunner-sdk/bin/xpidl -I PATH_TO/xulrunner-sdk/sdk/idl/ -m header IMyComponent.idl
 $PATH_TO/xulrunner-sdk/bin/xpidl -I PATH_TO/xulrunner-sdk/sdk/idl/ -m typelib IMyComponent.idl
 

Ten en cuenta que hay que ejecutar ambos comandos, en el primero se genera la plantilla y en la segunda el fichero binario.

Si todo ha ido bien, podremos empezar a implementar la clase C++ que implementará la interfaz que acabamos de generar, para ello podemos usar el fichero con la plantilla gnerada: IMyComponent.h. Copia el código desde el comentario

 /* Header file */
 class _MYCLASS_ : public IMyComponent
 {
 public:
   NS_DECL_ISUPPORTS
   NS_DECL_IMYCOMPONENT
 
   _MYCLASS_();
 
 private:
   ~_MYCLASS_();
 
 protected:
   /* additional members */
 };
 

en un fichero llamado MyComponent.h y modficalo sustituyendo los valores _MYCLASS_ por MyComponente. Además, se debe añadir información con una descripción de la interfaz, un identificador de contrato y un nuevo uuid para el componente (el anterior era para la interfaz), debes obtener algo similar a lo siguiente

fichero MyComponent.h

 #ifndef _MY_COMPONENT_H_
 #define _MY_COMPONENT_H_
 
 #include "IMyComponent.h"
 
 #define MY_COMPONENT_CONTRACTID "@goomer.com/MyComponent"
 #define MY_COMPONENT_CLASSNAME "A Simple XPCOM Sample"
 #define MY_COMPONENT_CID { 0x786b1ad1, 0x4a63, 0x44ac, \
   { 0x96, 0x84, 0xca, 0x8b, 0x99, 0x43, 0x6f, 0x93 } }
 
 class MyComponent : public IMyComponent
 {
 public:
   NS_DECL_ISUPPORTS
   NS_DECL_IMYCOMPONENT
 
   MyComponent();
   ~MyComponent();
 
 protected:
   /* additional members */
 };
 
 #endif
 

Intentaré explicar un poco que significa el código el fichero. Un componente XPCOM puede implementar una o varias interfaces, pero al menos debe implementar la interfaz nsISupports. El fichero de cabecera de la clase que vaya a implementar esas interfaces debe declarar con funciones de la api de XPCOM que interfaces va a implementar, la forma más rápida de hacerlo es utilizar las macros de la biblioteca XPGLUE, de ahí salen las líneas NS_DECL_ISUPPORTS y NS_DECL_IMYCOMPONENT, que indican que esta clase implementará la interfaz nsISupports y nuestra interfaz IMyComponent. Por otro lado, para poder obtener una referencia a esta clase, se necesita un identificador por el que preguntar, este identificador puede ser un uuid o una cadena con el formato como el que aparece en el código anterior en las líneas:

 #define MY_COMPONENT_CONTRACTID "@goomer.com/MyComponent"
 #define MY_COMPONENT_CLASSNAME "A Simple XPCOM Sample"
 #define MY_COMPONENT_CID { 0x786b1ad1, 0x4a63, 0x44ac, \
   { 0x96, 0x84, 0xca, 0x8b, 0x99, 0x43, 0x6f, 0x93 } }
 

Como observarás, el formato del CID es diferente al de los otros UUID, puedes obtener este formato pasando el parámetro -hdr a uuidgen o puedes visitar este enlace que te generará uno automáticamente:

http://mozilla.pettay.fi/cgi-bin/mozuuid.pl

El siguiente paso es implementar la interfaz, para ello volvemos a copiar la sección de la plantilla que con el código de implementación de la clase:

 /* Implementation file */
 NS_IMPL_ISUPPORTS1(_MYCLASS_, IMyComponent)
 
 _MYCLASS_::_MYCLASS_()
 {
   /* member initializers and constructor code */
 }
 
 _MYCLASS_::~_MYCLASS_()
 {
   /* destructor code */
 }
 
 /* long Add (in long a, in long b); */
 NS_IMETHODIMP _MYCLASS_::Add(PRInt32 a, PRInt32 b, PRInt32 *_retval)
 {
     return NS_ERROR_NOT_IMPLEMENTED;
 }
 

Lo copiamos en un fichero MyComponent.cpp e implementamos el código de la funcion Add:

fichero MyComponent.cpp

 #include "MyComponent.h"
 #include "xpcom-config.h"
 #include "nsIClassInfoImpl.h"
 
 NS_IMPL_ISUPPORTS1(MyComponent, IMyComponent)
 
 MyComponent::MyComponent()
 {
   /* member initializers and constructor code */
 }
 
 MyComponent::~MyComponent()
 {
   /* destructor code */
 }
 
 /* long Add (in long a, in long b); */
 NS_IMETHODIMP MyComponent::Add(PRInt32 a, PRInt32 b, PRInt32 *_retval)
 {
   *_retval = a + b;
   return NS_OK;
 }
 

En este código se usa la macro NS_IMPL_ISUPPORTS1 que indica como se implementa la interfaz IMyComponent y además ofrece una implementación por defecto de la interfaz nsISupport.

El último fichero que debemos crear es el fichero con el código que declara el módulo en el que va insertado el componente, y que debe ofrecer un método factory para construir el objeto que implementa la interfaz del componente, su código es muy sencillo:

fichero MyComponentModule.cpp

 #include "MyComponent.h"
 #include "nsIGenericFactory.h"
 #include "xpcom-config.h"
 #include "nsIClassInfoImpl.h"
 
 NS_GENERIC_FACTORY_CONSTRUCTOR(MyComponent);
 
 static nsModuleComponentInfo components[] =
 {
     {
        MY_COMPONENT_CLASSNAME,
        MY_COMPONENT_CID,
        MY_COMPONENT_CONTRACTID,
        MyComponentConstructor,
     }
 };
 
 
 NS_IMPL_NSGETMODULE(MyComponentModule, components);
 

Cómo se puede observar, seguimos usando las macros de XPCOMGLUE para facilitarnos la labor de escribir aburrido código de infraestructura.

Ya está preparados para compilar el componente, edita un fichero llamado Makefile con el siguiente contenido:

fichero Makefile

 CXX   = c++ 
 CPPFLAGS = -dynamiclib
 
 GECKO_SDK_PATH = PATH_TO/xulrunner-sdk/sdk
 
 GECKO_INCLUDES = -I $(GECKO_SDK_PATH)/include
 GECKO_LD_FLAGS = -L$(GECKO_SDK_PATH)/lib -L$(GECKO_SDK_PATH)/bin -Wl,-executable_path,$(GECKO_SDK_PATH)/../bin -lxpcomglue_s -lxpcom -lnspr4
 
 build: 
 	$(CXX) -Wall -Os -o MyComponentModule.dylib $(GECKO_CONFIG_INCLUDE) $(GECKO_DEFINES) $(GECKO_INCLUDES) $(GECKO_LD_FLAGS) $(CPPFLAGS) $(CXXFLAGS) MyComponent.cpp MyComponentModule.cpp 
 	chmod +x MyComponentModule.dylib
 clean:
 	rm MyComponentModule.dylib
 

Como se puede ver un makefile muy simple que incluye las opciones para generar la librería dinámica y enlaza con el framework de GECKO.

Lanzamos un

 $make
 

Y si todo va bien, deberíamos obtener el fichero MyComponentModule.dylib, que contendra la lógica de nuestra extensión.

Antes de empezar a diseñar la interfaz gráfica de usuario de la misma, vamos a construir la estructura de directorios para empaquetar el componente. En un directorio vació crea la estructura siguiente:

 |------chrome/
 |           |
 |           |------content/
 |
 |------components/
 

Y copia los ficheros MyComponentModule.dylib y IMyComponent.xpt generados anteriormente al directorio components

El resto de ficheros de la extensión los crearemos dentro del diretorio chrome/content/

Construiremos en primer lugar un fichero javascript con el código necesario para acceder al componente que acabamos de crear, crea el fichero MyComponent.js en el directorio chrome/content/

fichero MyComponent.js

 function sumThem() {
   tf1 = document.getElementById("dialog.input1").value;
   tf2 = document.getElementById("dialog.input2").value;
 
   var mycomponent = Components.classes["@goomer.com/MyComponent"].getService();
   mycomponent = mycomponent.QueryInterface(Components.interfaces.IMyComponent);
 
   tf3 = mycomponent.Add(tf1,tf2);
 
   document.getElementById("dialog.output").value = tf3;
 
 }
 
 function loadMyComponent() {
   openDialog("chrome://mycomponent/content/MyComponent.xul");
 }
 

Como se puede observar la función sum utiliza el identificador CID que hemos asignado al componente para obtener una referencia al mismo, busca la interfaz que nos interesa de entre las que este componente implementa y, por fin invoca al método Add de dicha interfaz.

Los valores para ser sumados los obtiene la función del valor de dos elementos a los que accede a través de los métodos DOM getElementById. Estos elementos son partes de la interfaz de usuario que vamos a especificar a continuación en XUL. Del mismo modo la función loadMyComponent abre un diálogo a descrito en una fichero XUL que crearemos a continuación. Por último, se puede observar como al cargar este último fichero, se sigue la ruta de la estructura de directorios que acabamos de crear.

Pasemos pues a describir la interfaz de usuario, para ello añadiremos dos ficheros más a nuestro directorio content. El primero MyComponent.xul describe un cuadro de diálogo con tres campos de texto (2 para los sumando y 1 para el resultado) además de un botón que ejecutará al recibir un clic la función javascript sumThem que acabamos de implementar:

fichero MyComponent.xul

 <?xml version="1.0"?>
 <?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
 <dialog id="MyComponent dlg"
         xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
         title="MyComponent Test"
         style="width: 30em;"
         persist="screenX screenY"
         screenX="24" screenY="24">
   
 
   <hbox>
     <separator orient="vertical" class="thin"/>
     <vbox flex="1">
       <separator class="thin"/>
       <hbox align="center">
         <textbox id="dialog.input1" flex="1" />
         <textbox id="dialog.input2" flex="1" />
         <button label="Sum"
            onclick="sumThem();"/>
       </hbox>
       <hbox align="center">
         <textbox id="dialog.output" flex="1" />
       </hbox>
     </vbox>
   </hbox>
  </dialog>
 

Como se puede observar, en este componente se llama a la función sumThem definida en el fichero javascript anterior mediante un callback en el botón de la interfaz XUL.

El otro fichero de interfaz es el overlay, que describe un botón con una etiqueta que se "mezclará" con el fichero XUL que describe la interfaz de firefox, de modo que la interfaz del navegador se modificará para mostrar el botón descrito en el overlay en la barra de estado del naveagdor.

fichero overlay.xul

 <?xml version="1.0"?>
 <overlay id="sample"
          xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
 
   
 
  <statusbar id="status-bar">
   <statusbarpanel id="my-panel" label="Hello, World" onclick="loadMyComponent()"/>
  </statusbar>
 </overlay>
 

En ambos ficheros XUL referencial el fichero javascript a través de la ruta del componente que estamos construyendo.

Ahora el componente está terminado, falta escribir los ficheros necesarios para distribuir la extensión, estos ficheros contienen metainformación sobre el plugin que firefox utilizará para registrar e iniciar el plugin.

El primer fichero se llama chrome.manifest y debe crearse al mismo nivel que el directorio chrome. En este fichero se describen aquellos contenidos que deben registrarse en el chrome de firefox. En nuestro caso, el único componente es el overlay, para el que deberemos indicar con que fichero debe ser mezclado. Especificaremos que este fichero de destino es el fichero XUL que describe la interfaz del navegador. Adicionalmente, también indicaremos que directorio dentro de la extensión es la que contiene los contenidos.

fichero chrome.manifest

 content     maycomponent    chrome/content/
 overlay chrome://browser/content/browser.xul chrome://mycomponent/chrome/sample.xul
 

El segundo fichero de configuración es un documento RDF llamado install.rdf y que contiene metadatos sobre el autor de la extensión, versión y versión de firefox con la que es compatible. El UUID que aparece en el fichero es el que identifica a firefox, ese valor debe ser el mismo para todas las extensiones.

fichero install.rdf

 <?xml version="1.0"?>
 
 <RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
      xmlns:em="http://www.mozilla.org/2004/em-rdf#">
 
   <Description about="urn:mozilla:install-manifest">
     <em:id>mycomponent@goomer.com</em:id>
     <em:version>1.0</em:version>
     <em:type>2</em:type>
 
     <!-- Target Application this extension can install into,
          with minimum and maximum supported versions. -->
     <em:targetApplication>
       <Description>
         <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
         <em:minVersion>3</em:minVersion>
         <em:maxVersion>3.0.*</em:maxVersion>
       </Description>
     </em:targetApplication>
 
     <!-- Front End MetaData -->
     <em:name>My Component</em:name>
     <em:description>A test extension</em:description>
     <em:creator>Your Name Here</em:creator>
     <em:homepageURL>http://www.example.com/</em:homepageURL>
   </Description>
 </RDF>
 

Nuestro componente está listo para ser empaquetado y distribuido, en este momento, nuestro arbol de directorios debe tener el siguiente aspecto:

 |------install.rdf
 |------chrome.manifest
 |------chrome/
 |           |
 |           |------content/
 |                        |------------MyComponent.js
 |                        |------------MyComponent.xul
 |                        |------------overlay.xul
 |
 |------components/
                 |---------------MyComponentModule.dylib
                 |---------------IMyComponent.xpt
 

Ahora debemos crear el fichero XPI que será el que se instale en el navegador, para ello comprimimos el árbol anterior en un fichero .ZIP y le cambiamos la extension por XPI. Nuestra extensión está lista para ser instalada. Para que podamos probarla, podemos escribir un fichero html con un pequeño script javascript que inicia la instalación de la extensión mediante la tecnología XPInstall.

 <dialog id="MyComponent dlg"
         xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
         title="MyComponent Test"
         style="width: 30em;"
         persist="screenX screenY"
         screenX="24" screenY="24">
   
 
 <a href="mycomponent.xpi"
   hash= "md5:37a50076a58a4be50bb88ca12102fc62"
   onclick="return install(event);">Instalame!!!</a>
 

Para el valor del atributo hash, puedes utilzar la utilidad de linea de comandos md5 que genera le valor de hash para un fichero, si no indicas un valor de hash correcto, el proceso de instalació fallará con una advertencia de seguridad.

Si ahora abres con firefox el fichero test.html y haces clic en el enlace, se inicará el proceso de instalación de la extensión y tras reiniciar el navegador, abajo a la derecha debería aparecer el botón XUL descrito en el overlay.

Si se hace clic en el botón, el cuadro de texto aparecerá, y al introducir los valores deseados y hacer clic en el botón de sumar, se utilizará el componente XPCOM para realizar la suma.

Para comprobar que el componente está correctamente instalado puedes utilizar una extensión llamada XPCOMViewer, que lista todos los componentes instalados con sus interfaces y métodos http://xpcomviewer.mozdev.org/.

¡Felicidades has terminado tu primera extensión para Firefox! El siguiente paso es la documentación de Mozilla para empezar hacer componentes y extensiones más útiles, que utilicen la funcionalidad ofrecida por la gran colección de componentes distribuidos con firefox.

Tags: mozilla

servido por Antonio 3 comentarios compártelo

3 comentarios · Escribe aquí tu comentario

Rocío

Rocío dijo

Hola Antonio,

Gracias por toda esta explicación, pues me ha resuelto ya un buen trozo de todo lo que tenía encima. Ahora, tengo otra pregunta... si lo que quiero es escribir el código del plugin partiendo de una aplicación escrita en java... que herramienta tengo que utilizar? es que estuve mirando javaXPCOM, para comunicar java y la parte de XPCOM, pero me da que solo sirve para incrustar Gecko en una aplicación java (en teoría tenía noticias de que era para comunicación entre java y XPCOM, pero por lo que he visto, o no está muy desarrollado o yo no me entero de nada...). Es que verás, la aplicación que tengo que convertir en plugin tiene muchísimas lineas de código, y para escribirlas todas en idl... me puede llevar un buen rato...

Ojalá tengas algún tipo de respuesta a esto... es que me urge bastante... y no encuentro a nadie que sepa del tema ni páginas que expliquen este tipo de cosas claramente (para mis conocimientos sobre este tema).

Muchas gracias!

Un saludo
Rocío

6 Julio 2009 | 10:53 PM

Escribe tu comentario


Sobre mí

Avatar de Antonio

un rato de sol

Barcelona/Salamanca, España
ver perfil »
contacto »
Trabajador del metal y del acero, en la gloriosa XING AG, escribo software con el que poder ganarme el jornal. En mi tiempo libre sigo tecleando código de bonitos colores a medio camino entre lo sublime y lo terrible. Últimamente me gustan mucho los gatos.

Fotos

Antonio Garrote Hernández todavía no ha subido ninguna foto.

¡Anímale a hacerlo!

Buscar

suscríbete

Selecciona el agregador que utilices para suscribirte a este blog (también puedes obtener la URL de los feeds):

¿Qué es esto?

Crea tu blog gratis en La Coctelera