SoFunction
Updated on 2024-10-29

Tutorial on wrapping GObject module for graphical programming in Python

Python is an excellent language for coding graphical interfaces. Because working code can be written quickly and does not require time-consuming compilation cycles, interfaces can be up and running immediately and can be used in a short time. Combine this with Python's ability to easily link native libraries, and you have an excellent environment.

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

However, there is a disadvantage to programming without C. Most of GNOME is written in C. Most of GNOME is written in C, and for widgets to be used in Python, they must be wrapped. This is a quick task for someone who knows how the wrapping process works, but it's not automatic, and widgets won't be wrapped unless they are part of the core GNOME libraries or are at least very useful. C programmers may have to write more complex code, but they do take this step first!

But it doesn't have to be that way! Although the technique of wrapping widget processes is traditionally known to very few people, it's not really that hard. If you can wrap new widgets as you find them, you can use them in your Python program right away.

This article describes how to encapsulate a C-coded GObject (the final base class for all GTK+ widgets and many related objects) so that it can be used from Python code. It is assumed that gnome-python V1. is installed on your machine (if not, see the references for links). If you are using a package, make sure that the development package is installed. You must also install Python 2.2 and its headers. It is assumed that you have knowledge of Make, Python, GTK+ 2, and some C knowledge.

To demonstrate the process, I'm going to package EggTrayIcon, which is a GTK+ window widget used to abstractly represent icons in the notification area. The library is in GNOME CVS in the libegg module. At the end of this article, we'll have a native Python module called trayicon that contains a TrayIcon object.

To start, get and (linked in the references section at the end of this article) and put them in a new directory. The source files should be built in the automake environment (but we won't be in that environment), so either remove the #include <> from those files, or create an empty file called, and then create an empty makefile; next, we'll populate it.
Creating Interface Definitions

The first step in the object encapsulation process is to create the file that specifies the API for the object. Definition files are written in a Scheme-like language, and while they are easy to generate for small interfaces, they can be overwhelming for large interfaces or for beginners.

gnome-python is provided with a tool called h2def. This tool will parse header files and generate rough definition files. Note: Since it does not actually parse C code, but only uses regular expressions, it does require a conventionally formatted GObject and will not correctly parse oddly formatted C code.

To generate the initial definition file, we call h2def as follows : python /usr/share/pygtk/2.0/codegen/ >

Note: If you did not install under /usr, you must change the path to point to where it is.

If we now look at the generated definition file, it should make some sense. The file contains the definition of the class EggTrayIcon, the constructor, and the methods send_message and cancel_message. The file doesn't have any obvious errors, and we don't want to remove any methods or fields, so we don't need to edit it. Note: This file is not specific to Python; it can be used by other language bindings.

Generate wrappers

Now that we have the interface definition, we can generate the code block for the Python wrapper. This involves generating an override file. The override file tells the code generator what header files to include, what the module names will be, and so on.

The override file is divided into sections (in the lex/yacc style) by using %%. These sections define which header files to include, the module names, which Python modules to include, which functions to ignore, and finally all the hand-wrapped functions. Here is the initial override file for the trayicon module.
List 1.

%%
headers
#include <>        
#include ""
#include ""
%%
modulename trayicon           
%%
import  as PyGtkPlug_Type    
%%
ignore-glob
 *_get_type              
%%

Let's examine the code again in more detail:

  headers
  #include <>
  #include ""
  #include ""

These are the header files to include when building a wrapper. It is always necessary to include and , and when we wrap, we must include them as well.
    

modulename trayicon

The modulename specification declares what module the wrapper will be in.
    

import  as PyGtkPlug_Type

These are Python imports for wrappers. Note the naming convention; it must be followed for modules to be compiled. Usually, importing the object's superclass is sufficient. For example, if the object is inherited directly from GObject, use:

  import  as PyGObject_Type
  ignore-glob
  *_get_type

This is a glob pattern (shell-style regular expression) of function names to be ignored.Python handles the type code for us, so we ignore the *_get_type functions; otherwise, they are wrapped.

Now that we've constructed the override file, we can use it to generate wrappers. The gnome-python binding provides a fantastic tool for generating wrappers, and we can use it however we like. Add the following to the makefile:
Listing 2. Initial makefile

Again, in detail:

 

  DEFS='pkg-config --variable=defsdir pygtk-2.0'

DEFS is the path to the file containing the Python GTK+ binding definitions.

  :  

The generated C code depends on the definition file and the override file.
   

 pygtk-codegen-2.0 --prefix trayicon \

The gnome-python code generator is called here. The prefix parameter is used as a prefix for variable names inside the generated code. You can name the parameter however you like, but using the module name keeps the symbolic names consistent.

  --register $(DEFS)/ \
  --register $(DEFS)/ \

Modules use types from GLib and GTK+, so we must also tell the code generator to load those types.

  --override  \

This parameter passes the overlay file we created to the code generator.

   > $@

Here, the last option for the code generator is to define the file itself. The code generator outputs on standard output, so we redirect it to the target .

If we run make now and look at the generated file, we will see C code wrapping every function in EggTrayIcon. Don't worry about the warning No ArgType for GdkScreen* - this is normal.

As you can see, the wrapper code looks complex, so we appreciate every line that the code generator writes for us. Later, we'll learn how to manually wrap individual methods when we want to tune the wrapper, without having to write all the wrappers ourselves.

Creating Modules

Now that the wrapper's code block has been created, you need a way to start it. This involves creating , a file that can be thought of as the Python module's main() function. This file is sample file code (similar to the overlay file), which we modify slightly. Here's what we'll use :
Listing 3. TrayIcon Module Code

#include <>
 
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");
  }
}

There are a few subtle differences that need to be clarified here, because there is more than one source code that uses the word trayicon. The name of the function inittrayicon and the name of the initialization module are the real names of the Python module, and therefore the name of the final shared object. The array trayicon_functions and the function trayicon_register_classes are named according to the --prefix argument of the code generator. As mentioned earlier, it's best to keep these names consistent so that coding the file doesn't become confusing.

Despite the potential for name source confusion, the C code is very simple. It initializes the GObject and trayicon modules and then registers those classes with Python.

Now that we have all the code blocks, we can generate the shared objects. Add the following to the makefile:
Listing 4. makefile additional code section

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'                  
 
:                  
  $(CC) $(LDFLAGS) -shared $^ -o $@

Let's scrutinize it again 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 use pkg-config to get the include path for GTK+ and PyGTK.
  

 LDFLAGS = 'pkg-config --libs gtk+-2.0 pygtk-2.0'

This line defines the linker flags. Use pkg-config again to get the correct library path.
    

:   

The shared object is constructed from the generated code, the module code we just wrote, and the EggTrayIcon implementation. The implicit rules construct the .o file from the .c file we created.

  $(CC) $(LDFLAGS) -shared $^ -o $@

Here we build the final shared library.

Running make now should generate C code by definition, compile the three C files, and finally link them all together. Nice work - we've built our first native Python module. If it doesn't compile and link, double-check these stages and make sure you didn't get any warnings earlier that would cause errors later.

Now that we have , we can try it out and use it in a Python program. It's a good idea to start by loading it and then listing its members. Run python in a shell to open the interactive interpreter, and then enter the following command.
Listing 5. Interactive test for 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
>>> ("2.0")
>>> import trayicon
>>> dir (trayicon)
['TrayIcon', '__doc__', '__file__', '__name__']

Hopefully, the results from dir will be the same as here. Now we're ready to start a larger example!
Listing 6. Hello example

#! /usr/bin/python
import pygtk
("2.0")
import gtk
import trayicon                
t = ("MyFirstTrayIcon")   
(("Hello"))           
t.show_all()
()

Refine it line by line:

  #! /usr/bin/python
  import pygtk
  ("2.0")
  import gtk
  import trayicon

Here, we first request and import the GTK+ bindings and then import the new module.

  t = ("MyFirstTrayIcon")

Now create the instance of the icon. Note: The constructor takes a string parameter - the icon name.

  (("Hello"))

The TrayIcon element is a GTK+ container, so you can add anything to it. Here, I'm adding a tabbed window widget.

  t.show_all()
  ()

Here, I set the window widget to be visible and then start the GTK+ main event loop.

Now, if you haven't already done so, add the Notification Area applet to the GNOME panel (right-click on the panel and select "Add to Panel" -> Utility -> Notification Area). Area). Running the test program should display "Hello" in the bar. Pretty cool, isn't it?

 (258×24)

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

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

But it doesn't work that way. the C prototype is send_message(int timeout, char* message, int length), so the Python API also needs the character pointer and length. This does work:

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

However, it's kind of hard to see. This is Python; programming should be simple. If we stick to this route, it will end in C, but without the semicolon. Fortunately, when using the gnome-python code generator, it is possible to hand-wrap individual methods.

Tuning Interface

So far we have the send_message(int timeout, char *message, int length) function. It would be nice if EggTrayIcon's Python API allowed us to call send_message(timeout, message). Fortunately, this is not too difficult.

Completing this step will again involve editing . That's what the name of the file is all about: the file contains mostly hand-covered wrapper functions. It's much harder to explain how these functions work than it is to show an example and explain their contents step-by-step, so here is the hand-wrapped send_message code.
List 7.

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); 
}

Once again, for the sake of clarity, we have broken down the list line by line:

  override egg_tray_icon_send_message kwargs

This line tells the code generator that we will provide a manual definition of egg_tray_icon_send_message, which should not generate a definition itself.

  static PyObject*
  _wrap_egg_tray_icon_send_message(PyGObject *self,
  PyObject *args, PyObject *kwargs)

This is the prototype Python-to-C bridge. It consists of a pointer to the GObject on which the method is being called, an array of arguments, and an array of keyword arguments. The return value is always PyObject*, because 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 of the keyword arguments that the function accepts. Providing the ability to use keyword arguments is not required, but it can make code with many arguments much clearer and does not require a lot of extra work.

  if (!PyArg_ParseTupleAndKeywords(args, kwargs,
  "is#:TrayIcon.send_message", kwlist,
  &timeout, &message, &len))
  return NULL;

This complex function call performs parameter parsing. We provide it with the keyword arguments we know and a list of all the given arguments, and it sets the value to which the final argument refers. The convoluted-looking string declares the type of variable needed, which we'll explain later.

  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, which then converts the returned int to a PyObject.

This may seem a bit scary at first, but it was originally copied from the generated code in the In most cases, this is perfectly possible if you only want to tune the parameters you need. Just 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 required parameters. The PyArg_ParseTupleAndKeywords function's convoluted-looking string defines the required arguments. Originally, it was isi:TrayIcon.send_message; this means that the arguments are int, char* (s for string), and int, in that order; and if an exception is thrown, the function is called TrayIcon.send_message. We don't want to have to specify string lengths in Python code, so we've changed isi to is#. Using s# instead of s means that PyArg_ParseTupleAndKeywords will automatically calculate the string length and set another variable for us - which is exactly what we want.

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 works as usual, then this modified example should have the same behavior, but with cleaner code.

endgame

We've taken the small but useful C GObject, wrapped it so that we can use it in Python, and even customized the wrapper to fit our needs. The techniques here can be applied multiple times to different objects, allowing you to use any GObject found in Python.