Position paper: The Python GUI work being done at Veritas.
Abstract
We have developed a C interface to Tk that is unencumbered by Tcl, and
have also written a Python interface to it. This paper talks about how
we arrived at this, looking at some of the alternatives along the way.
History
After the Usenix VHLL conference, we started looking at both Python and
Tk as serious alternatives to more proprietary technologies for building
applications. We also were interested in Tcl, but it
didn't meet our scalability, modularity, and extensibility requirements.
Our target applications are a set of distributed graphical system
administration tools. Veritas sells storage management products into
the Unix market.
We started out with Python's Tkinter module. This seemed initially
workable, but we felt it had a number of flaws:
Lack of documentation
Tcl is embedded
Class structure is oddly organized
Dictionaries as arguments made code ugly, or unnecessarily verbose
For us, the lack of documentation made us look elsewhere. (Yes, we
were aware of the Life Preserver.) We had never used Tk before, so we
backed off and wound up using the online help in extended wish and
toying around to discover how things worked. As a result, the simple
syntax of Tcl/Tk was contrasted against the somewhat stranger Tkinter
syntax.
With a growing understanding of Tcl/Tk and Python, we set out to write
our own Python-Tk interface that matched more closely the functional
simplicity of Tk. We called this module Tkgu. A key goal in
Tkgu was more direct Tcl/Tk access: everything passed in was a string or could
easily be converted by the module to a string then passed on to Tcl.
Python functions or methods would be converted to a unique string and
used as an identifier to Tcl. (Of course, the string is the single
datatype understood by Tcl.) In effect, Tcl and Python could be
intermixed relatively easily.
To give you a flavor of this module, here is the traditional Hello World
example:
import Tkgu
def printit():
print 'Howdy!'
# Create a Tkgu object
top = Tkgu.Tkgu()
# Create a frame widget.
frame1 = top.frame('.', '-relief raised -bd 2')
# Create a button widget inside the frame widget.
# Configure it seperately for sake of example.
button1 = top.button(frame1)
top.tcl(button1, 'configure', '-command', printit, '-text "Hello World"')
# Tell it to resize
top.pack(frame1, button1, '-expand yes -fill both')
top.mainloop()
In this example, frame1 and button1 are string objects, automatically
generated and used as identifiers to Tcl. The functions top.frame() and
top.button() append a unique identifier to the end of their parent's
name (e.g. the first argument, a string), concatenate the remaining
arguments, tack "frame" or "button" on the front, then pass it on to
Tcl. And of course, for function arguments, a Tcl command is created
that will invoke an associated Python function later.
As a refresher for comparison, here is the sample Hello World code from the
Tkinter Life Preserver:
from Tkinter import *
class Test(Frame):
def printit(self):
print "hi"
def createWidgets(self):
self.QUIT = Button(self, {'text': 'QUIT',
'fg': 'red',
'command': self.quit})
self.QUIT.pack({'side': 'left', 'fill': 'both'})
# a hello button
self.hi_there = Button(self, {'text': 'Hello',
'command' : self.printit})
self.hi_there.pack({'side': 'left'})
def __init__(self, master=None):
Frame.__init__(self, master)
Pack.config(self)
self.createWidgets()
test = Test()
test.mainloop()
As you can see the Tkinter code is a little more complex because it
uses Python classes to encapsulate Tk widgets. Tkgu uses a functional
interface. (This is not to say that Tkgu could not be used with
Python classes.) It is debatable which is better, but it is clear that
Tkgu has a more direct interface to Tcl.
The Tkgu programming interface worked fairly effectively. We wrote a
significant application to prove the concept. This application allowed
perusal and manipulation of a computer's disk drives with a Macintosh
like interface. To further prove this concept, we ported the entire
application to Windows NT (It took about a week, thanks to tknt).
Some of the key outcomes of this investigation were:
It was really easy to port existing scripts from Tcl to Python and Tkgu.
Tcl is still embedded.
Tcl was used as little as possible, eventhough intermixing
the two language was really easy. Python proved better
for building abstractions. In fact, the most practical approach was to
only use Tcl when we had to get to Tk. In this light, Tcl was just
extra baggage.
There were some obvious performance problems, including slow
startup time, and some slow interactive behavior.
Tkgu is a simplier than Tkinter, mostly because it uses a lot less code in all aspects: the C module,
the Python module, and application code are shorter.
The callback interface was not completely transparent, it required
the application to clean up. This was typically implemented as
a destruction function in a Python class.
One of the implications of using Tcl is that Python callbacks must be
referenced from some global lookup table. Tkgu hides this problem to a
large degree. Additionally, Tk widget identifiers (strings) are also
mapped into a global table. In terms of scalability, these are
problems.
Alternatives
One of the talks at the VHLL conference was by Malcolm Beattie about
his implementation of Tk for Perl that did not have Tcl bindings.
There was quite alot of interest in Tk bindings for other languages,
particularily for the gnu extension language (Guile) and Python. There are a
number of other languages that encapsulate Tcl/Tk and presumably could
do without Tcl.
At one of the VHLL BOF sessions, the Guile proponents were indicating
that they might implement a Tcl-less Tk interface. At the time the
TkPerl interface was still dependent on Perl. Guile has been recently
released and embeds Tcl/Tk in a similar fashion to Tkinter. It is
unclear what the future direction is.
Malcolm Beattie is no longer working on TkPerl, but Nick Ing-Simmons
has picked up the efforts with his own attempt at a language
independent version with initial bindings for Perl. A version based on
tk4.0b3 has been released. I'm not aware of any work being done to
implement other language bindings with this software.
The Perl Tk is a modified version of Tk, a supplementary set of C
functions to round out the Tk API, and Tcl code translated to Perl.
Architecturally, Perl Tk is exactly the same as Tcl/Tk, with all
the Tcl C library interfaces replaced with similar C interfaces,
and all the Tcl code replaced with Perl code.
There are a few issues with this approach that made it unpalatable to
our thinking: for each new language 4000 or more lines of Tcl code
would have to be ported; strings are still used as identifiers (for
widgets); the changes to Tk were extensive.
In addition, it was relatively difficult to understand what the steps
were to create a new language binding. The source has a certain lack
of organization and documentation. One annoying example is that Perl
bindings are in the same source file as the supplimentary C Tk
interfaces.
Introducing Rivet
So with that background in mind, we set out to implement an interface
to Tk that had the following properties:
Required minimum changes to Tk source code.
Used C pointers as identifiers.
Used no Tcl evaluation, commands, or identifiers.
Implemented a C language interface that could be leveraged for
a language binding (especially Python).
Was implemented only in C.
We were not concerned about the following issues:
Beauty of interface for C programmers. Argc/argv lists may
have to be created by hand, and attention must be paid to
storage management.
String conversions done inside Tk widget methods.
Removing all of vestigial Tcl (Tcl hash routines, for example).
We call this interface Rivet. The bulk of Rivet is a C
implementation of the Tcl code that is typically found in
/usr/local/lib/tcl/tk. The remaining changes are to the Tk source
directly, which is mostly the same as Tk with ifdefs where the Tcl was
assumed. (Currently there are about 200 ifdefs). These are typically
areas where Tcl_Eval is
involved. Additionally, all references to the global Tcl variable
table and the global Tcl command table have been removed.
Constructing most of Rivet took about two months, the Python
interface about two weeks. The Python interface to Rivet
looks somewhat similar to Tkgu, as described above, but of course is
implemented quite differently. Rivet is based on tk4.0b3. One of the
reasons for minimizing the number of changes to Tk is simplify the
upgrade to the final Tk 4.0 release. In fact, the upgrade from tk4.0b2
to tk4.0b3 took an afternoon.
The following is an example of the Hello World example in the Python
binding to Rivet:
import rivet
def printit():
# If this were a bound member function, self would be
# passed in as the first argument.
print "Howdy!"
top = rivet.init()
frame1 = top.frame('-bd', 4, '-relief', 'raised')
button1 = frame1.button()
button1.configure('-text', 'Hello World', '-command', printit)
rivet.pack(frame1, button1, '-expand', 'yes', '-fill', 'both')
rivet.mainloop()
The Rivet C module implements classes for each of the Tk widget classes.
In the above example, frame1 and button1 are Python objects. Each of
them has a number of methods associated with their class that typically map
directly onto the Tk widget method. button1.configure() and
frame1.button() are examples of these methods.
Because the Rivet binding is implemented in C, it is not
possible to subclass the Rivet widget objects with Python.
It would be nice if this restriction were removed from Python.
Behind the Python interface
Rivet C code to implement Hello World looks like the following:
#include "rivet.h"
int
printit(Rivetobj button, ClientData closure)
{
printf("Howdy!");
return TCL_CONTINUE;
}
int
main(argc, argv)
int argc;
char **argv;
{
Rivetobj top;
Rivetobj frame1;
Rivetobj button1;
top = rivet_init(argc, argv);
/* Create a frame widget, passing initialization options
* along in a vararg list.
*/
frame1 = rivet_create(FrameClass, top,
"-relief", "raised",
"-bd", "2",
0);
if (!frame1)
exit(1); /* error message is in top->interp->result. */
/* Create a button widget inside the frame widget. */
button1 = rivet_create(ButtonClass, frame1, 0, 0);
if (!button1)
exit(1);
/* configure button1 seperately for the sake of the example.
* rivet_va_cmd builds an argc/argv list from a null terminated
* argument list of C strings. And passes it to the Tk function
* associated with object (in this case button1).
*/
rivet_va_cmd(button1, "configure", "-text", "Hello World", 0);
/*
* Now register the button's callback function, this is equivalent
* to setting the button's "-command" script. But callbacks can't
* go through the normal Tk argument processing routines because
* they aren't strings.
*/
rivet_button_set_command(button1, printit, 0);
/* Pack the widgets. rivet_va_func builds an argc/argv list from
* it's null terminated arguments, then calls a Tcl command function.
*/
rivet_va_func(top, Tk_PackCmd,
rivet_path(frame1), rivet_path(button1),
"-expand", "yes", "-fill", "both", 0);
Tk_MainLoop();
}
In the C example, you can see the widgets (Rivetobj frame1 and
button1) are created using a class handle (ButtonClass and
FrameClass). This class handle is a Rivet data structure and has all
the necessary function pointers for creating, calling, and destroying a
Rivet object. The Rivet object created by these functions is in fact a
pointer to the C structure created by Tk to represent the widget. By
convention, these various Tk structures all have the same first three
fields in common, and in fact we use this alignment to build the
generic rivet_create() and rivet_va_cmd() functions.
Tk functionality involves more than just widgets, so Rivet
provides additional interfaces. These interfaces are much more adhoc
in nature. Some functions, such as pack, can be handled generically
with the rivet_va_func() function. One function which
required considerable changes is the bind interface, which makes a
large number of assumptions about executing callbacks. In these
cases, replacement functions have been written.
Currently, Rivet is not quite ready for release outside Veritas.
During the next few months we hope to work though some remaining issues
and concentrate on building a serious application with it. At present,
it needs some work to complete the Python interface, documentation
needs to be written, and a number of minor issues with the C interface
(such as handling images and menu entries) need to be cleaned up. At the appropriate
time we hope to release it as freely as possible.
We do not hold much hope in getting John Ousterhout to buy back any
changes that we have made to Tk. (It is a common request on
comp.lang.tcl for a C interface to Tk.) Ousterhout's position is that he
will incorporate volunteered work of this nature, assuming it does not
violate any other principles of Tcl/Tk. It is not clear if our
work fits his requirements.
Comments on this paper can be sent to
Brian Warkentine,
brianw@veritas.com.
Sometimes you're the windshield, and sometimes you're the bug.
--Dire Straits