Tcl/Tk has proven to be an excellent language for building small
programs that require a Graphical User Interface (GUI). However, it is
often inadequate for use in large commercial applications for a number
of reasons:

  *  Execution speed is usually too slow for serious computation.
  *  Complex data structures are difficult to construct.
  *  The lack of structure and typing in the Tcl language complicates
     the development of large codes.
  *  Tcl/Tk source code is easily read by the end user, making it hard
     for developers to protect proprietary algorithms.
  *  Large Tcl/Tk programs typically consist of many separate files
     that must be correctly positioned within the target computer's
     file system.  This can make the programs difficult to install,
     maintain and administer.

To circumvent these problems, we have constructed a system that makes
it easy for a C or C++ program to invoke and interact with Tcl/Tk.
This allows data structures and compute-intensive algorithms to be
coded in C or C++ while the GUI is coded in Tcl/Tk. It also allows the
entire application to be compiled into a single stand-alone
executable, which is not easily readable by the end user and which can
be run on computers that do not have Tcl/Tk installed.  We call our
system "ET" for "Embedded Tk".


1.  A Simple Example: ``Hello, World!''

The following is an ET implementation of the classic "Hello, World!"
program:


    void main(int argc, char **argv){
      Et_Init(&argc,argv);
      ET( button .b -text {Hello, World!} -command exit; pack .b );
      Et_MainLoop();
    }

This example is short, but is serves to illustrate the basic structure
of any ET application.  The first line of the main() procedure is a
call to the function Et_Init().  This function initializes the ET
system by creating a Tcl/Tk interpreter, connecting to the X11 server,
and creating a main window.  The last line of main() implements the
event loop.  Everything in between constitutes setup code.  In this
example, the setup code is a short Tcl/Tk script contained within the
special macro ET(). The et2c macro preprocessor will replace this ET()
macro with C code that causes the enclosed Tcl/Tk script to be
executed.

Of course, there is nothing in this example that could not also be
done by calling Tcl/Tk library routines directly, without the
intervening ET abstraction.  The advantage of using ET is that it
makes the interface between C and Tcl/Tk considerably less cumbersome
and error-prone, allowing the programmer to focus more mental energy
on the algorithm and less on the syntax of the programming language.

1.1 Compiling ``Hello, World!''

To compile the hello world example, we must first process the source
file using the et2c macro preprocessor, then link the results with the
et.o library.  Suppose the example code is contained in the file
hello.c. Then to compile the example (on most systems) requires the
following steps:



    et2c hello.c >hello_.c
    cc -o hello hello_.c et.o -ltk -ltcl -lX11 -lm


Assuming it is statically linked, the resulting executable file hello
contains everything needed to run the program: the Tcl/Tk interpreter,
the startup scripts and the application code.  The program can be
moved to other binary-compatible computers and executed there even if
the other computers do not have Tcl/Tk installed.

Additional information is provided below.

1.2 How to obtain sources and documentation

Complete sources to the et2c macro preprocessor and et.o library
comprise less than 2000 lines of code, including comments.  These
sources, together with source code to all example programs discussed
below, are available for anonymous FTP from ftp.vnet.net in the
directory /pub/users/drh.

A copy of this documentation is also available from the same FTP site.
The documentation is available in either PostScript, HTML, or an ASCII
text file.


2.  A Summary Of Services Provided By ET

The overall goal of ET is to simplify the interface between C and an
embedded Tcl/Tk-based GUI. To this end, the ET system provides a
number of services that aid in initializing the Tcl/Tk interpreter and
in transferring data and control between Tcl/Tk and C. The services
provided by ET are summarized here and described in more detail in
subsequent sections.

2.1 Routines to initialization the Tcl/Tk interpreter

The et.o library includes routines Et_Init() and Et_MainLoop() that
initialize the ET package and implement the X11 event loop.  A third
routine Et_ReadStdin() allows standard input to be read and
interpreted by the Tcl/Tk interpreter at run-time.

2.2 Macros to invoking Tcl/Tk from within C

The ET() macro looks and works just like a function in C, except that
its argument is a Tcl/Tk script instead of C code.  ET() returns
either ET_OK or ET_ERROR depending upon the success or failure of the
script.  Similar routines ET_STR(), ET_INT() and ET_DBL() also take a
Tcl/Tk script as their argument, but return a string, an integer, or a
double-precision floating point number instead of the status code.

2.3 A method to pass variable contents from C to Tcl/Tk

Wherever the string %d(x) occurs inside an ET() macro, 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) will substitute 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.

2.4 Macros for creating new Tcl/Tk commands in C

The macro "ET_PROC( newcmd ){ ... }" defines a C function that is
invoked whenever the newcmd command is executed by the Tcl/Tk
interpreter.  Parameters argc and argv describe the arguments to the
command.  If a file named xyzzy.c contains one or more ET_PROC macros,
then the commands associated with those macros are registered with the
Tcl/Tk interpreter by invoking "ET_INSTALL_COMMANDS( xyzzy.c )" after
the Et_Init() in the main procedure.

2.5 Macros for linking external Tcl/Tk scripts into a C program

The macro "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 in the C program 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 option is given to the et2c macro preprocessor, loading of
the external Tcl/Tk script is deferred to run-time.

2.6 Tcl/Tk return status macros

The macros ET_OK and ET_ERROR are set equal to TCL_OK and TCL_ERROR.
This often eliminates the need to put "#include " at the beginning of
files that use ET.

2.7 Convenience variables

ET defines three global C variables as a convenience to the
programmer.  Et_Interp is a pointer to the Tcl/Tk interpreter used by
ET. Et_MainWindow is the main window of the ET application.
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.


3.  Example 2: A Decimal Clock

The preceding "Hello, World!" example program demonstrated the basic
structure of an ET application including the use of the Et_Init()
function to initialize the Tcl/Tk interpreter and the Et_MainLoop()
function for implementing the X11 event loop.  The following program
will demonstrate additional aspects of the the ET system.

3.1 Source code for the decimal clock example


  /* This file implements a clock that shows the hour as 
  ** fixed-point number X, such that
  **
  **      0.000 <= X < 24.000
  **
  ** X represents a fractional hour, not hours and minutes.
  ** Thus the time "8.500" means half past 8 o'clock, not
  ** ten minutes till 9.
  */
  #include <time.h>

  void main(int argc, char **argv){
    Et_Init(&argc,argv);
    ET_INSTALL_COMMANDS;
    ET( 
      label .x -width 6 -text 00.000 -relief raised -bd 2
      pack .x
      UpdateTime
    );
    Et_MainLoop();
  }

  /* Update the time displayed in the text widget named ".x".
  ** Reschedule this routine to be called again after 3.6 
  ** seconds.
  */
  ET_PROC( UpdateTime ){
    struct tm *pTime; /* The time of day, decoded */
    time_t t;         /* Number of seconds since the epoch */
    char buf[40];     /* The time value is written here */

    t = time(0);
    pTime = localtime(&t);
    sprintf(buf,"%2d.%03d",pTime->tm_hour,
       (pTime->tm_sec + 60*pTime->tm_min)*10/36);
    ET( .x config -text %s(buf); after 3600 UpdateTime );
    return ET_OK;
  }

                              **Image**
             3.1 Typical appearance of the decimal clock

3.2 Discussion of the decimal clock example

This example implements a clock program that displays the time in
thousandths of the hour, rather than the more usual hours, minutes and
seconds.  (Such a display might be useful, for instance, to a
consultant who bills time in tenth hour increments.) The code for this
example is contained in the file named dclock.c.

3.2.1 Initialization and event loop routines.  As in the first example
, the main() function to dclock begins with a call to Et_Init() and
ends with a call to Et_MainLoop(), with setup code in between.  If you
didn't see it before, note here that the Et_Init() function takes two
arguments -- a pointer to an integer that is the number of parameters
to the program, and a pointer to an array of pointers to strings that
are the program parameters.  Note especially that the first argument
is passed by reference, not by value.  The Et_Init() function requires
these arguments so that it can detect and act upon command line
arguments related to the initialization of Tcl/Tk. Any such arguments
detected are removed from the argc and argv variables before Et_Init()
returns, so the rest of the program need not be aware of their
existence.  The arguments currently understood by Et_Init() are
-geometry, -display, -name and -sync.  The use and meaning of these
arguments is exactly the same as in the standard Tcl/Tk interpreter
program "wish".

3.2.2 The ET_PROC macro.  The main difference between dclock and the
first example is that the setup code for dclock has an "
ET_INSTALL_COMMANDS" macro and there is an "ET_PROC" function defined
after main().  Let's begin by describing the ET_PROC macro.

The ET_PROC macro is nothing more than a convenient shorthand for
creating new Tcl/Tk commands in C. To create a new Tcl/Tk command, one
writes ET_PROC followed by the name of the new command in parentheses
and the C code corresponding to the new command in curly braces.
Within a single ET source file there can be any number of ET_PROC
macros, as long as the command names defined are all unique.  The et2c
macro preprocessor translates the ET_PROC macro into a C function
definition that implements the command, so ET_PROC macros should only
be used in places where it is legal to write C function definitions.

3.2.2.1 Parameters to an ET_PROC function.  The function created by an
ET_PROC macro has four parameters, though only two are commonly used.
The two useful parameters are argc and argv, which are the number of
arguments to the Tcl/Tk command and the value of each argument.  (The
command name itself counts as an argument here.) Hence, the argc and
argv parameters work just like the first two parameters to main() in a
typical C program.  Another parameter to every ET_PROC function is the
pointer to the Tcl/Tk interpreter, interp.  This variable is exactly
equal to the global variable Et_Interp.  The last parameter is called
clientData and is defined to be a pointer to anything.  It actually
points to the structure that defines the main window of the
application, and is therefore the same as the global variable
Et_MainWindow.

                              **Image**
        3.2 Summary of the parameters to each ET_PROC command

3.2.3 The ET_INSTALL_COMMANDS macro.  The ET_PROC macro will create a
C function that can be used as a Tcl/Tk command, but that function and
the corresponding command name must still be registered with the
Tcl/Tk interpreter before the command can be used.  This is the job of
the ET_INSTALL_COMMANDS macro.  Thus, in the dclock example, we must
invoke the ET_INSTALL_COMMANDS macro to register the UpdateTime
command prior to using the the UpdateTime command in any Tcl script.
Because new Tcl/Tk commands must be registered before they are used,
the ET_INSTALL_COMMANDS macros are usually the first setup code to
follow the Et_Init() function call.

Each instance of an ET_INSTALL_COMMANDS macro registers all ET_PROC
commands defined in a single source file.  The dclock example has only
a single ET_PROC command, but even if it had had 50, a single
ET_INSTALL_COMMANDS macro within the main() function would have been
sufficient to install them all.

The name of the source file containing the ET_PROC commands that are
to be registered is given as an argument to the ET_INSTALL_COMMANDS
macro.  If no argument is given, then the name of the file containing
the ET_INSTALL_COMMANDS macro is used.  Hence, the line in the dclock
example that registers the UpdateTime command can be written in either
of the following ways:


  ET_INSTALL_COMMANDS;

  ET_INSTALL_COMMANDS( dclock.c );

Note that the ET_INSTALL_COMMANDS macro does not actually open or read
the file named in its argument.  The macro just mangles the file name
in order to generate a unique procedure name for its own internal use.
The file itself is never accessed.  For this reason, the file name
specified as an argument to the ET_INSTALL_COMMANDS macro should not
contain a path, even if the named file is in a different directory.

3.2.4 The ET() macro.  We have already considered the ET() macro once,
in connection with the setup code for the "Hello, World!" example, and
we also observe that the ET() macro reappears in the setup code for
dclock and in the UpdateTime function.  Let's look at this macro in
more detail.

An ET() macro works just like a function, except that its argument is
a Tcl/Tk script instead of a C expression.  When an ET() macro is
executed, its argument is evaluated by the Tcl/Tk interpreter and an
integer status code is returned.  The status code will be either ET_OK
if the script was successful, or ET_ERROR if the script encountered an
error.  (An ET() macro might also return TCL_RETURN, TCL_BREAK, or
TCL_CONTINUE under rare circumstances.)

In the dclock example, a single ET() macro is used to initialize the
display of the decimal clock.  Three Tcl/Tk commands are contained
within the macro.  The first command creates a label widget for use as
the clock face, the second packs this label, and the third calls the
ET_PROC command named UpdateTime to cause the time on the clock face
to be updated.  (The UpdateTime command will arrange to call itself
again after a fixed interval, in order to update the time to the next
thousandth of an hour.)

The Tcl/Tk script contained in an ET() macro executes at the global
context level.  This means that the Tcl/Tk code within an ET() macro
can create and access only global Tcl/Tk variables.

3.2.4.1 The %s() phrase within an ET() macro.  Now consider the ET()
macro contained in the UpdateTime function.  The role of this macro is
to first change the label on the .x label widget to be the current
time and then reschedule the UpdateTime command to run again in 3.6
seconds.  The time value is stored in the character string buf[].
Within the argument to the ET() macro, the special phrase %s(buf)
causes the contents of the character string stored in buf[] to be
substituted in placed of the %s(buf) phrase itself.  The effect is
similar to a %s substitution in the format string of a printf
function.  In fact, the statement



    ET( .x config -text %s(buf); after 3600 UpdateTime );


is logical equivalent to



    char buf2[1000];
    sprintf(buf2," .x config -text %s; after 3600 UpdateTime ",buf);
    Tcl_GlobalEval(Et_Interp,buf2);


except that with the ET() macro there is never a danger of overflowing
the temporary buffer buf2[].

3.2.4.2 Other substitution phrases within ET() macros.  The phrase
%s(...) is replaced by the string contents of its argument within an
ET() macro.  Similarly, the phrases %d(...) and %f(...) are replaced
by ASCII representations of the integer and floating point number
given by the expression in their arguments.  The names of the
substitution phrases are taken from similar substitution tokens in the
format string of the printf function.  Note, however, that option
flags, precision and field widths are not allowed in an ET() macro
substitution phrase, as they are in printf.  The phrase %3.7f is
understood by printf but is is not understood by ET(). In an ET()
macro the only allowed form of a substitution phrase is where the
format letter immediately follows the percent symbol.

The ET() macro supports an additional substitution phrase not found in
standard printf: the %q(...). substitution.  The %q() works just like
%s() with the addition that it inserts extra backslash characters into
the substituted string in order to escape characters of the string
that would otherwise have special meaning to Tcl/Tk. Consider an
example.



  char *s = "The price is $1.45";
  ET( puts "%q(s)" );


Because the %q(...) macro was used instead of %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.

                              **Image**
    3.3 Summary of substitution phrases understood by ET() macros

3.2.5 Variations on the ET() macro.  The ET() macro used in all
examples so far returns a status code indicating success or failure of
the enclosed Tcl script.  Sometimes, though, it is useful to have
access to the string returned by the Tcl script, instead of the status
code.  For these cases one can use the ET_STR() macro in place of ET()

The ET_STR() macro works just like ET() in most respects.  The sole
argument to ET_STR() is a Tcl/Tk script to which the usual %s(), %d(),
%f() and %q() substitutions are applied.  The difference between
ET_STR() and ET() is that ET_STR() returns a pointer to a
null-terminated string that is the result of the Tcl/Tk script if the
script was successful.  If the script failed, then ET_STR() returns a
NULL pointer.

It is very important to note that the string returned by ET_STR() is
ephemeral -- it will likely be deallocated, overwritten or otherwise
corrupted as soon as the next Tcl/Tk command is executed.  Therefore,
if you need to use this string for any length of time, it is a good
idea to make a copy.  In the following code fragment, the C string
variable entryText is made to point to a copy of the contents of an
entry widget named .entry.



  char *entryText = strdup( ET_STR(.entry get) );


It is not necessary to make a copy of the string returned by ET_STR()
if the string is used immediately and then discarded.  The following
two examples show uses of the ET_STR() macro where the result does not
need to be copied.  The first example shows a quick way to find the
width, height and location of the main window for an application:



  int width, height, x, y;
  sscanf(ET_STR(wm geometry .),"%dx%d+%d+%d",&width,&height,&x,&y);


The next example shows a convenient way to tell if a given widget is a
button:



  char *widget_name = ".xyz";
  if( strcmp(ET_STR(winfo class %s(widget_name)),"Button")==0 ){
    /* The widget is a button */
  }else{
    /* The widget is not a button */
  }


There also exist versions of the ET() macro that return an integer and
a floating point number: ET_INT() and ET_DBL(). These work much like
ET_STR() except that the returned string is converted to an integer or
to a double using the functions atoi() or atof().  The values 0 and
0.0 are returned if the Tcl/Tk script given in the argument fails or
if the returned string is not a valid number.

The ET_INT() and ET_DBL() macros are often used to read the values of
integer and floating point Tcl/Tk variables.  For instance, if Width
is a global Tcl/Tk variable containing an integer value, then we can
load that value into the integer C variable iWidth using the following
statement:



  iWidth = ET_INT( set Width );


The ET_INT() is also useful for recording the integer id number of an
object created on a Tcl/Tk canvas widget.  In the following example, a
line is created on the canvas widget .c and its id is recorded in the
integer C variable id.  Later, this id is used to delete the line.



  id = ET_INT( .c create line 100 100 200 200 -width 2 );
  /* ... intervening code omitted ... */
  ET( .c delete %d(id) );


The last example of the ET_INT() macro shows a convenient way to tell
if the X11 server is color or monochrome:



  if( ET_INT(winfo screendepth .)==1 ){
    /* The display is monochrome */
  }else{
    /* The display is color */
  }


                              **Image**
             3.4 Summary of variations on the ET() macro


4.  Example 3: fontchooser

As its name implies, the next example is a small utility program that
can be used to select X11 fonts.  The source code is contained in two
files, fontchooser.c and fontchooser.tcl.  We will look at the C code
first.

4.1 Source code to the font chooser


  /*
  ** This program allows the user to view the various fonts
  ** available on the X server.
  **
  ** Preprocess this file using "et2c" then link with "et.o".
  */
  #include "tk.h"     /* This automatically loads Xlib.h */

  void main(int argc, char **argv){
    Et_Init(&argc,argv);
    ET_INSTALL_COMMANDS;
    ET_INCLUDE( fontchooser.tcl );
    Et_MainLoop();
  }

  /* This function parses up font names as follows:
  **
  **         Font Family               Font size
  **    __________________________  ________________
  **   /                          \/                \
  **   -misc-fixed-medium-r-normal--10-100-75-75-c-60-iso8859-1
  **                                |   |  \___/    | \_______/
  **                                |   |    |      |     |
  **                                |   |    |      |     `-- Always as shown
  **                                |   |    |      |
  **             The point size ----'   |    |      `--- 10x average width
  **                                    |    |
  **              This field ignored----'    `--- Resolution in dots per inch
  **
  **
  ** If $name is a font name (the first 6 fields of the X11 font name)
  ** then this procedure defines the global variable $Font($name), giving
  ** it as a value a list of available font sizes in ascending order.
  ** Only fonts of a particular resolution are included.  By default, the
  ** resolution selected is 75dpi, but this can be changed by the
  ** argument to the command.
  **
  ** This command also creates global variable FontCount that holds the
  ** number of entries in the Font() array.
  */
  ET_PROC( FindFonts ){
    char **fontnames;    /* The names of all fonts in the selected resolution */
    int count;           /* Number of fonts */
    int i;               /* Loop counter */
    char pattern[400];   /* Buffer to hold a pattern used to select fonts. */
  
    if( argc==1 ){
      strcpy(pattern,"*-75-75-*-*-iso8859-1");
    }else if( argc==2 ){
      extern int atoi();
      int resolution = atoi(argv[1]);
      sprintf(pattern,"*-%d-%d-*-*-iso8859-1",resolution,resolution);
    }
    fontnames = XListFonts(Et_Display,pattern,1000,&count);
    ET(
      catch {unset Font}
      set FontCount 0
    );
    for(i=0; iresult = "Wrong # args";
      return ET_ERROR;
    }
    if( sscanf(argv[1],"%d-%*d-%*d-%*d-%*c-%d",&leftHeight,&leftWidth)!=2 ){
      interp->result = "First argument is not a font size";
      return ET_ERROR;
    }
    if( sscanf(argv[2],"%d-%*d-%*d-%*d-%*c-%d",&rightHeight,&rightWidth)!=2 ){
      interp->result = "Second argument is not a font size";
      return ET_ERROR;
    }
    result = leftHeight - rightHeight;
    if( result==0 ) result = leftWidth - rightWidth;
    sprintf(interp->result,"%d",result);
    return ET_OK;
  }

                              **Image**
          4.1 Typical appearance of the fontchooser program

4.2 Analysis of the fontchooser source code

As is the prior examples, the main() function for the fontchooser
begins and ends with calls to Et_Init() and Et_MainLoop().
Immediately following the Et_Init() call is an ET_INSTALL_COMMANDS
macro that registers the two commands FindFonts and FontSizeCompare
with the Tcl/Tk interpreter.

4.2.1 Using the argc and argv parameters to an ET_PROC function.  The
FindFonts routine is used to query the X server for the names of all
available fonts at a particular resolution specified by the argument
to the FindFonts routine.  If no resolution is specified (if the
FindFonts command is not given an argument in the Tcl/Tk script) then
the resolution defaults to 75 dots per inch.  The argc and argv
parameters are used to determine the number and value of arguments to
the FindFonts command.  The specified resolution is then used to
construct a search pattern for the fonts.



    if( argc==1 ){
      strcpy(pattern,"*-75-75-*-*-iso8859-1");
    }else if( argc==2 ){
      extern int atoi();
      int resolution = atoi(argv[1]);
      sprintf(pattern,"*-%d-%d-*-*-iso8859-1",resolution,resolution);
    }


4.2.2 Global variables defined by ET.  After creating a search
pattern, the The Xlib function XListFonts() is used find all fonts
that match that pattern.



    fontnames = XListFonts(Et_Display,pattern,1000,&count);


The first argument to XListFonts(), as in many Xlib functions, is a
pointer to a Display structure that defines the connection to the X
server.  The fontchooser program uses the convenience variable
Et_Display to fill this argument.  Et_Display is a global variable
defined in the et.o library and initialized to the active X connection
by the Et_Init() function.  The Et_Display variable is available for
use by any function that needs a Display pointer.

The ET system defines two global C variables besides Et_Display:
Et_Interp and Et_MainWindow.  The Et_Interp variable is a pointer to
the Tcl/Tk interpreter used by ET. This variable is very handy since
many routines in the Tcl/Tk library require a pointer to the
interpreter as their first argument.  The Et_MainWindow variable
defines the main window of the application, the window named "."
within Tcl/Tk scripts.  The main window is needed by a few Tcl/Tk
library routines, but is not as widely used as the other global
variables in ET.

All three global C variables in ET are initialized by the Et_Init()
routine and never change after initialization.

                              **Image**
                   4.2 Summary of global variables

4.2.3 Other actions of the FindFonts command.  After calling
XListFonts(), the FindFonts command splits each name into a "font
family" and a "font size".  For each font family, it creates an entry
in the global Tcl/Tk array variable Font with the font family name as
the index and a list of sizes for that font as the value.  A new entry
in the Font array is created, or else a new size is added to the list
of font sizes in that entry, by the following ET() macro:



      ET( 
        if {![info exists {Font(%s(nameStart))}]} {
          set {Font(%s(nameStart))} {}
          incr FontCount
        }
        lappend {Font(%s(nameStart))} {%s(cp)}
      );


After all fonts returned by XListFonts have been processed, the list
of sizes on each entry in the Font array variable is sorted by the
final ET() macro in the FindFonts command:



    ET(
      foreach i [array names Font] {
        set Font($i) [lsort -command FontSizeCompare $Font($i)]
      }
    );


4.2.4 Operation of the FontSizeCompare command.  The FontSizeCompare
command is used to sort into ascending order the font sizes listed in
a single entry of the Font array.  The only place it is used is on the
lsort command contained in the final ET() macro of the FindFonts
routine.

Unlike any previously described ET_PROC command, FontSizeCompare makes
use of the interp parameter.  Recall that the interp parameter is a
pointer to the Tcl/Tk interpreter, and is therefore always equal to
the global C variable Et_Interp.  Hence, one could have used the
Et_Interp variable in place of the interp parameter throughout the
FindSizeCompare function and obtained the same result.

4.2.5 Constructing the GUI for the fontchooser.  The Tcl/Tk code that
defines the GUI for the fontchooser is contained in a separate file
fontchooser.tcl.  A small portion of this file follows:



  # This code accompanies the "fontchooser.c" file.  It does most of the
  # work of setting up and operating the font chooser.

  # Title the font chooser and make it resizeable.
  #
  wm title . "Font Chooser"
  wm iconname . "FontChooser"
  wm minsize . 1 1

  # Construct a panel for selecting the font family.
  #
  frame .name -bd 0 -relief raised
     

... 136 lines omitted ...


  # Begin by displaying the 75 dot-per-inch fonts
  #
  update
  LoadFontInfo 75


When the script in the file fontchooser.tcl executes, it constructs
the listboxes, scrollbars, menu and menu buttons of the fontchooser,
and finally calls the LoadFontInfo function.  The LoadFontInfo command
is defined by a proc statement in the part of the fontchooser.tcl file
that was omitted from the listing.  The LoadFontInfo function calls
FindFonts and then populates the listboxes accordingly.

The interesting thing about this example is how the script in
fontchooser.tcl is invoked.  In the prior examples ("Hello, World!"
and dclock) the Tcl/Tk script that setup the application was very
short and fit into an ET() macro in the main() function.  This same
approach could have been taken with the fontchooser.  We could have
put the entire text of the Tcl/Tk script into a 152 line ET() macro.
But that is inconvenient.  It is much easier to use an ET_INCLUDE
macro.

4.2.6 The ET_INCLUDE macro.  An ET_INCLUDE macro is similar to a
#include in the standard C preprocessor.  A #include reads in an
external C file as if it were part of the original C code.  ET_INCLUDE
does much the same thing for Tcl/Tk code.  It copies an external
Tcl/Tk script into the original C program, and causes that script to
be executed when control reaches the macro.

An important characteristic of the ET_INCLUDE macro is that it loads
the external Tcl/Tk script into the C program at compile time, not at
run time.  This means that a copy of the Tcl/Tk script actually
becomes part of the resulting executable.  To clarify this point,
consider the difference between the following two statements:



  ET( source fontchooser.tcl );

  ET_INCLUDE( fontchooser.tcl );


Both statements causes the file named fontchooser.tcl to be read and
executed by the Tcl/Tk interpreter.  The difference is that in the
first statement, the file is opened and read in at run-time,
immediately before the contained script is executed.  This means that
the file fontchooser.tcl must be available for reading by the program
in order for the program to work correctly.  In the second case, the
file is opened and read when the program is compiled.  The only work
left to do at run-time is to pass the contained script to the Tcl/Tk
interpreter.  In the second statement, then, the file fontchooser.tcl
does not have to be available to the program for correct operation.

4.2.7 How the ET_INCLUDE macro locates files.  The external script
file specified by an ET_INCLUDE macro need not be in the same
directory as the C program containing the ET_INCLUDE for the include
operation to work.  If the external script is in a different
directory, however, the name of that directory must be specified to
the et2c macro preprocessor using one or more "-Idirectory" command
line switches.

The algorithm used by et2c to locate a file is to first check the
working directory.  If the file is not there, then look in the
directory specified by the first -I option.  If the file is still not
found, then search the directory specified by the second -I option.
And so forth.  An error is reported only when the file mamed in the
ET_INCLUDE macro is missing from the working directory and from every
directory specified by -I options.  Note that this is essentially the
same algorithm used by the C compiler to find files named in #include
preprocessor directives.

4.2.8 The -dynamic option to et2c.  In a deliverable program, it is
usually best to load external Tcl/Tk scripts at compile time so that
the scripts will be bound into a single executable.  However, during
development it is sometimes advantageous to load external Tcl/Tk
scripts at run-time.  To do so allows these scripts to be modified
without having to recompile the C code.

The -dynamic option on the command line of the et2c preprocessor will
causes ET_INCLUDE macros to read their files at run-time instead of at
compile-time.  In effect, the -dynamic option causes macros of the
form ET_INCLUDE(X) to be converted into ET(source X). Generally
speaking, it is a good idea to use the -dynamic option on et2c
whenever the -g option (for symbolic debugging information) is being
used on the C compiler.

4.2.9 Use of ET_INCLUDE inside the et.o library.  When Tcl/Tk first
starts up, it must normally read a list of a dozen or so Tcl scripts
that contain definitions of widget bindings and related support
procedures.  In the standard interactive Tcl/Tk interpreter wish,
these files are read a run-time from a standard directory.  In an ET
application, however, these startup files are loaded into the
executable at compile time using ET_INCLUDE macros.

Startup files are loaded into the Et_Init() function that is part of
the et.o library.  The relevant source code followings:



    /*
     * Execute the start-up Tcl/Tk scripts. In the standard version of
     * wish, these are read from the library at run-time.  In this version
     * the scripts are compiled in.
     *
     * Some startup scripts contain "source" commands.  (Ex: tk.tcl in
     * Tk4.0).  This won't do for a stand-alone program.  For that reason,
     * the "source" command is disabled while the startup scripts are
     * being read.
     */
     ET( rename source __source__; proc source {args} {} );
     ET_INCLUDE( init.tcl );
     ET_INCLUDE( tk.tcl );
     ET_INCLUDE( button.tcl );
     ET_INCLUDE( dialog.tcl );
     ET_INCLUDE( entry.tcl );
     ET_INCLUDE( focus.tcl );
     ET_INCLUDE( listbox.tcl );
     ET_INCLUDE( menu.tcl );
     ET_INCLUDE( obsolete.tcl );
     ET_INCLUDE( optionMenu.tcl );
     ET_INCLUDE( palette.tcl );
     ET_INCLUDE( parray.tcl );
     ET_INCLUDE( text.tcl );
     ET_INCLUDE( scale.tcl );
     ET_INCLUDE( scrollbar.tcl );
     ET_INCLUDE( tearoff.tcl );
     ET_INCLUDE( tkerror.tcl );
     ET( rename source {}; rename __source__ source );


It is because of these 17 ET_INCLUDE macros that the et.c file must be
preprocessed by et2c before being compiled into et.o.


5.  Example 4: etwish

The short code that follows implements the interactive Tcl/Tk shell "
wish" using ET:



  main(int argc, char **argv){
    Et_Init(&argc,argv);
    Et_ReadStdin();
    Et_MainLoop();
  }


This program illustrates the use of Et_ReadStdin() routine.  The
Et_ReadStdin() routine causes ET to monitor standard input, and to
interpret all characters received as Tcl/Tk commands.  This is, of
course, the essential function of the interactive Tcl/Tk shell.

The program generated by this code example differs from the standard
wish program in two important ways.

  *  In the example here, the Tcl/Tk startup scripts are bound to the
     executable at compile-time, but in the standard wish they are
     read into the executable at run-time.
  *  This example does not support the -f command line switch that
     will cause wish to take its input from a file instead of from
     standard input.


6.  Example 5: runscript

The next example implements a version of wish that takes its input
from a file instead of from standard input.  The file that is read
must reside in the same directory as the executable and must have the
same name as the executable but with the addition of a .tcl suffix.
For instance, if the executable that results from compiling the
following program is named fuzzy, then the result of executing fuzzy
is that the Tcl/Tk script found in the same directory as fuzzy and
named fuzzy.tcl is read and executed.



  void
  main(int argc, char **argv){
    Et_Init(&argc,argv);
    ET( source $cmd_dir/$cmd_name.tcl );
    Et_MainLoop();
  }


6.1 The $cmd_dir and $cmd_name variables

The operation of the runscript program depends on the existence of two
Tcl/Tk variables computed by Et_Init() and named cmd_dir and cmd_name.
The cmd_dir variable stores the name of the directory that holds the
currently running executable.  The cmd_name variables stores the base
name of the executable.  The cmd_name and especially the cmd_dir
variables are included as a standard part of ET in order to encourage
people to write programs that do not use hard-coded absolute
pathnames.

In most modern operating systems, a file can have two kinds of names:
absolute and relative.  An absolute pathname means the name of a file
relative to the root directory of the filesystem.  A relative
pathname, on the other hand, describes a file relative to some other
reference directory, usually the working directory.  Experience has
shown that it is generally bad style to hard-code absolute pathnames
into a program.

The cmd_dir variable helps programmers to avoid hard-coded absolute
pathnames by allowing them to locate auxiliary files relative to the
executable.  For example, if a program named acctrec needs to access a
data file named acctrec.db then it can do so be look for acctrec.db in
a directory relative to the directory that contains acctrec.  The
programmer might 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, so long as they are in the same position
relative to one another.

The runscript example demonstrates the use relative pathnames in this
way.  The executable for runscript locates and executes a Tcl/Tk
script contained in a file in the same directory as itself.  The name
of the script is the name of the executable with a ".tcl" suffix
appended.  Using this scheme, the executable and script can be renamed
and moved to different directories at will, and they will still run
correctly so long as they remain together and keep the same name
prefix.  Such flexibility makes a program much easier to install and
administer.


7.  Example 6: bltgraph

The next program will demonstrate how to use ET with an extension
package to Tcl/Tk, in this case the BLT extension.  The example is
very simple.  All it does is turn the graph2 demo which comes with the
BLT package into a stand-alone C program.  A real program would, of
course, want to do more, but this example serves to illustrate the
essential concepts.



  /*
  ** This program demonstrates how to use ET with
  ** extensions packages for Tcl/Tk, such as BLT.
  */
  #include 

  int main(int argc, char **argv){
    Et_Init(&argc,argv);
    if( Blt_Init(Et_Interp)!=ET_OK ){
      fprintf(stderr,"Can't initialize the BLT extension.\n");
      exit(1);
    }
    ET_INCLUDE( graph2 );
    Et_MainLoop();
    return 0;
  }


The bltgraph program starts like every other ET program with a call to
Et_Init().  This call creates the Tcl/Tk interpreter and activates the
standard Tk widget commands.  The second line of the program is a call
to Blt_Init().  The Blt_Init() function is the entry point in the BLT
library that initializes the BLT extension widgets and registers the
extra BLT commands with the Tcl/Tk interpreter.  Other extension
packages will have a similar initialization functions whose name is
the name of the extension package followed by an underscore and the
suffix Init.  The example program shows the initialization of a single
extension package, though we could just as easily have inserted calls
to the initialization routines for 10 different extensions, if our
application had the need.

After the BLT package has been initialized, the only other code before
the call to Et_MainLoop() is an ET_INCLUDE macro which reads in a
Tcl/Tk script named graph2.  This script is one of the demonstrations
that is included with the BLT distribution and contained in the demos
subdirectory.  In order for the et2c preprocessor to locate this
script, you will either have to copy it into the working directory, or
else put a -I option on the command line to tell et2c where the script
is found.

This example is included with the ET distribution, but the Makefile
does not build it by default.  If you want to compile this example,
first edit the Makefile to define the BLT_DIR macro appropriately,
then type make bltgraph.


8.  Other example programs

The standard ET distribution includes several more example programs
that are described briefly in the following paragraphs.

8.1 The bell program

The first additional example program is called bell.  This is a small
utility that can be used to change the pitch, duration and volume of
the console "beep".  Complete source code is contained in the single
file bell.c.

When run, the bell utility displays three horizontal sliders, one each
for pitch, duration and volume, and three buttons.  The user selects
the desired parameters for the "beep" on the sliders.  Pressing the "
test" button causes a beep to sound with the chosen parameters.
Pressing the "set" button tells the X server to use the chosen
parameters for all subsequent beeps.  The "quit" button is used to
exit the utility.

The bell program consists of the main() function and a single ET_PROC
function named bell.  The main() function creates the GUI for the
utility using 21 lines of Tcl/Tk code contained within a single ET()
macro.  The bell function is responsible for sounding the bell and
change the parameters of the bell tone using the XBell() and
XChangeKeyboardControl() Xlib functions.

8.2 The color program

The color utility is intended to aid the user in selected named
colors.  The sources code is contained in two files color.c and
color.tcl.

Upon startup, the color utility displays a wide color swatch across
the top of its main window.  On the lower left side of the window are
six sliders representing both the RGB and HSV color components of the
swatch.  The user can change the color of the swatch by moving these
sliders.  On the lower right are six smaller color labels showing the
named colors that are "closest" to the color shown in the main color
swatch.

The implementation of this utility is roughly four parts C to one part
Tcl. This is because several of the key algorithms, including the RGB
to HSV conversion routines and the procedures for finding the nearby
colors, are all implemented in C for speed.

                              **Image**
                 8.1 Screen shot of the color program

8.3 The perfmon program

The next example is a graphical CPU performance monitoring tool for
the Linux operating system called perfmon.  The source code for
perfmon is contained in two files called perfmon.c and perfmon.tcl.

When invoked, the perfmon utility displays a small window containing
three bar graphs labeled "Core", "Swap" and "CPU". Each bar graph is
continually updated to show the amount of usage of the corresponding
hardware resource.

8.3.1 Explanation of the perfmon display.  The "Core" graph shows how
much of main memory is in use.  The red part of the graph is that
portion of memory that contains the text, heap and stack of executing
programs.  The blue part of the graph shows main memory that is
currently being used as disk cache.  The green part of the graph
represents the amount of unused memory.

The "Swap" graph works like the "Core" graph except that it shows the
amount of swap space used instead of main memory.  There is no blue
line on the "Swap" graph since swap space is never used as disk cache.

The "CPU" graph shows in red the amount of time the CPU spent doing
actual work.  The blue part of the CPU graph is the amount of time the
CPU spend executing the operating system kernel.  The green part of
the graph represents the time the CPU was idle.

Double-clicking over any part of the perfmon utility brings up an
auxiliary window in which the user can change the frequency with which
the graphs are updated, and the time interval over which the values on
the graph are averaged.  By default, the update interval is about 10
times per second, which is barely noticeable on a Pentium, but tends
to overwhelm a 486.  Users of slower hardware may wish to change the
update interval to minimize the impact of this utility on system
performance.

The implementation of this utility pays careful attention to speed, so
as not to impose an unacceptable load on the system.  If nothing else,
the perfmon program demonstrates that it is possible to use Tcl/Tk in
a high-speed, performance critical application.

                              **Image**
                8.2 Screen shot of the perfmon program

8.4 The tkedit program

The two files tkedit.c and tkedit.tcl together implement an ASCII text
editor based on the Tcl/Tk text widget.  This editor features menu
options to dynamically change the width, height and font and for
cutting, copying, deleting and pasting text.  Within the editor, the
cursor can be moved by clicking with the mouse, pressing the arrow
keys on the keyboard, or by using the EMACS cursor movement control
sequences.

8.5 The tkterm program

The files getpty.c, tkterm.c and tkterm.tcl contain source code for a
vt100 terminal emulator.  You can use tkterm whereever you are now
using xterm.

The main window for tkterm is implemented using a Tcl/Tk text widget.
The main window, its associated scrollbar and a menu bar across the
top of the application are all coded by the Tcl/Tk script contained in
the file tkterm.tcl.  The C code in getpty.c handles the messy details
of opening a pseudo-TTY and attaching a shell on the other side.
(Most of this code was copied from the sources for the "rxvt" terminal
emulator program.) The bulk of the code for tkterm is contained in the
C file tkterm.c and is concerned with translating VT100 escape codes
into commands for manipulating the text widget.

The sources to tkterm are an example of a moderately complex
application using ET. The tkterm.c file contains 7 ET_PROC macros, 33
ET macros and numerious uses of other ET features.


9.  Compiling an ET application

The first step in compiling an application that uses ET is to compile
ET itself.  This is relatively easy as there are only two source code
files: et2c.c and et40.c. (All other C source files in the ET
distribution are example programs.) The et2c.c file is the source code
for the et2c preprocessor and the et40.c is the source code for the
et.o library.

Compile the et2c macro preprocessor using any ANSI or K&R C compiler.
The code makes minimal demands of the language, and should be very
portable.  Some systems my require a -I option on the compiler command
line, however, to tell the compiler where to find the include file
tcl.h. The following command assumes the source code to tcl is found
in /usr/local/src/tcl7.4



  cc -O -o et2c -I/usr/local/src/tcl7.4 et2c.c


The et.o library is generated from et40.c in two steps.  First you
must filter the source file et40.c using the et2c preprocessor.  The
output of et2c is then sent through the C compiler to generate et.o.
The et2c command will normally need two -I options to tell the
preprocessor where to look for the Tcl/Tk startup scripts.  If you are
not sure where the Tcl/Tk startup files are found on your system, you
can find out using the following command:



  echo 'puts $auto_path; destroy .' | wish


The default locations are /usr/local/lib/tcl and /usr/local/lib/tk.
Assuming this is where the startup files are on your system, then the
command to preprocess the et.c source file is the following:



  et2c -I/usr/local/lib/tcl -I/usr/local/lib/tk et40.c >et_.c


The output of the preprocessor now needs to be compiled using an ANSI
C compiler.  The et40.c source code makes use of the tk.h and tcl.h
include files, so it may be necessary to put -I options on the
compiler command line to tell the compiler where these files are
located.  The following command is typical:



  cc -c -o et.o -I/usr/local/src/tcl7.4 -I/usr/local/src/tk4.0 et_.c


Having compiled the et2c preprocessor and et.o library, compiling the
rest of the application is simple.  Just run each file through the
preprocessor and then compile the output as you normally would.  For
example, the source file main.c would be compiled as follows:



  et2c main.c >main_.c
  cc -c -o main.o main_.c
  rm main_.c


The final rm command is just to clean up the intermediate file and is
not strictly necessary.

After all C source files have been compiled into .o files, they can be
linked together, and with the Tcl/Tk library using a command such as
the following:



  cc -o myapp main.o file1.o file2.o et.o -ltk -ltcl -lX11 -lm


This example links together the files main.o, file1.o and file2.o into
an executable named myapp.  All ET applications must be linked with
et.o and the Tcl/Tk libraries.  The Tcl/Tk libraries require, in turn,
the X11 library and the math library.  On some systems it may be
necessary to include one or more -L options on the command line to
tell the linker were to find these libraries.  Applications that use
other libraries or Tcl/Tk extension packages will probably need
addition -l switches.

The default action of the linker is usually to bind to shared
libraries for Tcl/Tk and X11 if shared libraries are available.  If
the executable is to be moved to other sites, where these libraries
may not be installed, it is best to force the use of static libraries
in the link.  The command-line option to achieve this is usually
-static or -Bstatic, though it varies from system to system.

9.1 Source file suffix conventions

We like to use the suffix .c for ET application source files even
though the files do not contain pure C code.  The reason is that ET
source code looks like C even if it isn't.  To files output from the
et2c preprocessor we give the suffix _.c.

Other users have reported that they don't like to use the .c suffix on
ET source files since it implies that the file can be directly
compiled using the C compiler.  They prefer a different suffix for the
ET source, and reserve the .c suffix for the output of the et2c
preprocessor.  Like this:



  et2c main.et >main.c
  cc -c -o main.o main.c
  rm main.c


The method you use is purely a matter of personal preference.  The
et2c preprocessor makes no assumptions about file names.  Most C
compilers, however, require that their input files end with .c so be
sure the output of et2c is written to a file with that suffix.

9.2 Compiling using an older K&R C 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 will need to first
give a -K+R option to et2c and then give a -DK_AND_R option to the C
compiler.  Like this:



  et2c -K+R -I/usr/lib/tcl -I/usr/lib/tk et.c >et_.c
  cc -DK_AND_R -I/usr/src/tcl7.4 -I/usr/src/tk4.0 -c -o et.o et_.c


When compiling application code using 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.

9.3 Where to store the ET files

The source code to the et2c preprocessor and the et.o library is small
-- less than 2100 lines total, including comments.  For that reason,
we find it convenient to include a copy of the sources in the source
tree for projects that use ET. The makefiles for these projects
includes steps to build the preprocessor and library as a precondition
to compiling the application code.  In this way, we never have to "
install" ET in order to use it.  This also allows the source tree to
be shipped to another site and compiled there without having to ship
ET separately.


10.  Summary and conclusion

The ET system provides a simple and convenient mechanism for combining
a Tcl/Tk based graphical user interface and a C program into a single
executable.  The system gives a simple method for calling Tcl/Tk from
C, for generating new Tcl/Tk commands written in C, and for including
external Tcl/Tk scripts as part of a C program.

ET is currently in use in several large-scale (more than 100000 lines
of code) development efforts, and is proving that it is capable of
providing an easy-to-use yet robust interface between Tcl/Tk and C.


11.  Acknowledgments

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


12.  Author's Name and Address

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