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/h2def.py eggtrayicon.h > trayicon.defs
Note that you will have to change the path to point to where h2def.py 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
%% headers #include <Python.h> #include "pygobject.h" #include "eggtrayicon.h" %% modulename trayicon %% import gtk.Plug as PyGtkPlug_Type %% ignore-glob *_get_type %%
Let's go over that code again in more detail:
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.modulename trayicon
Themodulenamespecification states what module the wrapper will be in.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_Typeignore-glob*_get_type
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_typefunctions; 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:
DEFS='pkg-config --variable=defsdir pygtk-2.0'DEFSis the path containing the Python GTK+ binding definitions files.trayicon.c: trayicon.defs trayicon.override
The generated C code depends on the definitions file and the override file.pygtk-codegen-2.0 --prefix trayicon \
Here thegnome-pythoncode generator is called. Theprefixargument 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.--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.--override trayicon.override \
This argument passes the override file we created to the code generator.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[];
DL_EXPORT(void)
inittrayicon(void)
{
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.so: trayicon.o eggtrayicon.o trayiconmodule.o
$(CC) $(LDFLAGS) -shared $^ -o $@Once again, let's go through this line by line:
CFLAGS = 'pkg-config --cflags gtk+-2.0 pygtk-2.0' -I/usr/include/python2.2/ -I.
This line defines the C compilation flags. We usepkg-configto get the include paths for GTK+ and PyGTK.LDFLAGS = 'pkg-config --libs gtk+-2.0 pygtk-2.0'
This line defines the linker flags. Again usingpkg-configto get the correct library paths.trayicon.so: trayicon.o eggtrayicon.o trayiconmodule.o
The shared object is constructed from the generated code, the module code we just wrote, and the implementation ofEggTrayIcon. The implicit rules build .o files from the .c files we created.$(CC) $(LDFLAGS) -shared $^ -o $@
Here we build the final shared library.
Now running make trayicon.so 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 trayicon.so, 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
pygtk.require("2.0")
import gtk
import trayicon
t = trayicon.TrayIcon("MyFirstTrayIcon")
t.add(gtk.Label("Hello"))
t.show_all()
gtk.main()Breaking it down line by line:
#! /usr/bin/pythonimport pygtkpygtk.require("2.0")import gtkimport trayicon
Here we first require and import the GTK+ bindings, and then import our new module.t = trayicon.TrayIcon("MyFirstTrayIcon")
Now create an instance of trayicon.TrayIcon. Note that the constructor takes a string argument, the name of the icon.t.add(gtk.Label("Hello"))
TrayIcon elements are GTK+ containers, so you can add anything into them. Here I add a label widget.t.show_all()gtk.main()
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
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:
override egg_tray_icon_send_message kwargs
This line tells the code generator that we will be providing a manual definition ofegg_tray_icon_send_message, and it should not generate one itself.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 aPyObject*, as all values in Python are objects (even integers).{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.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.ret = egg_tray_icon_send_message(EGG_TRAY_ICON(self->obj),timeout, message, len);return PyInt_FromLong(ret);}
Here we actually callegg_tray_icon_send_messageand convert the returnedintinto aPyObject.
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
- PDF of this content
- Sample code (l-wrap.zip | 7KB)
Related topics
- Download a tarball of the sources discussed in this article, including eggtrayicon.c and working examples.
- Download
gnome-pythonversion 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.