Gobject introspection, bindings automáticos a python, javascript, etc

danigm's picture

Hace ya algún tiempo empecé a desarrollar una pequeña biblioteca, sobre la que ya hablé en mi blog, para tratar ebooks de tipo epub usango glib.

En un alarde de originalidad, llamé a la bibliotecta libgepub, está escrita en C, utilizando GObject y libarchive y libxml2. En un principio utilizaba webkit para renderizar directamente el contenido, puesto que un epub no es más que xhtml, pero para hacer la biblioteca más usable, decidí que mejor extraía el texto con libxml y devolvía el texto con alguna información de formato, ya que al fin y al cabo, en un libro el formato es relativamente poco importante.

Teniendo ya una biblioteca hecha que da una funcionalidad básica, me propuse que era hora de probar Gobject Introspection.

Gobject Introspection (gir) es una tecnolgía que se inventó la gente esta de Gnome antes de la salida de gnome3. Es más, toda la interfaz nueva de gnome, gnome-shell se basa en gir.

La idea de gir es proporcionar bindings casi automáticos a cualquier lenguaje de programación a partir de bibliotecas escritas en C. Así por ejemplo, desde javascript, gjs, tenemos acceso a todo Gtk+:

const Gtk = imports.gi.Gtk;
    const GLib = imports.gi.GLib;

    // Initialize the gtk
    Gtk.init(null, 0);

    let mwindow = new Gtk.Window ({type : Gtk.WindowType.TOPLEVEL});
    let label = new Gtk.Label ({label : "Hello World"});

    // Set the window title
    mwindow.title = "Hello World!";
    mwindow.connect ("destroy", function(){Gtk.main_quit()});

    // Add the label
    mwindow.add (label);

    // Show the widgets
    label.show ();
    mwindow.show();

    Gtk.main();

Y lo mismo pasa python, con pyGobject, y con muchos otros lenguajes.

Cómo funciona

Así a groso modo, se puede decir que gir genera unos ficheros xml donde está toda la información necesaria para que un binding sepa cómo tiene que hacer las llamadas a una biblioteca en C y cómo tiene que gestionar la memoria de los objetos, conversiones de tipos y esas cosas.

Esos ficheros xml están en tu sistema normalmente en /usr/share/gir-1.0/ con extensión .gir. Esos ficheros, por temas de eficiencia se compilan a un formato binario con extensión typelib y estos son los verdaderos ficheros que utilizan los binding y que se suelen encontrar en /usr/lib/girepository-1.0/.

Por lo tanto, los bindings a diferentes lenguajes son genéricos y lo que hacen, en lugar de tener implementadas las llamadas internamente, como antes tenía pygtk, es mirar los typelib de forma dinámica, por lo que en el momento en el que una biblioteca en C implemente gir, y la instales en tu sistema, automáticamente tus bindings de javascript, python, vala o lo que sea tendrán acceso a esa nueva librería sin necesidad de actualizaciones ni nada. Así pues, el mantenimiento de los bindings se reduce drasticamente y el soporte de librerías aumenta a coste 0, todo ventajas.

Generando gir para mi biblioteca

Gracias a gir se facilita todo, pero cae de parte del desarrollador de la biblioteca la responsabilidad de generar la introspección para que los bindings tengan acceso.

Para añadir gir a libgepub lo que he hecho es seguir este simple manual. Siguiendo el método 2, que aunque es menos portable, es más simple por mi parte.

En el fichero configure.ac añadí esta línea:

GOBJECT_INTROSPECTION_CHECK([1.30.0])

y en el Makefile.am general añadí esta otra:

DISTCHECK_CONFIGURE_FLAGS = --enable-introspection

Hasta aquí todo fácil. Nada complicado ni difícil de hacer. Ahora es cuando viene la parte un poco más complicada, el Makefile.am de libgepub. En principio lo que hices es copiar lo que ponía en el wiki de gir y cambié Foo por Gepub y bueno, metí las dependencias de mi biblioteca y pocos cambios más, bueno sí, el número de versión, algo que me dio algunos problemas.

Una cosa importante a tener en cuenta es que los nombres tienen mucha importancia en todo esto. Gobject-introspection hace muchas cosillas y supone que tu código está escrito de una forma determinada, así que si los nombres de las clases o los métodos no son como tienen que ser, por defecto gir no va a funcionar y tendrás que liarla para darle soporte. En mi caso, por ejemplo, tenía como nombre de clase GEPUBDoc, y para que todo funcionara y me detectara los métodos tuve que renombrarla a GepubDoc. Por lo tanto, el mejor consejo a seguir a la hora de implementar una biblioteca es seguir las guias de estilo de código para que en un futuro todas estas cosas automágicas funcionen.

Luego tan sólo hay que anotar un poco el código para que los bindings se creen correctamente y no haya petes inesperados. La forma de anotar el código es sencilla y tan sólo requiere que se diga quién tiene el control de la memoria de los datos que se devuelven y poco más. Por ejemplo las anotaciones de GepubTextChunk son muy sencillas.

Las anotaciones no son más que comentarios en C con un formato determinado y unas palabras clave. Los métodos que no se anoten también son reconocidos por gir y se generan bindings, pero se suponen cosas que pueden provocar que los bindings que lo utilizan peten estrepitosamente, por lo que es más que recomendable anotar cada método público.

Como he dicho antes, el número de versión también es importante y me dio algunos problemas. Yo tenía como número de versión 0.0.1 y así lo puse en el gir, pero tras hacer el make install y ver que el typelib se generaba bien y se instalaba donde tenía que estar luego no tenía acceso a Gepub desde gjs.

Después de preguntar en el canal irc #introspection de irc.gnome.org me digeron que probara cambiando el número de versión, que lo mismo gir no soportaba tres números de versión. Así lo hice y funcionó toda la magia.

Gjs

gjs> const Gepub = imports.gi.Gepub;
gjs> let doc = Gepub.Doc.new("/tmp/book.epub")
gjs> doc
[object instance proxy GIName:Gepub.Doc jsobj@0x7f615b0255a0 native@0xb20040]
gjs> doc.go_next()
gjs> doc.get_text()
[object instance proxy GIName:Gepub.TextChunk jsobj@0x7f615b025990 native@0xa40870],[object instance proxy GIName:Gepub.TextChunk jsobj@0x7f615b0259d8 native@0xa408a0],[object instance proxy GIName:Gepub.TextChunk jsobj@0x7f615b025a20 native@0xa408d0],[object instance proxy GIName:Gepub.TextChunk jsobj@0x7f615b025a68 native@0xa40900],[object instance proxy GIName:Gepub.TextChunk jsobj@0x7f615b025ab0 native@0xa40930],[object instance proxy GIName:Gepub.TextChunk jsobj@0x7f615b025af8 native@0xa40960],[object instance proxy GIName:Gepub.TextChunk jsobj@0x7f615b025b40 native@0xa40990],[object instance proxy GIName:Gepub.TextChunk jsobj@0x7f615b025b88 native@0xa40a00]
gjs> doc.get_text()[0].text()
"Todo es mío"
gjs> doc.get_text()[0].type_str()
"header"

PyGobject

In [1]: from gi.repository import Gepub
In [2]: doc = Gepub.Doc.new("/tmp/book.epub")
In [3]: doc.go_next()
In [4]: doc.get_text()
Out[4]: 
[<TextChunk object at 0x1e826e0 (GepubTextChunk at 0x1be4550)>,
 <TextChunk object at 0x1e82730 (GepubTextChunk at 0x1be4580)>,
 <TextChunk object at 0x1e82780 (GepubTextChunk at 0x1be6610)>,
 <TextChunk object at 0x1e827d0 (GepubTextChunk at 0x1be6640)>,
 <TextChunk object at 0x1e82820 (GepubTextChunk at 0x1be6670)>,
 <TextChunk object at 0x1e82870 (GepubTextChunk at 0x1be66a0)>,
 <TextChunk object at 0x1e828c0 (GepubTextChunk at 0x1be66d0)>,
 <TextChunk object at 0x1e82910 (GepubTextChunk at 0x1be6700)>]
In [5]: doc.get_text()[0].text()
Out[5]: 'Todo es m\xc3\xado'
In [6]: doc.get_text()[0].type_str()
Out[6]: 'header'

Lo malo de Gobject Introspection

En principio toda esta magia es genial y para el desarrollador facilita muchísimo la tarea, puesto que te da bindings automáticos para multitud de lenguajes a un coste realmente bajo.

Sin embargo, desde el punto de vista del desarrollador, o usuario de estos bindings, toda esta automatización trae dolores de cabeza. Como todo es automático, la documentación es mínima, teniendo que recurrir casi siempre a la documentación de la librería en C cuando estás programando en javascript o en python, por lo que tienes que andar deduciendo cómo se traducen los parámetros a tu binding en particular o incluso tienes que andar yendote al mismo fichero .gir para ver cómo se llaman los métodos y qué tipo de parámetros reciben.

Por supuesto estos problemas son debidos a la poca madurez de la tecnología e igual que se generan los bindings se puede generar documentación específica para cada binding a partir de la introspección. Quizás esto sea un trabajo a hacer en un futuro próximo para cada binding. De momento yo me conformo con que sea usable y sabiendo que conociendo gir y peleándote un poco con el lenguaje se consigue avanzar y desarrollar con el lenguaje que más te guste.

Comments

3
Ángel Guzmán Maeso's picture

Buen trabajo con ePub, aunque lo has explicado bastante, creo que faltaría explicar algunos detalles más profundos en la implementación.

Sobre gir, en mi opinión yo lo veo bastante inmaduro y poco estable, aún a pesar de que hace casi un año que empezara a integrarse. En particular hay muchas bibliotecas usando código de GTK2 y GTK3 a la vez y para hacer una pequeña aplicación o migración se pierde mucho tiempo por la falta de documentación. No en los métodos más usados, sino en renombrado de métodos y constantes poco frecuentes. Por ejemplo usar un simple gtk.STATUS_NORMAL, que pase a ser Gtk.StatusFlag.NORMAL.

Además de grandes regresiones como con Gstreamer. Por mi parte creo que falta que cuando se intente innovar, que se piense mucho en no romper funcionalidades actuales y reducir las posibles regresiones.

Esperemos que vaya mejorando el soporte y escribiéndose más documentación, a veces no queda otra que descargarse el código fuente de aplicaciones populares que utilizan métodos que no están escritos en documentación por ningún sitio (ejem ejem Canonical).

PD: Creo que tienes alguna faltilla como "probocar"

danigm's picture

Pues sí, con el problema de la falta de documentación es con lo que más me he encontrado y por eso he acabado muchas veces mirando directamente el .gir para ver cómo se llama tal o cual método que está documentado en la librería en C.

Acabo de enviar una propuesta de charla para la guadec-es de este año sobre este tema precisamente. Tengo en mente un proyecto de autodocumentación para intentar solventar estos problemas, a ver si durante la guadec me entero mejor de cómo va todo esto y buscamos una solución para mejorar todo lo posible la documentación específica y no tener que andar mirando código fuente o documentación de funciones en C cuando estás escribiendo código en python.

Ángel Guzmán Maeso's picture

Lo más necesario que veo yo ahora mismo, es crear un visor interactivo de .gir. Los .gir en realidad son XML y se podrían leer ubicacione estandar como /usr/share/gir-1.0/ cargando todos los .gir y luego al seleccionarlos, que se pudieran explorar las clases, atributos, objetos, señales, etc. Algo como devhelp pero orientado a .gir.

PD: No funciona la notificación por email de los comentarios. Quizás te haga algún módulo falta o activar alguna opción (yo me he metido hoy de nuevo y he visto que habías respondido).