An Introduction To Embedded Tk

Table Of Contents

List Of Figures

1 Embedded Tk

If you've ever tried to build a large-scale, compute-intensive, or commercial application using Tcl/Tk, you probably had a difficult time of it. A pure Tcl/Tk script is terrific for writing small programs or for prototyping, but it is often inadequate for really big problems. This is due to several factors:

The usual way to avoid these troubles is to code in C or C++ rather than Tcl/Tk. C is fast and well-structured. Compiled C code is difficult for users to read. And statically-linked C programs will run on any binary-compatible computer, independent of other software.

But programming a graphical user interface in pure C is time-consuming and error-prone. The job can be made somewhat easier by using Tcl/Tk's C interface, and having your C program call the Tcl/Tk library routines directly. Many people have done this, some successfully. The task is still tough, though, because unlike its scripting language, Tcl/Tk's C interface is not easy to use. Properly initializing the Tcl/Tk interpreter takes skill and finesse, and calling the interpreter from C is a dull chore.

And so the problem remains: Do you go for the speed and structure of C or the power and simplicity of Tcl/Tk?

The Embedded Tk system (hereafter ``ET'') was created to resolve this conundrum. ET is a simple preprocessor and small interface library that make it easy to mix Tcl/Tk and C in the same program. With ET, you can put a few commands of Tcl/Tk code in the middle of a C routine. ET also makes it very easy to write C functions that work as new Tcl/Tk commands -- effectively allowing you to put pieces of C code in the middle of your Tcl script. These features gives you the speed and structure of C with the power and simplicity of Tcl/Tk. As an added benefit, an application written using ET will compile into a stand-alone executable that will run on any binary-compatible computer, even if the other computer doesn't have Tcl/Tk installed.

2 An Example: ``Hello, World!''

The ET system is designed to be easy to use. To see this, let's look at the classic ``Hello, World!'' program, coded using ET.

    void main(int argc, char **argv){
      Et_Init(&argc,argv);
      ET( button .b -text {Hello, World!} -command exit; pack .b );
      Et_MainLoop();
    }
If you compile and link these 5 lines, you'll get a stand-alone executable that pops up a ``Hello, World!'' button, and goes away when the button is clicked.

Let's take this program apart to see how it works. The first thing it does is call the Et_Init() procedure. This procedure performs the tedious and confusing work needed to start up the Tcl/Tk interpreter, initialize widget bindings, create the main window ``.'', and so forth. The last line is a call to another procedure Et_MainLoop() that implements the event loop. (If you don't know what an event loop is, don't worry. We'll have more to say about event loops in section 4.) The most interesting part of the example is the middle line, the one that looks like a call to a function named ET(). The ET() function is special. It looks and is used like a regular C function, but takes a Tcl/Tk script as its argument instead of a C expression. Its function is to execute the enclosed Tcl/Tk. In this particular example, the ET() function creates the ``Hello, World!'' button.

Because of the ET() function, we can't give the ``Hello, World!'' source code directly to a C compiler and expect it to work. We have to run it through a preprocessor first. Like this:

    et2c hello.c > hello_.c
The et2c preprocessor converts the ET() function into real, compilable C code. The preprocessor also takes care of some other housekeeping details, like adding prototypes to the top of the file so that we don't have to bother with a #include. After it has been preprocessed, the source code can be compiled like any other C program.
    cc -O -o hello hello_.c et.o -ltk -ltcl -lXll -lm
Notice that you must link the program with ET's et.o library file, and with libraries for Tcl/Tk and X11. (See section 13 for instructions on building applications for Microsoft-Windows or Macintosh.)

And that's all there is too it!

3 How To Avoid Reading The Rest Of This Article

If you're restless to start programming and are the type of person who prefers to learn at the keyboard rather than from a book, this section is for you. It contains a terse overview of the features of ET. Peruse this section, glance quickly at the examples, and you'll be ready to start coding. You can use the rest of the article as a reference guide when you run into trouble.

On the other hand, if you are new to graphical interface programming, are a little unsteady with C, or just have a more deliberate and cautious attitude toward life, then you may prefer to lightly skim or even skip this section and focus instead on the tutorial-like text that follows.

The ET system consists of two things: the et2c preprocessor and the et.o library. The preprocessor takes care of translating ET source code (which looks a whole lot like C) into genuine C code that your compiler will understand. The et.o library contains a few support routines.

Among the support routines in et.o are Et_Init() and Et_MainLoop() for initializing the ET package and implement the event loop, respectively. A third routine, Et_ReadStdin(), allows standard input to be read and interpreted by the Tcl/Tk interpreter at run-time. The et.o library defines three global C variables as a convenience. Et_Interp is a pointer to the Tcl/Tk interpreter used by ET. Et_MainWindow is the main window. Et_Display is the Display pointer required as the first argument to many Xlib routines. ET also provides two global Tcl variables, cmd_name and cmd_dir. These contain the name of the executable and the directory where the executable is found.

The et2c preprocessor is used to convert an ET source file into real C code. It creates the illusion of giving the C language some new statements, like ET_INSTALL_COMMANDS and ET_PROC and some special new functions like ET().

The ET() function is used as if it were a regular C function, except that its argument is a Tcl/Tk script. The job of the ET() is to execute the script. ET() returns either ET_OK or ET_ERROR depending upon whether the script suceeded or failed. Similar routines ET_STR(), ET_INT(), and ET_DBL() also take a Tcl/Tk script as their argument, but return the string, the integer, or the double-precision floating point number that was the result of the last Tcl/Tk command in the argument script.

Wherever the string %d(x) occurs inside an ET() function, the integer C expression x is converted to ASCII and substituted in place of the %d(x). Similarly, %s(x) can be used to substitute a character string, and %f(x) substitutes a floating point value. The string %q(x) works like %s(x) except that a backslash is inserted before each character that has special meaning to Tcl/Tk.

The special construct ET_PROC( newcmd ){...} defines a C function that is invoked whenever the newcmd Tcl/Tk command is executed. Formal parameters to this function, argc and argv, describe the arguments to the command. The formal parameter interp is a pointer to the Tcl/Tk interpreter. If a file named aux.c contains one or more ET_PROC macros, the commands associated with those macros are registered with the Tcl/Tk interpreter by invoking ET_INSTALL_COMMANDS(aux.c) after the Et_Init() in the main procedure.

The statement ET_INCLUDE( script.tcl ) causes the Tcl/Tk script in the file script.tcl to be made a part of the C program and executed at the point where the ET_INCLUDE() macro is found. The external Tcl/Tk script is normally read into the C program at compile-time and thus becomes part of the executable. However, if the -dynamic command-line option is given to the et2c preprocessor, loading of the external Tcl/Tk script is deferred to run-time.

Finally, at the top of its output files, the et2c preprocessor inserts #defines that make ET_OK and ET_ERROR equivalent to TCL_OK and TCL_ERROR. This often eliminates the need to put ``#include <tcl.h>'' at the beginning of files that use ET.

And that's everything in ET! All the rest is just detail.

4 A Quick Review Of Event Driven Programs

Before we delve into the details of ET, it may be helpful to review the concept of an event loop and an event-driven program. Many ET users have never before written an event-driven graphical user interface (GUI) and may be unfamiliar with how such programs operate. If you are such a user, you may profit from this quick review. But if you are already familiar with event-driven programs, feel free skip ahead to section 5.

The only inputs to a GUI are ``events.'' An event is a notification that something interesting has happened. Events arrive whenever the mouse moves, or a mouse button is pressed or released, or a key of the keyboard is pressed, and so forth. A event-driven GUI differs from more familiar command-line programs in that its inputs (e.g. events) do not arrived in any predictable sequence. Any kind of events can arrive at any time, and the GUI program must be prepared to deal with them.

The code for an event-driven GUI can be divided into two parts: the initialization code and the event loop. The initialization code runs first and does nothing more than allocate and initialize the internal data structures of the application. As soon as the initialization code completes, the application enters the event loop. Within the event loop, the program waits for the next event to arrive, reads the event, and processes it appropriately. The loop then repeats. The event loop does not exit until the program terminates.

This is a schematic view of a typical GUI program:

   main(){
      /* Initialization code */
      while( /* More work to do */ ){
        /* Wait for the next event to arrive */
        /* Read the next event */
        /* Take appropriate action for the event just read */
      }
   }
Don't worry about the details here. Most of the event loop processing is handled automatically by Tcl/Tk and ET. The important things to know are that the event loop exists, it runs after the initialization code, and that it doesn't terminate until the program exits.

If you've never written an event-driven program before, and you are like most people, then you will have a little trouble at first. To help you get started, here are some important points to remember:

  1. The initialization code does not interact with the user.

    The initialization code does only one thing -- initialize. It creates the main windows of the application (but it doesn't draw the windows -- that happens in the event loop!) and it sets up internal data structures. But the initialization code should never wait for input or respond to an event. Waiting and reading inputs and responding to events should happen only in the event loop.

  2. All user-initiated processing occurs in callbacks.

    Everything that a GUI program does is in response to some event. Any C procedure or Tcl/Tk command that is called in response to an event is referred to as a callback. Because all inputs to a GUI program are in the form of events, the only place for user-initiated processing to occur is within the callback routines.

  3. Don't let a callback compute for more than a fraction of a second.

    A callback should do its job quickly and then return. Otherwise, the event loop will not be able to respond to new events as they arrive, and the program will appear to ``hang''. If you have a callback that needs to execute for more than a few hundred milliseconds, you should either invoke the ``update idletasks'' Tcl/Tk command periodically within the callback, or you should break the callback's calculations up into several separate routines that can be invoked by separate events.

  4. Don't leak memory.

    Once started, GUI programs tend to run for a long time -- hours, days, weeks or even months. Hence, you should take special care to avoid memory leaks. A memory leak occurs when you allocate a chunk of memory from the heap using malloc() but don't return that memory to the heap using free() when you are done with it. Because the memory was not released by free() it can never be reused. When this happens, the amount of memory required by your application will constantly increase, until at some point it will consume all memory available, and then die. Memory leaks are probably the most common bug in GUI programs (which is why I mention them.)

5 The Main Body Of An ET Application

The main() routines for ET applications all look pretty much alike. Here's a template:

  void main(int argc, char **argv){
    Et_Init(&argc,argv);   /* Start the Tcl/Tk interpreter */
    /* Create new Tcl/Tk commands here */
    /* Initialize data structures here */
    /* Create windows for the application here */
    Et_MainLoop();         /* The event loop */
  }
When you need to write an ET application, but you aren't sure where to begin, this template is a good starting point. Type in the above template and make sure you can successfully compile and run it. (The program that results from compiling the template creates a blank window that doesn't respond to any mouse or keyboard inputs. Its the equivalent of ``wish /dev/null''.) After you get the template running, slowly begin adding bits of code, recompiling and testing as you go, until you have a complete application.

Let's take a closer look at each line of the template, so that you can better understand what is going on.

The first line of main() is a call to the Et_Init() procedure. The Et_Init() procedure initializes the ET system and the Tcl/Tk interpreter. It must be called before any other ET function or statement. The parameters are the argc and argv formal parameters of main(). Et_Init() uses these parameters to look for command-line options. ET currently understands four:

Notice the ``&'' before the argc parameter to Et_Init(). The number of command line arguments is passed to Et_Init() by address, not by value. This is so Et_Init() can change the value of argc. Whenever Et_Init() sees one of the above command-line options, it removes that option from the option list in argc and argv. Hence, after Et_Init() returns, only application-specific command line options remain.

For example, suppose you invoke an ET program like this:

    myapp -quiet -display stego:0 file1.data
The values of argc and argv passed into the Et_Init() function are:
    argc = 5
    argv = { "myapp", "-quiet", "-display", "stego:0", "file1.data", 0 }
The Et_Init() function will see the -display stego:0 part and act upon it accordingly. It will then remove those fields from the argument list, so that after Et_Init() returns, the values are these:
    argc = 3
    argv = { "myapp", "-quiet", "file1.data", 0 }
In this way, the initialization code that follows Et_Init() never sees the ET-specific command line arguments.

After the Et_Init() procedure comes the initialization code. Normally, you begin the initialization by creating and registering all the new Tcl/Tk commands you will need. The details are described in section 6. Basically it involves replacing the comment in the template with one or more ET_INSTALL_COMMANDS statements. Once you've created the new Tcl/Tk commands, you may need to construct internal C data structures, or create linkages between C variables and Tcl variables using Tcl's Tcl_LinkVar() function. Command-line options that haven't been removed by Et_Init() are often processed here, as well. Finally, you will probably want to create the initial windows for the application. The ET() function (see section 7) and ET_INCLUDE() procedure (see section 8) are both good for this.

Of course, this is only a suggested outline of how to initialize your application. You should feel free to do something different if your program requires it. The only ground rule is that the initialization code shouldn't try to interact with the user. Instead, use callback routines to respond to user inputs.

The last line of main() is a call to the Et_MainLoop() procedure. Et_MainLoop() implements the event loop. It will not return until the program is ready to exit.

6 Writing Tcl/Tk Routines In C

One of the first things people tend to do with ET is create new Tcl/Tk commands, written in C, that do computations that are either too slow or impossible with a pure Tcl. This is a two-step process. First you have to write the C code using the ET_PROC construct. Then you have to register your new Tcl/Tk command with the Tcl/Tk interpreter using the ET_INSTALL_COMMANDS statement. We will consider each of these steps in turn.

6.1 The Decimal Clock Sample Program

To help illustrate the concepts, this section introduces a new sample program: the decimal clock. The decimal clock displays the current time of day as a decimal number of hours. For instance, 8:30am displays as ``8.500''. 11:15pm shows as ``23.250''. And so forth. A screen shot of this program is shown in figure 6.1.


**Image**

Figure 6.1: Typical appearance of the decimal clock


We'll begin by looking at the main procedure for the decimal clock program.

  void main(int argc, char **argv){
    Et_Init(&argc, argv);
    ET_INSTALL_COMMANDS;
    ET(
      label .x -bd 2 -relief raised -width 7
      pack .x
      proc Update {} {
        .x config -text [DecimalTime]
        after 3600 Update
      }
      Update
    );
    Et_MainLoop();
  }
As you can see, the main procedure is just a copy of the program template from section 5, with some of the comments replaced by actual initialization code. The first initialization action is to invoke the special ET statement ET_INSTALL_COMMANDS. Don't worry about what this does just yet -- we'll return to it a little later. The second initialization action is a single ET() function containing 7 lines of Tcl/Tk. This Tcl/Tk code does three things: Like all well-behaved ET programs, the main procedure for the decimal clock concludes by entering the event loop.

6.2 The ET_PROC Statement

The core of the decimal clock program is a new Tcl/Tk command, DecimalTime, that returns the current time of day as a decimal number of hours. This new command is written in C, using the special ET_PROC construct of ET. The code looks like this:

  #include "tcl.h"
  #include <time.h>

  ET_PROC( DecimalTime ){
    struct tm *pTime;  /* The time of day decoded */
    time_t now;        /* Number of seconds since the epoch */

    now = time(0);
    pTime = localtime(&now);
    sprintf(interp->result,"%2d.%03d",pTime->tm_hour,
      (pTime->tm_sec + 60*pTime->tm_min)*10/36);
    return ET_OK;
  }
The magic is in the ET_PROC keyword. The et2c preprocessor recognizes this keyword and converts the code that follows it into a compilable C function that implements the Tcl/Tk command. In general, you can create new Tcl/Tk commands using a template like this:
  ET_PROC( name-of-the-new-command ){
    /* C code to implement the command */
  }
You could, of course, construct approprate C functions by hand, but that involves writing a bunch of messy details that detract from the legibility of the code. The ET_PROC mechanism is much easier to write and understand, and much less subject to error.

Though they do not appear explicitly in the source code, every function created using ET_PROC has four formal parameters.

The decimal clock example uses the interp formal parameter on the sixth line of the ET_PROC function. In particular, the DecimalTime function writes its result (e.g. the time as a decimal number) into the result field of interp. It's OK to write up to about 200 bytes of text into the result field of the interp parameter, and that text will become the return value of the Tcl/Tk command. If you need to return more than about 200 bytes of text, then you should set the result using one of the routines from the Tcl library designed for that purpose: Tcl_SetResult(), Tcl_AppendResult(), or Tcl_AppendElement(). (These routines are documented by Tcl's manual pages under the name ``SetResult''.) If all this seems too complicated, then you can choose to do nothing at all, in which case the return value defaults to an empty string.

Another important feature of every ET_PROC function is its return value. Every ET_PROC should return either ET_OK or ET_ERROR, depending on whether or not the function encountered any errors. (ET_OK and ET_ERROR are #define constants inserted by et2c and have the save values as TCL_OK and TCL_ERROR.) It is impossible for the DecimalClock function to fail, so it always returns ET_OK, but most ET_PROC functions can return either result.

Part of Tcl's result protocol is that if a command returns ET_ERROR it should put an error message in the interp->result field. If we had wanted to be pedantic, we could have put a test in the DecimalTime function to make sure it is called with no arguments. Like this:

  ET_PROC( DecimalTime ){
    struct tm *pTime;  /* The time of day decoded */
    time_t now;        /* Number of seconds since the epoch */

    if( argc!=1 ){
      Tcl_AppendResult(interp,"The ",argv[0],
        " command should have no argument!",0);
      return ET_ERROR;
    } 
    /* The rest of the code is omitted ... */
  }
New Tcl/Tk commands that take a fixed format normally need to have some checks like this, to make sure they aren't called with too many or too few arguments.

6.3 The ET_INSTALL_COMMANDS statement

We've seen how the ET_PROC constuct will create a new Tcl/Tk command. But that command must still be registered with the Tcl interpreter before it can be used. Fortunately, ET makes this very easy.

ET uses the ET_INSTALL_COMMANDS keyword to register ET_PROC commands with the Tcl interpreter. The et2c preprocessor converts the ET_INSTALL_COMMANDS keyword into a sequence of C instructions that register every ET_PROC in the current file. In the main() procedure of the decimal clock example, the ET_INSTALL_COMMANDS keyword that immediately follows the Et_Init() function is used to register the DecimalTime command. As it turns out, DecimalTime is the only ET_PROC function in the same source file, but even if there had be 100 others, they would have all been registered by that single ET_INSTALL_COMMANDS statement.

The ET_INSTALL_COMMANDS keyword can also be used to register ET_PROC functions in separate source files, simply by putting the name of the source file in parentheses after the ET_INSTALL_COMMANDS keyword. Like this:

   ET_INSTALL_COMMANDS( otherfile.c );
A larger program will typically have many ET_INSTALL_COMMANDS statements immediately following the Et_Init() function, one statement for each file that contains ET_PROC functions. One recent commercial project used 33 ET_INSTALL_COMMANDS statements following the Et_Init() function!

6.4 Summary Of Writing Tcl/Tk Commands In C

Before leaving this section, let's briefly summarize the steps needed to create new Tcl/Tk commands in C using ET. First you create one or more commands using the ET_PROC construct, as follows:

  ET_PROC( name-of-the-new-command ){
    /* C code to implement the command */
    return ET_OK;  /* Don't forget the return value! */
  }
Then, you register these commands with the Tcl interpreter using an ET_INSTALL_COMMANDS statement after the Et_Init() function call within main(). Like this:
  ET_INSTALL_COMMANDS( name-of-file-containing-ET_PROCs.c );
And that's all you have to do!

The ET_PROC construct lets you put a C routine in the middle of Tcl/Tk. The next section will take a closer look at ET() which allows you to put Tcl/Tk in the middle of a C routine.

7 The ET() Function And Its Siblings

If you've been keeping up with the examples, you've already seen the ET() function used twice to insert a few lines of Tcl/Tk in the middle of a C procedure. But the ET() function can do a lot more, as this section will show.

7.1 Moving Information From Tcl/Tk To C

The first thing to note about ET() is that, just like a real C function, it has a return value. ET() returns an integer status code which is either ET_OK or ET_ERROR depending on whether the enclosed Tcl/Tk was successful or failed. (ET() might also return TCL_RETURN, TCL_BREAK, or TCL_CONTINUE under rare circumstances.)

The status return of ET() is nice, but in practice it turns out to be mostly useless. What you really need is the string value returned by the enclosed Tcl/Tk script. That's the purpose of the ET_STR() function.

The ET_STR() function works a lot like ET(). You put in a Tcl/Tk script as the argument, and the script gets executed. But instead of returning a status code, ET_STR() returns a pointer to a string that was the result of the last Tcl/Tk command in its argument.

The ET_STR() function turns out to be a very handy mechanism for querying values from Tcl/Tk. For instance, suppose your program has an entry widget named ``.entry'' and some piece of C code needs to know the current contents of the entry. You can write this:

  char *entryText = ET_STR(.entry get);
Or imagine that you need to know the current size and position of your main window. You might use code like this:
  int width, height, x, y;
  sscanf(ET_STR(wm geometry .),"%dx%d+%d+%d",&width,&height,&x,&y);
Does your C routine need to know the value of a Tcl variable? You could use the cumbersome Tcl_GetVar() function, but it's much easier to say:
  char *zCustomerName = ET_STR(set CustomerName);
Possible uses for ET_STR() seem limitless.

But, there are two subtleties with ET_STR() that programmers should always keep in mind. The first is that the Tcl/Tk script in the argument is executed at Tcl's global variable context level. This means that all of the Tcl/Tk variables ET_STR() creates, and the only Tcl/Tk variables it can access, are global variables. This limitation also applies to the regular ET() function, and to two other function we haven't talked about yet: ET_INT() and ET_DBL(). ET provides no means for C code to access or modify local variables. On the other hand, this has not proven to be a serious hardship in practice.

The second subtlety with ET_STR() is more dangerous, but fortunately applies to ET_STR() only. Recall that ET_STR() returns a pointer to a string, not the string itself. The string actually resides in memory that is held deep within the bowels of Tcl/Tk. The danger is that the next Tcl/Tk command may choose to change, deallocate, or reuse this memory, corrupting the value returned by ET_STR(). We say that the return value of ET_STR() is ``ephemeral.''

One way to overcome the ephemerality of ET_STR() is by making a copy of the returned string. The strdup() function is good for this. (Unfortunately, strdup() is missing from a lot of C libraries. You may have to write your own string duplicator.) In place of the examples given above, you might write

  char *entryText = strdup( ET_STR(.entry get) );
or
  char *zCustomerName = strdup( ET_STR(set CustomerName) );
The strdup() function uses malloc() to get the memory it needs, so if you use this approach, be sure to free() the value when you are done to avoid a memory leak!

The other way to overcome the ephemerality of ET_STR() is simply not to use the returned string for very long. You should be safe in using the returned string as long as you don't invoke any other Tcl/Tk commands, or return to the event loop. Code like this

  sscanf(ET_STR(wm geometry .),"%dx%d+%d+%d",&width,&height,&x,&y);
is OK since we need the return value only for the duration of the sscanf() function and sscanf() doesn't use Tcl/Tk.

In addition to ET() and ET_STR(), the ET system provides two other functions named ET_INT() and ET_DBL(). Both take a Tcl/Tk script for their argument, as you would expect. But ET_INT() returns an integer result and ET_DBL() returns a floating-point value (a double). In a sense, these two functions are extensions of ET_STR(). In fact, ET_INT() does essentially the same thing as

   int v = strtol( ET_STR(...), 0, 0);
and ET_DBL() is equivalent to
   double r = strtod( ET_STR(...), 0);
Because ET_INT() and ET_DBL() return a value, not a pointer, their results are not ephemeral nor subject to the problems that can come up with ET_STR().

7.2 Moving Information From C To Tcl/Tk

We've seen how ET_STR(), ET_INT() and ET_DBL() can be used to pass values from Tcl/Tk back to C. But how do you go the other way and transfer C variable values into Tcl/Tk? ET has a mechanism to accomplish this too, of course.

Within the argument to any ET() function (or ET_STR() or ET_INT() or ET_DBL()), the string ``%d(x)'' is special. When ET sees such a string, it evalutes the integer C expression ``x'', converts the resulting integer into decimal, and substitutes the integer's decimal value for the original string. For example, suppose you want to initialize the Tcl/Tk variable named nPayment to be twelve times the value of a C variable called nYear. You might write the following code:

    ET( set nPayment %d(12*nYear) );
As another example, suppose you want to draw a circle on the canvas .cnvs centered at (x,y) with radius r. You could say:
    id = ET_INT( .cnvs create oval %d(x-r) %d(y-r) %d(x+r) %d(y+r) );
Notice here how the ET_INT() function was used to record the integer object ID returned by the Tcl/Tk canvas create command. This allows us to later delete or modify the circle by referring to its ID. For example, to change the fill color of the circle, we could execute the following:
    ET( .cnvs itemconfig %d(id) -fill skyblue );

If you want to substitute a string or floating-point value into an ET() argument, you can use %s(x) and %f(x) in place of %d(x). The names of these substitutions phrases were inspired by the equivalent substitution tokens in the standard C library function printf(). Note, however, that you cannot specify a field-width, precision, or option flag in ET() like you can in printf(). In other words, you can use conversions like %-10.3f in prinf() but not in ET(). The ET() function will accepts only specification, such as %f.

But the ET() function does support a conversion specifier that standard printf() does not: the %q(x) substitution. The %q works like %s in that it expects its argument to be a null-terminated string, but unlike %s the %q converter inserts extra backslash characters into the string in order to escape characters that have special meaning to Tcl/Tk. Consider an example.

  char *s = "The price is $1.45";
  ET( puts "%q(s)" );
Because %q(s) was used instead of %s(s), an extra backslash is inserted immediately before the ``$''. The command string passed to the Tcl/Tk interpreter is therefore:
  puts "The price is \$1.45"
This gives the expected result. Without the extra backslash, Tcl/Tk would have tried to expand ``$1'' as a variable, resulting in an error message like this:
  can't read "1": no such variable
In general, it is always a good idea to use %q(...) instead of %s(...) around strings that originate from outside the program--you never know when such strings may contain a character that needs to be escaped.

7.3 Summary Of The ET() Function

And that's everything there is to know about the ET() function and its siblings. In case you missed something amid all the details, here's a 10-second review of the essential facts:

Now lets move on and talk about a similar construct, ET_INCLUDE(), that allows you incorporate whole files full of Tcl/Tk into your application.

8 Including External Tcl/Tk Scripts In A C Program

In the sample programs seen so far in this article, Tcl/Tk code in an ET() function was used to construct the main window. This works fine for the examples, since their windows are uncomplicated and can be constructed with a few lines code. But in a real application, or even a more complex example, the amount of Tcl/Tk code needed to initialize the program's windows can quickly grow to hundreds or thousands of lines. It is impractical and irksome to put this much code into an ET() statement, so the ET system provides another way to get the job done: the ET_INCLUDE() statement.

The ET_INCLUDE() statement is similar in concept to the #include statement in the C preprocessor. Both take a filename as their argument, and both read the named file into the original source program. The ET_INCLUDE() statement expects its file to be pure Tcl/Tk code, though. Its job is to turn the Tcl/Tk source into a form that the C compiler can understand, and to arrange for the Tcl/Tk to be executed when control reaches the ET_INCLUDE() statement.

An example may help to clarify this idea. In the decimal clock program (way back at the beginning of section 6), there are 7 lines of Tcl/Tk in an ET() function used to create the application's main window. Now suppose we move those 7 lines of Tcl/Tk into a separate file named dclock.tcl. Then we could replace the ET() function with an ET_INCLUDE() statement that references the new file like this:

  void main(int argc, char **argv){
    Et_Init(&argc, argv);
    ET_INSTALL_COMMANDS;
    ET_INCLUDE( dclock.tcl );
    Et_MainLoop();
  }
When the et2c preprocessor sees the ET_INCLUDE() statement, it locates the specified file, reads that file into the C program, and makes arrangements for the text of the file to be executed as if it had all appeared within an ET() function.

Well, almost like an ET() function. There are a couple of minor differences. The ET_INCLUDE() does not understand the various %s(...) substitutions as ET() does. Also, ET_INCLUDE() is a true procedure, not a function. It doesn't return a value like ET() so you can't use an ET_INCLUDE() in an expression.

It is important to understand the difference between an ET_INCLUDE() statement like this

    ET_INCLUDE( dclock.tcl );
and the source command of Tcl/Tk, used as follows:
    ET( source dclock.tcl );
The ET_INCLUDE() statement reads the Tcl/Tk into the program at compile-time, effectively making the Tcl/Tk code part of the executable. The Tcl source command, on the other hand, opens and reads the file at run-time, as the application executes. This makes the executable a little smaller, but it also means that the file containing the Tcl/Tk must be available to the executable whenever it runs. If you move just the executable, but not the Tcl/Tk file, to another computer, or even another directory, then it will no longer work because it won't be able to locate and read the Tcl/Tk file.

The ability to read an external Tcl/Tk script and make it part of the executable program is an important feature of ET. But while you are developing and testing a program, it is sometimes convenient to turn this feature off and to have the application read its scripts at run-time instead of compile-time. That way, you can make changes to the Tcl/Tk script and rerun your program with the changes, but without having to recompile. You can do this using the -dynamic option to the et2c proprocessor. Whenever you run et2c with the -dynamic command-line option, it effective turns instances of the statement

    ET_INCLUDE( filename.tcl );
into the statement
    ET( source filename.tcl );
This feature has proven very helpful during development. But be careful to turn it off before doing your final build, or else you won't be able to move your executable to other machines!

There is just one other feature of the ET_INCLUDE() statement that we need to discuss before moving on, and that is the algorithm it uses to locate the Tcl/Tk source code files. Just like the C preprocessor's #include statement, the ET_INCLUDE() mechanism can include files found in other directories.

The et2c preprocessor always looks first in the working directory for files named by an ET_INCLUDE() statement. If the file is found there, no further search is made. But if the file is not found, then et2c will also look in all directories named in -I command line options. For example, if you run et2c like this:

    et2c -I../tcl -I/usr/local/lib/tcl app.c >app_.c
and the app.c file contains a line of the form:
    ET_INCLUDE( setup.tcl );
then et2c will search for the setup.tcl first in the ``.'' directory, then in ../tcl and in /usr/local/lib/tcl. It will use the first instance of setup.tcl that it finds.

9 Global Variables In ET

The et.o library for ET defines three global C variables that are sometimes of use to programmers. In addition, Et_Init() creates two new global Tcl/Tk variables that many programs find useful. This section will describe what all of these variables do, and suggest ways that they can be used.

9.1 Global C Variables Created By ET

Perhaps the most useful of the global variables available in ET is Et_Interp. This variable is a pointer to the Tcl/Tk interpreter, the one created by Et_Init() and used to execute all Tcl/Tk commands within the program. The Et_Interp variable has the same value as the interp formal parameter found in every ET_PROC() function.

The Et_Interp variable is useful because you may often want to call C routines in the Tcl/Tk library, and most of these routines require a pointer to the interpreter as their first parameter. For instance, suppose in the initialization code you want to create a link between the global C variable nClients and a Tcl/Tk variable by the same name. Using the Et_Interp variable as the first parameter to the Tcl function Tcl_LinkVar(), you could write:

   Tcl_LinkVar(Et_Interp,"nClients",(char*)&nClients,TCL_LINK_INT);
Having done this, any changes to the C nClients variable will be reflected in the Tcl/Tk variable, and vice versa.

Perhaps the second most useful global varible is Et_Display. This variable contains the Display pointer required as the first argument to most Xlib routines. It is used by daring, down-to-the-bare-metal programmers who like to call Xlib directly.

Here's an example. Suppose you want to create a new Tcl/Tk command, PitchedBell, that makes the X terminal emit a beep with a pitch specified by its sole argument. Once such a command is implemented, then the following Tcl/Tk code would emit a single tone at the pitch of concert A:

   PitchedBell 440
Here a short piece of Tcl/Tk code that plays the opening bar of Beethoven's Fifth Symphony:
   foreach pitch {784 784 784 659} {
      PitchedBell $pitch
      after 200
   }
You probably get the idea. Here's the code that implements the PitchedBell command:
   #include <tk.h>   /* Will also pickup <Xlib.h> */

   ET_PROC( PitchedBell ){
      XKeyboardControl ctrl;   /* For changing the bell pitch */

      if( argc!=2 ){
        interp->result = 
          "Wrong # args.  Should be: ``PitchedBell PITCH''";
        return ET_ERROR;
      }
      ctrl.bell_pitch = atoi( argv[1] );
      XChangeKeyboardControl(Et_Display,KBBellPitch,&ctrl);
      XBell(Et_Display,0);
      XFlush(Et_Display);
      return ET_OK;
   }
After checking to make sure it has exactly one argument, the PitchedBell command uses the XChangeKeyboardControl() function of Xlib to change the bell pitch. It then rings the bell using the XBell() Xlib function, and finally flushes the Xlib message queue using XFlush() to force the bell to be rung immediately. All three of these Xlib functions require a Display pointer as their first argument, a role that is perfectly filled by the Et_Display global variable.

The third and final global C variable in ET is Et_MainWindow. This variable is a pointer to the Tcl/Tk structure that defines the application's main window. Back in the days of Tk3.6, several Tcl/Tk library functions required this value as a parameter. But the Tcl/Tk library interface changed in the move to Tk4.0, so that the main window pointer is no longer required. Hence, the Et_MainWindow variable isn't used much any more. It has been kept around as an historical artifact.

9.2 Tcl/Tk Variables Created By ET

Besides the 3 global C variables, ET also provides two Tcl/Tk variables that are of frequent use: cmd_name and cmd_dir. The cmd_name variable contains the name of the file holding the executable for the application, and cmd_dir is the name of the directory containing that file.

The cmd_name and cmd_dir variables are useful to programs that need to read or write auxiliary data files. In order to open an auxiliary file, the program needs to know the files pathname, but it is not a good idea to hard-code a complete pathname into the program. Otherwise, the auxiliary file can't be moved without recompiling the program. By careful use of cmd_name and/or cmd_dir, we can arrange to have auxiliary files located in a directory relative to the executable, rather that at some fixed location. That way, a system adminstrator is free to move the auxiliary file to a different directory as long as the executable moves with it.

For example, suppose you are writing a program named acctrec that needs to access a data file named acctrec.db. Furthermore, suppose the data file is located in a directory ../data relative to the executable. Then to open the data file for reading, a program could write:

  char *fullName = ET_STR( return $cmd_dir/../data/$cmd_name.db );
  FILE *fp = fopen(fullName,"r");
Using this scheme, both the executable and the datafile can be placed anywhere in the filesystem, as long as they are in the same position relative to one another. They can also be renamed, so long as they retain the same base name. This flexibility is a boon to system adminstraters, and also make the program less sensitive to installation errors.

10 Reading From Standard Input

There's one last feature of ET that we haven't discussed: the Et_ReadStdin() procedure. If this procedure is called (with no arguments) in between the calls to Et_Init() and Et_MainLoop(), ET will make arrangements to read all data that appears on standard input and interpret that data as Tcl/Tk commands.

You can use the Et_ReadStdin() to implement the interactive wish interpreter for Tcl/Tk. The code would look like this:

  main(int argc, char **argv){
    Et_Init(&argc,argv);
    Et_ReadStdin();
    Et_MainLoop();
  }
Let's call this program etwish in order to distinguish it from the standard wish that comes with Tcl/tk. The etwish program differs from wish in two ways. First, wish reads a set of 15 or so Tcl/Tk scripts from a well-known directory when it first starts up. Thus, to install wish, you have to have both the wish executable and the 15 startup scripts. But with etwish, the 15 startup scripts are compiled into the executable (using ET_INCLUDE() statements inside the Et_Init() function) so the external scripts are no longer required. This does make the etwish executable slightly larger (by about 64K bytes), but it also makes the program much easier to install and administer.

The second difference between wish and the etwish program shown above is that etwish is always interactive. It will not read a script from a file given as a command line argument like standard wish will. But we can remove that difference using a little more code.

  main(int argc, char **argv){
    Et_Init(&argc,argv);
    if( argc>2 && (strcmp(argv[1],"-f")==0 || strcmp(argv[1],"-file")==0) ){
      ET( source "%q(argv[2])" );
    }else if( argc>1 ){
      ET( source "%q(argv[1])" );
    }else{
      Et_ReadStdin();
    }
    Et_MainLoop();
  }
This revised program serves as a great template for building customized editions of wish that have one or more new Tcl/Tk commands written in C. All you have to do is code the new commands using the ET_PROC() mechanism and insert a single ET_INSTALL_COMMANDS statement right after the Et_Init().

11 Compiling ET Applications

We've already discussed the basics of compiling ET applications back in section 2 when we put together the ``Hello, World!'' example. Basically, all you do is preprocess your source files with et2c and then run the results through the C compiler. But that synopsis omits a lot of detail. This section fills in the missing information.

11.1 Compiling ET Itself

But before we begin talking about how to compile ET applications, we need to first mention how to compile ET itself -- the et2c preprocessor and the et.o library. (If you have one of the platforms supported by the CD-ROM in the back of this book, then you already have precompiled versions of et2c and et.o and can skip this step.)

The source code to the et2c preprocessor is contained in a single file named et2c.c. The preprocessor is written in highly portable K&R C and should compile without change on just about any 32-bit architecture. All you have to do is this:

  cc -O -o et2c et2c.c

Compiling the et.o library is a little more problematic, but still not difficult. There are three steps. First you have to select an appropriate source code file. There are different versions of the source code (sometimes radically different) depending on which version of Tcl/Tk you are using. For Tk version 3.6, choose et36.c. For Tk version 4.0, choose et40.c. For Tk version 4.1 on UNIX and X11, choose et41.c. Your ET distribution may also have other options, such as versions for MS-Windows or Macintosh, or versions with built-in support for various Tcl extensions.

Let's suppose, for the sake of discussion, that you selected the source file et41.c. The next step is to preprocess this file using et2c. This step is a little tricky because we have to use the -I option to et2c to tell the preprocessor where to find the Tcl/Tk startup scripts.

Recall that the stardard Tcl/Tk interpreter program, wish, reads and executes a series of Tcl/Tk scripts when it first starts up. These scripts set up default widget bindings, create procedures for handling menus, and so forth. The names of the directories from which these scripts are loaded are hard-coded in the wish executable. There are about 15 different startup scripts (the number varies from one version of Tcl/Tk to the next) and wish will not run without them.

But ET applications don't read the startup scripts at run-time. Instead, a series of ET_INCLUDE() statements inside the Et_Init() function bind the startup scripts into an ET executable at compile-time. This feature is what enables ET applications to run on machines that do not have Tcl/Tk installed.

It is because of 15 or so startup scripts included by ET_INCLUDE() statements in the ET library that we have to preprocess the library source code using et2c. But we also have to tell et2c what directories to use when searching for the startup scripts. If Tcl/Tk has already been installed on your system, then you can find out the names of the startup script directories by executing the following wish script:

  #! wish
  puts $tk_library
  puts $tcl_library
Let's suppose that the startup scripts are located in the directories /usr/local/lib/tcl and /usr/local/lib/tk. Then the command to preprocess the ET library source code would be the following:
  et2c -I/usr/local/lib/tcl -I/usr/local/lib/tk et41.c >et.c

After preprocessing the library source code, all that remains is to compile it. The library references the <tk.h> header file, which in turn references <tcl.h>, so you may have to add some -I options to the compiler command line to specify the directories where these header files are located. The following is typical:

  cc -c -o et.o -I/usr/include/tcl -I/usr/include/tk et.c

11.2 Compiling The Application Code

Once you get et2c and et.o compiled, the hard work is done. To build your application, simply run each source file through the et2c preprocessor before compiling it, and add the et.o library with the final link. For example, the steps to compile a program from two source files, appmain.c and appaux.c, are the something like following on most systems:

   et2c appmain.c >temp.c
   cc -c temp.c -o appmain.o
   et2c appaux.c >temp.c
   cc -c temp.c -o appaux.o
   cc appmain.o appaux.o et.o -ltk -ltcl -lX11 -lm

If you're using a Makefile, you might want to redefine the default rule for converting C source code into object code to incorporate the et2c preprocessor step. Like this:

    .c.o:
          et2c $<>temp.c
          cc -c -o $@ temp.c
The et2c does not harm files that don't use ET constructs, so this rule will work for every file in your project.

11.3 Turning Off Script Compression In The Preprocessor

The et2c preprocessor attempts to save memory and improve performance of your application by removing comments and unnecessary spaces from the Tcl/Tk code inside ET() functions and loaded by ET_INCLUDE() statements. This mechanism works well most of the time, but it is not foolproof. It is theoretically possible for a valid Tcl/Tk script to be corrupted by et2c's compression attempts. If you experience trouble, and suspect that et2c is messing up your Tcl/Tk code, then you can turn script compression off using the -nocompress command line option.

11.4 Compiling Using An Older K&R Compiler

If it is your misfortune not to have an ANSI C compiler, you can still use ET. The source code to et2c is pure K&R C and should work fine under older compilers. The source code to et.o is another matter. To compile the library using an older compiler you need to first give a -K+R option to et2c and then give a -DK_AND_R option to the C compiler:

  et2c -K+R -I/usr/local/lib/tcl -I/usr/local/lib/tk et40.c >et.c
  cc -DK_AND_R -I/usr/include/tcl -I/usr/include/tk -c et.c
When compiling application code with an older compiler, just give the -K+R option to et2c. It is not necessary to give the -DK_AND_R option to the C compiler when compiling objects other than et.c.

12 Other ET Sample Programs

Besides the very simple ``Hello, World!'' and decimal clock programs presented above, ET is distributed with a number of non-trivial sample programs. This section will briefly overview what several of these example programs do, and why ET was important to their implementation. We won't try to explain the details of how the programs work, though. You can figure that out for yourself by looking at the source code.

12.1 The Color Chooser

There is a color chooser tool for X11 called color. The sources to color are in the files color.c and color.tcl. A screen image of the program is shown in figure 12.1.


**Image**

Figure 12.1: Typical appearance of the color program


The X11 Window System supports displays with over 280 quadrillion distinct colors (48 bits per pixel). But from this vast number, a few hundred colors are assigned English names like ``blue'' or ``turquoise'' or ``peachpuff''. All the rest are given arcane hexadecimal designations like ``#b22cd8517f32''. It is best to use colors with English names whenever possible.

The purpose of the color program it to help select colors with English names. At the top of the application is large swatch showing one of the 280 quadrillion X11 colors, together with either its English name (if it has one) or its hexadecimal value. Sliders on the lower left side of the window allow the user to vary the color of the swatch by changing various color components. On the lower right side of the window are six smaller swatches that show colors with English names that are similar to the color in the main swatch. Moving any of the six color component sliders causes the colors in all swatches, and the other sliders, to update in real time. Clicking on any of the smaller swatches transfers its color to the main swatch, updating all of the sliders and swatches appropriately.

In theory, there is nothing to prevent the color program from being coded in pure Tcl/Tk, but in practice, such an implementation would be much too slow. For this reason, two key routines are coded in C. The ET_PROC command ChangeComponent is called whenever one of the color component sliders is moved. This routine moves the other sliders, changes the color of the main swatch, then computes close colors for the smaller swatches. Another ET_PROC command named ChangeColor is called whenever the user clicks on one of the smaller swatches. This routine changes the color of the main swatch, then updates the sliders and the smaller swatches accordingly.

12.2 The VT100 Terminal Emulator

The example named tkterm implements a VT100 terminal emulator. The tkterm program can be used as a direct replacement for the more familiar emulator programs xterm or rxvt.

The sources for tkterm are contained in three separate files. The main procedure is in tkterm.c. Tcl/Tk for constructing the main window for the application is in tkterm.tcl. Finally, the file getpty.c takes care of the messy details of allocating a pseudo-TTY for the emulator and invoking a shell in the pseudo-TTY. (Much of the code in getpty.c was copied from rxvt.)

The tkterm program simulates the VT100 display using an ordinary Tcl/Tk text widget. C routines in tkterm.c interpret the characters and escape sequences coming into the program and use ET() functions to insert characters into their proper places within the text widget. The tkterm.c file is almost 1000 lines long, and is mostly devoted to interpreting the VT100 escape codes.

The tkterm program is an example of an application that could not be coded in pure Tcl/Tk, since Tcl/Tk has no provisions for dealing with pseudo-TTYs or TTYs in ``raw'' mode. But even if it could, we would probably still want to use some C code, since it seems unlikely that a Tcl/Tk script would be able to process the VT100 escape sequences efficiently.

12.3 A Real-Time Performance Monitor For Linux

The perfmon program is a system performance monitor for the Linux operating system. It uses bar graphs to shows the amount of memory, swap space, CPU time currently being used. The display is updated 10 times per second. There are two source code files for this application: perfmon.c and perfmon.tcl.

The main display of the perfmon program is implemented using a Tcl/Tk canvas widget. But for efficiency's sake, the logic that computes the current memory, swap space, and CPU usages is all coded in C. The C code obtains the system performance data by reading the files /proc/stat and /proc/meminfo. It then processes this information into the desired preformance measurements and makes appropriate changes to the Tcl/Tk bar graphs using ET() function calls.

On a 90MHz Pentium and with an update frequency of 10 times per second, the prefmon program uses a negligible amount of the CPU time. So in addition to being a nifty desktop utility for a Linux workstation, this example demonstrates that Tcl/Tk applications can be very efficient.

12.4 An ASCII Text Editor And A File Browser

The two programs tkedit and browser implement, respectively, an ASCII text editor and a UNIX file browser utility. Source code to these programs is in the files tkedit.c, tkedit.tcl, browser.c and browser.tcl.

Both of these programs could just as well have been implemented as pure Tcl/Tk scripts, with no loss of features or performance. (In fact, the browser can be used as pure script by invoking the browser.tcl using wish.) But, sometimes you want a program to be a real executable, not a script. For instance, you may want to be able to run the program on machines that do not have Tcl/Tk installed. Or, perhaps you want the programs to run on machines that have a different, incompatible version of Tcl/Tk installed.

The tkedit and browser programs are examples of how to convert a pure Tcl/Tk script into a stand-alone program using ET. The idea is very simple. Your C code simply initializes ET, invokes your script using a single ET_INCLUDE() statement, and then enters the event loop. Like this:

  void main(int argc, char **argv){
    Et_Init(&argc,argv);
    ET_INCLUDE( browser.tcl );
    Et_MainLoop();
  }
Compiling this code results in a stand-alone application that can be run on any binary-compatible machine.

13 Using ET To Build MS-Windows And Macintosh Applications

ET, like Tcl/Tk, was originally written to support the open X11 windowing system only. But nowadays, people often need to write applications for popular proprietary windowing systems such as Windows95 or Macintosh. Beginning with release 4.1, Tcl/Tk supports these proprietary products, and so does ET. (Actually, only Windows95 is supported as of this writing. The author has no access to a Macintosh system on which to develop and test a Macintosh port.)

On a Macintosh, ET applications that don't call Xlib directly should compile with little or no change. The Mac won't support the Et_ReadStdin() routine, or the Et_Display global variable, but then again, neither of these make much sense on a Mac. The application will compile in much the same way as it does for X11, except that you should use the et41mac.c source file to the et.o library.

More change is required to support Windows95, however. The Windows version of ET doesn't contain either Et_Init() or Et_MainLoop(). Instead these functions will be invoked automatically. An ET program for Windows should contain a single Et_Main() procedure definition to do all its setup, and nothing more. Hence, if your application used to look like this:

  void main(int argc, char **argv){
    Et_Init(&argc,argv);
    /* Your setup code here */
    Et_MainLoop();
  }
then under Windows, it will look like this instead:
  void Et_Main(int argc, char **argv){
    /* Your setup code here */
  }
Besides that, and the obvious fact that Et_Display is not supported, a Windows ET application should work just like an X11 ET application. It is compiled in the same way, except that you should use the et41win.c source file for the et.o library.

14 Summary And Acknowledgements

Over the past two years, many people have used ET to build programs from a mixture of Tcl/Tk and C. Projects have ranged in size from student programming assignments up to large-scale (100,000+ lines) commercial development efforts. In all cases, ET has proven to be an effective alternative to other GUI toolkits.

The original implementation of ET grew out of a programming contract from Lucent Technologies (formerly AT&T Bell Laboratories). Lucent Technologies was in turn funded under a contract from the United States Navy. Many thanks go to Richard Blanchard at Lucent Technologies and to Charlie Roop, Dave Toms and Clair Guthrie at PMO-428 for allowing ET to be released to the public domain.

The author can be reached at:

D. Richard Hipp
Hipp, Wyrick & Company, Inc.
6200 Maple Cove Lane
Charlotte, NC 28269
704-948-4565
drh@vnet.net