GIR support in gnome-class

Recently I've been working again in the rust port of libgepub, libgepub is C code, but in the rust-migration branch almost all the real functionality is done with rust and the GepubDoc class is a GObject wrapper around that code.

For this reason I was thinking about to use gnome-class to implement GepubDoc.

Gnome-class is a rust lib to write GObject code in rust that's compatible with the C binary API so then you can call this new GObject code written with gnome-class from C. I've worked a little in gnome-class, implementing a basic properties support.

GObject Introspection

A great advantage of write GObject code in C is that you can have automatic bindings using GObject Introspection. This is great because you only need to write the C code, write correctly the documentation of the public API and then you'll get GIR for free, and that means that you can use your lib from gjs, python and other automatic bindings.

GIR defines a simple XML document to declare the public API and there is the meta information needed for bindings, to know how to call the lib functions and how to convert params and also provides memory management information, for languages with garbage collector and so. These files are places under /usr/share/gir-1.0/ and it looks like this:

      <method name="get_cover" c:identifier="gepub_doc_get_cover">
        <return-value transfer-ownership="full">
          <doc xml:space="preserve">cover file path to retrieve with
gepub_doc_get_resource</doc>
          <type name="utf8" c:type="gchar*"/>
        </return-value>
        <parameters>
          <instance-parameter name="doc" transfer-ownership="none">
            <doc xml:space="preserve">a #GepubDoc</doc>
            <type name="Doc" c:type="GepubDoc*"/>
          </instance-parameter>
        </parameters>
      </method>

This xml is compiled to a binary format to be used from bindings and those compiled gir files are places under /usr/lib64/girepository-1.0/ with the .typelib extension.

This is great, and the GepubDoc class is exported as GIR and indeed it's used from javascript code in the gnome-books application using this. If we want to migrate libgepub from C to rust, we need to provide this GIR to don't break the compatibility in other apps. That's done currently in the rust-migration branch by hand, creating the GepubDoc in C and calling the rust code from there.

With gnome-class it should be possible to write that GepubDoc code in rust and have the same ABI, but currently gnome-class doesn't generate GIR, that's a little problem that I can try to solve using my dev skills.

Adding GIR to gnome-class

So, libgepub is the excuse to start to implement GIR in gnome-class. There's an issue in the gitlab with an initial syntax proposal and I started from that:

#[generate_gir("foo.gir")]
#[generate_c_header("foo.h")]
gobject_gen! {
    "foo.gir",
    "foo.h",

    class Foo {
        ...
    }

    impl Foo {
        ...
    }
}

The first thing to do is to add a way to tell that we want to generate the GIR file in the class definition. In this proposal, the GIR tag is before the gobject_gen proc-macro, that can't be done, or I don't know how to do that easily, so I changed to a declaration inside the proc-macro, that's what we're parsing in gnome-class:

gobject_gen! {
    #[generate_gir("Counter.gir")]
    class Counter {
      f: Cell<u32>,
    }

    impl Counter {
        pub fn add(&self, x: u32) -> u32 {
            self.get_priv().f.set(self.get() + x);
            self.get()
        }

        pub fn get(&self) -> u32 {
            self.get_priv().f.get()
        }
    }
}

With the code insithe de gobject_gen I only need to update the parser to allow this new syntax:

named!{parse_gir -> Option<LitStr>,
    option!(do_parse!(
        punct!(#)                               >>
        name: brackets!(do_parse!(
            call!(keyword("generate_gir"))      >>
            name: parens!(syn!(LitStr))         >>
            (name.1)
        )) >>
        (name.1)
    ))
}

impl Synom for ast::Class {
    named!(parse -> Self, do_parse!(
        gir: call!(parse_gir)                                    >>
        call!(keyword("class"))                                  >>
        name: syn!(Ident)                                        >>
        extends: option!(do_parse!(
            punct!(:)                                            >>
            superclass: syn!(Path)                               >>
            // FIXME: interfaces
            (superclass)))                                       >>
        fields: syn!(FieldsNamed)                                >>
        (ast::Class {
            gir,
            name,
            extends,
            fields,
        })
    ));

Then, I'm storing the GIR file name in the ast class so I can check if that field is None or Some and generate the GIR in that case.

Generating the XML

The parser was the easier part, we've now the hability to define if we want to generate the GIR file and we're storing that information in the High-level Internal Representation (hir).

To generate the XML I've to add a new call before generating the code:

     let result: Result<proc_macro2::TokenStream> =
-        hir::Program::from_ast_program(&ast_program).and_then(|program| Ok(gen::codegen(&program)));
+        hir::Program::from_ast_program(&ast_program)
+            .and_then(|program| {
+                gen::gir::generate(&program)?;
+                Ok(gen::codegen(&program))
+            });

     match result {
         Ok(tokens) => {

The gir module generates the XML, iterating over all classes defined in the program.

The XML isn't really hard to generate, we've almost all the information so we can iterate over all the class methods and add to the XML. There's some information that's not really easy to get so I've let some TODO in this initial version, like:

  • Find the parent class name (GObject.Object by default)
  • Get the library version and shared-lib path (lib#CLASSNAME#1.0.0 by default)
  • Get dependencies

So this is not a full featured GIR support, but it's an initial step to get it, and it works, at least with simple examples :D

Example project

To test that this is working correctly I've created a simple example project, that uses gnome-class, generates the gir and has an small script to call the generated code from python.

This is the rust code:

gobject_gen! {
    #[generate_gir("Counter.gir")]
    class Counter {
      f: Cell<u32>,
    }

    impl Counter {
        pub fn add(&self, x: u32) -> u32 {
            self.get_priv().f.set(self.get() + x);
            self.get()
        }

        pub fn get(&self) -> u32 {
            self.get_priv().f.get()
        }
    }
}

I've a bash script to compile and generate the typelib file from the xml, generated by gnome-class:

cargo +nightly build
g-ir-compiler Counter.gir > Counter-1.0.typelib

And I've written a test in python using gi.repository:

import unittest
from gi.repository import Counter

class TestCounter(unittest.TestCase):

    def test_new(self):
        c = Counter.Counter()
        self.assertEqual(c.get(), 0)

    def test_add(self):
        c = Counter.Counter()
        self.assertEqual(c.add(1), 1)
        self.assertEqual(c.add(3), 4)

    def test_get(self):
        c = Counter.Counter()
        self.assertEqual(c.get(), 0)
        self.assertEqual(c.add(1), 1)
        self.assertEqual(c.get(), 1)

if __name__ == '__main__':
    unittest.main()

This is working right now, so we're getting closer to the final goal of having libgepub written with rust code and have GIR generation.

Comments !