Wrap GObjects in Python

You don't have to be a C guru to wrap modules for Python

Python is a wonderful language for coding graphical interfaces. Thanks to the speed at which working code can be written and the lack of a time-consuming compile cycle, interfaces can be up and running in minutes and usable not long after that. Couple this with Python's ability to easily link to native libraries, and an excellent environment emerges.

gnome-python is the package that wraps GNOME and its associated libraries for Python. This enables you to write applications in Python that look and feel exactly the same as the core GNOME applications, but in a fraction of the time it would take to write in C.

However, there is a drawback to not coding in C. The majority of GNOME is written in C, and for widgets to be used in Python, they have to be wrapped. This is a quick task for people who know how the wrapping process works, but it is not automatic, and unless widgets are part of the core GNOME libraries, or at least very useful, they will not be wrapped. The C coders may have to write more complicated code, but they do get all of the new toys first!

But it doesn't have to be that way. Although the process of wrapping widgets is traditionally an art known only to a select few, it is not really that hard. If you can wrap new widgets as you find them, you can use them in your Python programs right away.

This article describes how to wrap a C-coded GObject (the ultimate base class of all GTK+ widgets and many related objects), so that it can be used from Python code. gnome-python version 1.99.x is assumed to be installed on your machine (if it is not, please see Related topics for a link). If you are using packages, ensure that the development package is installed. Also, Python 2.2 and its headers must be installed. Knowledge of Make, Python, GTK+ 2, and some C is assumed.

To demonstrate the process, I will wrap EggTrayIcon, a GTK+ widget that abstracts an icon in the notification area. This library is in GNOME CVS, in the libegg module. By the end of this article, we will have a native Python module called trayicon, which contains a TrayIcon object.

To start, take eggtrayicon.c and eggtrayicon.h (links are at the end of this article in the Related topics section) and put them in a new directory. This source expects to be built in an automake environment (but we will not be in one), so either remove #include <config.h> from the files or create an empty file called config.h and then create an empty makefile; as we proceed, we will fill it in.

Creating the interface definition

The first step on the route to wrapping this object is to create trayicon.defs, a file that specifies the API to the object. Definition files are written in a Scheme-like language, and although they are easy to produce for small interfaces, they can be tiresome to write for large interfaces or for beginners.

gnome-python comes with a tool called h2def. This tool will parse a header file and generate a rough definitions file. Note that as it does not actually parse the C code, but simply uses regular expressions, it does expect traditionally formatted GObjects and may not correctly parse strangely formatted C code.

To generate our initial definitions file, we call h2def like so:

python /usr/share/pygtk/2.0/codegen/ eggtrayicon.h > trayicon.defs

Note that you will have to change the path to point to where lives if you do not have it installed under /usr.

If we now look at the generated definitions file, it should make some sense. There is a definition for the class EggTrayIcon, the constructors, then the methods send_message and cancel_message. This file does not have any obvious mistakes, and we do not want to remove any of the methods or fields, so we do not need to edit it. Note that this file is not Python-specific and can be used by other language bindings too.

Generating the wrapper

Now that we have the interface definition, we can generate the bulk of the Python wrapper. This involves generating an override file. Override files tell the code generator what headers to include, what the name of the module will be, and so on.

An override file is separated into multiple sections using %% (in the style of lex/yacc). These sections define what headers to include, the name of the module, what Python modules to include, what functions to ignore, and finally any manual wrapping functions. This is the initial override file for our trayicon module.

Listing 1. trayicon.override
#include <Python.h>               
#include "pygobject.h"
#include "eggtrayicon.h"
modulename trayicon                     
import gtk.Plug as PyGtkPlug_Type       

Let's go over that code again in more detail:

  1. headers
    #include <Python.h>
    #include "pygobject.h"
    #include "eggtrayicon.h"
    These are the header files to include when building the wrapper. Python.h and pygobject.h always need to be included, and as we are wrapping eggtrayicon.h, we need to include that too.
  2. modulename trayicon
    The modulename specification states what module the wrapper will be in.
  3. import gtk.Plug as PyGtkPlug_Type
    These are the Python imports for the wrapper. Note the naming convention; you must adhere to it for the module to compile. It is generally sufficient to import the superclass for your object. For example, if your object directly inherited from GObject, you would use:
    import gobject.GObject as PyGObject_Type
  4. ignore-glob
    This is a glob pattern (a shell-style regular expression) of function names to ignore. Python handles the type code for us, so we ignore the *_get_type functions; otherwise, they would be wrapped.

Now that we have constructed our override file, we can use it to generate the wrapper. The gnome-python bindings provide a magic tool to generate the wrapper, which we can use trivially. Add the following to the makefile:

Listing 2. Initial makefile
DEFS='pkg-config --variable=defsdir pygtk-2.0'         

trayicon.c: trayicon.defs trayicon.override            
    pygtk-codegen-2.0 --prefix trayicon \              
    --register $(DEFS)/gdk-types.defs \                
    --register $(DEFS)/gtk-types.defs \
    --override trayicon.override \                     
    trayicon.defs > $@

Again, in more detail:

  1. DEFS='pkg-config --variable=defsdir pygtk-2.0'
    DEFS is the path containing the Python GTK+ binding definitions files.
  2. trayicon.c: trayicon.defs trayicon.override
    The generated C code depends on the definitions file and the override file.
  3. pygtk-codegen-2.0 --prefix trayicon \
    Here the gnome-python code generator is called. The prefix argument is used as a prefix for the variable names inside the generated code. This can be anything you want, but using the module name keeps symbol names consistent.
  4. --register $(DEFS)/gdk-types.defs \
    --register $(DEFS)/gtk-types.defs \
    Our module uses types from GLib and GTK+, so we must also tell the code generator to load these.
  5. --override trayicon.override \
    This argument passes the override file we created to the code generator.
  6. trayicon.defs > $@
    Here the final option to the code generator is the definitions file itself. The code generator outputs on standard output, so we redirect this into the target, trayicon.c.

If we now run make trayicon.c and look at the generated file, we see the C code wrapping each function in EggTrayIcon. Don't worry about the warning No ArgType for GdkScreen* -- it's normal.

As you can see, the wrapping code can look complicated, so we thank the code generator for every line it writes for us. Later we'll learn how to manually wrap individual methods when we want to fine-tune the wrapping, without having to write all of the wrapper ourselves.

Creating the module

Now that the bulk of the wrapper has been created, we need a way to start it. This involves creating trayiconmodule.c, which can be considered the main() function for Python modules. This file is boilerplate code (similar to the override file), which we slightly modify. This is the trayiconmodule.c we'll be using:

Listing 3. TrayIcon module code
#include <pygobject.h>
void trayicon_register_classes (PyObject *d); 
extern PyMethodDef trayicon_functions[];
    PyObject *m, *d;
    init_pygobject ();
    m = Py_InitModule ("trayicon", trayicon_functions);
    d = PyModule_GetDict (m);
    trayicon_register_classes (d);
    if (PyErr_Occurred ()) {
        Py_FatalError ("can't initialise module trayicon");

A few subtleties need to be explained here, as there are multiple sources for the word trayicon. The name of the function inittrayicon and the name the module is initialized as are the real name of the Python module, and thus the name of the final shared object. The array trayicon_functions and the function trayicon_register_classes are named after the --prefix argument to the code generator. As noted, it's best to keep these the same so that coding this file doesn't become very confusing.

Despite the potentially confusing source of names, this C code is very straightforward. It initializes GObject and the trayicon module, and then registers the classes with Python.

Now we have all the pieces, we can generate a shared object. Add this to the makefile:

Listing 4. Makefile additions
CFLAGS = 'pkg-config --cflags gtk+-2.0 pygtk-2.0' -I/usr/include/python2.2/ -I.    
LDFLAGS = 'pkg-config --libs gtk+-2.0 pygtk-2.0'                             trayicon.o eggtrayicon.o trayiconmodule.o                             
    $(CC) $(LDFLAGS) -shared $^ -o $@

Once again, let's go through this line by line:

  1. CFLAGS = 'pkg-config --cflags gtk+-2.0 pygtk-2.0' -I/usr/include/python2.2/ -I.
    This line defines the C compilation flags. We use pkg-config to get the include paths for GTK+ and PyGTK.
  2. LDFLAGS = 'pkg-config --libs gtk+-2.0 pygtk-2.0'
    This line defines the linker flags. Again using pkg-config to get the correct library paths.
  3. trayicon.o eggtrayicon.o trayiconmodule.o
    The shared object is constructed from the generated code, the module code we just wrote, and the implementation of EggTrayIcon. The implicit rules build .o files from the .c files we created.
  4. $(CC) $(LDFLAGS) -shared $^ -o $@
    Here we build the final shared library.

Now running make should generate the C code from the definitions, compile the three C files, and finally link it all together. Well done -- we have built our first native Python module. If it didn't compile and link, double-check the stages and ensure that there were no early warnings causing errors later on.

Now that we have, we can try and use it in a Python program. A good start is to load it, and then list its members. Run python in a shell to get to the interactive interpreter, and enter the commands below.

Listing 5. Interactive test of TrayIcon
$ python
Python 2.2.2 (#1, Jan 18 2003, 10:18:59)
[GCC 3.2.2 20030109 (Debian prerelease)] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import pygtk
>>> pygtk.require("2.0")
>>> import trayicon
>>> dir (trayicon)
['TrayIcon', '__doc__', '__file__', '__name__']

Hopefully the result from dir was the same for you as it was here. Now we are ready for a bigger example!

Listing 6. Hello example
#! /usr/bin/python
import pygtk
import gtk
import trayicon                               
t = trayicon.TrayIcon("MyFirstTrayIcon")      

Breaking it down line by line:

  1. #! /usr/bin/python
    import pygtk
    import gtk
    import trayicon
    Here we first require and import the GTK+ bindings, and then import our new module.
  2. t = trayicon.TrayIcon("MyFirstTrayIcon")
    Now create an instance of trayicon.TrayIcon. Note that the constructor takes a string argument, the name of the icon.
  3. t.add(gtk.Label("Hello"))
    TrayIcon elements are GTK+ containers, so you can add anything into them. Here I add a label widget.
  4. t.show_all()
    Here I set the widgets to be visible, and start the GTK+ main event loop.

Now, if you have not done so already, add the Notification Area applet to your GNOME Panel (right-click on the panel, and then "Add to Panel" -> Utility -> Notification Area). Running the test program should result in "Hello" being displayed in the tray. Cool, huh?

Figure 1. Hello example
Hello example

What else does the notification area allow us to do? Well, programs can tell the notification area to display a message. How this message is actually displayed is implementation specific; at present the GNOME notification area displays a tooltip. We can send a message by calling the send_message() function. A quick look at the API tells us that it expects a timeout and the message, so this should work:

t = trayicon.TrayIcon("test")
t.send_message(1000, "My First Message")

But no. The C prototype is send_message(int timeout, char* message, int length), so the Python API also expects a character pointer and a length. This does work:

t = trayicon.TrayIcon("test")
message = "My First Message"
t.send_message(1000, message, len(message))

That, however, is a little ugly. This is Python; programming is meant to be simple. If we carry on down this route, we'll end up with C, but without semicolons. Luckily, we can manually wrap individual methods when using the gnome-python code generator.

Fine-tuning the interface

What we have so far is the send_message(int timeout, char *message, int length) function. It would be better if the Python API to EggTrayIcon allowed us to call send_message(timeout, message). Luckily, this is not too hard.

Doing this involves editing trayicon.override again. This is where the name for the file makes sense: this file mainly contains manually overridden wrapper functions. Explaining how these work is a lot harder than just showing an example and stepping through that, so here is the manually wrapped send_message code.

Listing 7. Manual override
override egg_tray_icon_send_message kwargs 
static PyObject*
_wrap_egg_tray_icon_send_message(PyGObject *self,
                                 PyObject *args, PyObject *kwargs) 
    static char *kwlist[] = {"timeout", "message", NULL}; 
    int timeout, len, ret;
    char *message;

    if (!PyArg_ParseTupleAndKeywords(args, kwargs,    
                                     "is#:TrayIcon.send_message", kwlist,
                                     &timeout, &message, &len))
        return NULL;
    ret = egg_tray_icon_send_message(EGG_TRAY_ICON(self->obj),
                                     timeout, message, len);
    return PyInt_FromLong(ret); 

For clarity, we'll break that down line by line once again:

  1. override egg_tray_icon_send_message kwargs
    This line tells the code generator that we will be providing a manual definition of egg_tray_icon_send_message, and it should not generate one itself.
  2. static PyObject*
    _wrap_egg_tray_icon_send_message(PyGObject *self,
    PyObject *args, PyObject *kwargs)
    This is the prototype for the Python-to-C bridge. It is composed of a pointer to the GObject that the method is being called on, an array of arguments, and an array of keyword arguments. The return value is always a PyObject*, as all values in Python are objects (even integers).
  3. {
    static char *kwlist[] = {"timeout", "message", NULL};
    int timeout, len, ret;
    char *message;
    This array defines the names for the keyword argument this function understands. Providing the ability to use a keyword argument is not essential, but it can make code with lots of arguments a lot clearer, and is not much extra work.
  4. if (!PyArg_ParseTupleAndKeywords(args, kwargs,
    "is#:TrayIcon.send_message", kwlist,
    &timeout, &message, &len))
    return NULL;
    This hairy function call does the argument parsing. We give it the list of keyword arguments we understand and all of the arguments we were given, and it will set the values pointed to by the final arguments. The cryptic-looking string declared the types of variables expected, and will be explained later.
  5. ret = egg_tray_icon_send_message(EGG_TRAY_ICON(self->obj),
    timeout, message, len);
    return PyInt_FromLong(ret);
    Here we actually call egg_tray_icon_send_message and convert the returned int into a PyObject.

This looks a little scary at first, but initially it was copied from the generated code in trayicon.c. In most situations, this is perfectly possible if all you want to do is fine-tune the expected parameters. Simply copy and paste the relevant function from the generated C, add the magic override line, and edit the code until it does what you want.

The most important change is to modify the expected parameters. The obscure-looking string in the PyArg_ParseTupleAndKeywords function defines the expected arguments. Originally it was isi:TrayIcon.send_message; this means that the parameters are an int, a char* (s for string), and an int; and that if an exception is thrown, the function is called TrayIcon.send_message. We don't want to have to specify the length of the string in Python code, so we change the isi to is#. Using s# instead of s means that PyArg_ParseTupleAndKeywords will automatically compute the length of the string and set another variable for us -- just what we wanted.

To use the new wrapper, simply rebuild the shared object and change the send_message call in the test program to:

t.send_message(1000, message)

If everything has gone to plan, this modified example should have the same behavior, but with clearer code.

End game

We have taken a small but useful C GObject, wrapped it so that it can be used in Python, and even tailored the wrapper to suit our needs. The techniques here can be applied many times to different objects, allowing you to use any GObject you find in Python.

Downloadable resources

Related topics

  • Download a tarball of the sources discussed in this article, including eggtrayicon.c and working examples.
  • Download gnome-python version 1.99.x at the gnome-python home page.
  • Read previous developerWorks introduction to GObject.
  • The GTK+ home page has links to documentation, downloads, and code samples.
  • Download Python 2.2 from the Python home page. This site also has plenty of documentation on Python.
  • You can download all of the GNOME programs mentioned in this article from the GNOME FTP server. The server provides a list of mirror sites as well.
  • "The Camel and the Snake" looks at open source development with Perl, Python, and DB2.
ArticleTitle=Wrap GObjects in Python