Extending Tcl
Clif Flynt, in Tcl/Tk (Third Edition), 2012
15.5 Embedding the Tcl Interpreter
The most common use of Tcl is as an interpreter with extensions to add functionality. The most common commercial use of the Tcl interpreter is to embed it within a C, C\# or C++ application. In this architecture, the mainline code is compiled and the Tcl interpreter is invoked to run specific scripts—to read a configuration file, display a GUI, to allow a user to customize behavior, etc.
This technique is very powerful, since it provides you with the speed and power of a compiled language with the versatility and ease of customization of an interpreted language.
Examples of this style of programming include most of the high-end CAD and EDA packages, Cisco's IOS, the TuxRacer game, the sendmail tclmilter and the Apache Rivet package. These applications define configuration options and simple operations in a Tcl script file and then perform the CPU-intensive operations within a compiled section.
Embedding the Tcl interpreter within a C application requires a few items:
- 1.
The Tcl include files (tcl.h, tk.h).
- 2.
The Tcl library for the target system (libTcl.a, libTcl.so, libTcl.dll.)
- 3.
Some support code in the application to link to the Tcl interpreter.
The include files and libraries are included with the standard Tcl distributions, or they will be created if you compile Tcl and Tk from source (described in the next section).
Embedding Tcl or Tk into a C or C++ application uses these functions:
Tcl_FindExecutable | Fills internal Tcl variable that is used by info nameofexecutable. This is not absolutely required, but is good to include. |
Tcl_CreateInterp | Creates a new interpreter. After this command has been completed the new interpreter can be used for some applications. The compiled commands will be available, but Tcl commands that are implemented in scripts (for example, parray) will not be loaded until Tcl_Init has been called. |
Tcl_Init | Loads the Tcl initialization scripts to fully initialize the interpreter and create all standard commands. |
Tk_Init | Loads the Tk initialization scripts. This is only valid if you have linked the Tk interpreter into your application. |
Tcl_Eval | Evaluates a Tcl script and returns TCL_OK or TCL_FAIL. |
Tcl_Exit | Clean up and exit. |
The Tcl_FindExecutable and Tcl_CreateInterp function calls can be done in any order. Both of these should be done before calling the Tcl_Init or Tk_Init functions. The Tcl_Init function should be invoked before Tk_Init. The Tcl_Eval function should not be called until the previous functions have returned.
The first two function calls when initializing an embedded Tcl interpreter should be Tcl_FindExecutable and Tcl_CreateInterp. These two calls can be done in either order, but both should be done before calling Tcl_Init.
Syntax: void Tcl_FindExecutable (argv0)
Determines the complete path to the executable. If the argv0 argument includes a rooted path that is used, else the current working directory and argv[0] value are used.
argv0 | C and C++ applications pass the command line arguments as an array of strings. The first element of this array will be the name of the executable. |
Syntax: Tcl_Interp *Tcl_CreateInterp ()
Creates a new interpreter and defines the basic commands.
The Tcl_CreateInterp command returns a pointer to the new Tcl interpreter. This is actually a pointer to the Tcl interpreter state structure. Multiple interpreters share the same code base, but each have their own state structure.
This interpreter pointer is passed to most Tcl library functions.
The Tcl_Init and Tk_Init functions both require the tt Tcl_Interp pointer.
Syntax: int Tcl_Init (interp)
Initializes the Tcl Interpreter. This involves loading and evaluating the init.tcl script and other scripts in the library.
interp | A pointer to the Tcl_Interp structure returned by a call to Tcl_CreateInterp. |
The Tcl_Init and Tk_Init functions return an integer return that conforms to normal C conventions–a ‘‘0" (TCL_OK) indicates success and a non-zero return (TCL_FAIL) indicates an error. Information about the failure can be obtained by using Tcl_GetVar to retrieve the contents of the errorInfo global variable.
After an interpreter has been created and initialized your application can send scripts to be evaluated. These scripts can be simple Tcl commands, or long and complex scripts. There are several flavors of the Tcl_Eval command depending on whether the script is already in a Tcl object, a file, or included in the code as a string. The most basic function is Tcl_Eval.
Syntax: Tcl_Eval (interp, string)
Evaluate a string of Tcl commands within an interpreter.
interp | A pointer to the Tcl_Interp structure returned by a call to Tcl_CreateInterp. |
string | A printable string consisting of one or more Tcl commands. |
The last step is to invoke Tcl_Exit to exit your application. This is preferred over the normal exit function because it will do a bit more cleanup of the Tcl interpreter and make sure no data is left in limbo.
Syntax: Tcl_Exit (status)
Clean up the Tcl interpreter state and exit.
status | An integer status to return to the OS. The standard is to return 0 for a success and non-zero for some exception exit. |
The next example is a minimal main.c which will create and initialize a Tcl interpreter and then load and evaluate the commands in a file.
main.c
#include <stdlib.h>
#include <tcl.h>
#include <tk.h>
main(int argc, char *argv[]) {
// Tcl ‘glue’ variables
Tcl_Interp *interp; /* Interpreter for application. */
int rtn;
// Create the interp and initialize it.
Tcl_FindExecutable(argv[0]);
interp = Tcl_CreateInterp();
if (Tcl_Init(interp) == TCL_ERROR) {
return TCL_ERROR;
}
if (Tk_Init(interp) == TCL_ERROR) {
return TCL_ERROR;
}
// Run the Tcl script file - hardcoded for myScript.tcl
rtn = Tcl_Eval(interp, "source myScript.tcl");
if (rtn != TCL_OK) {
printf("Failed Tcl_Eval: %d \n%s\n", rtn,
Tcl_GetVar(interp, "errorInfo", TCL_GLOBAL_ONLY));
exit(-1);
}
Tcl_Exit(0);
}
In the previous example, the script performs all the operations. It might load another extension, create a GUI, create a web server or anything else.
If you're embedding Tcl within a set of compiled code, you probably have a set of compiled functions that you'd like the Tcl scripts to have access to. These can be built into the executable, rather than loaded from a compiled library, as is done with extensions.
The functions described in the previous sections about building a Tcl extension are also used to create new commands to be used with an embedded interpreter.
The next example shows a main.c with a new command being created. The new command is factorcount, which finds the number of factors a value has by dividing the value by all smaller integers to see which will divide it evenly. This function has no use except that it chews up fewer CPU cycles when compiled than it does when interpreted.
main.c with new command
#include <stdlib.h>
#include <math.h>
#include <tcl.h>
#include <tk.h>
#include "./factorcountInt.h"
main(int argc, char *argv[]) {
// Tcl ‘glue’ variables
Tcl_Interp *interp; /* Interpreter for application. */
int rtn;
// Create the interp and initialize it.
interp = Tcl_CreateInterp();
Tcl_FindExecutable(argv[0]);
if (Tcl_Init(interp) == TCL_ERROR) {
return TCL_ERROR;
}
if (Tk_Init(interp) == TCL_ERROR) {
return TCL_ERROR;
}
// Add the factorcount command
Tcl_CreateObjCommand(interp, "factorcount",
Factorcount_Cmd, (ClientData) NULL, NULL);
// Run the Tcl script file - hardcoded for myScript.tcl
rtn = Tcl_Eval(interp, "source myScriptFactor.tcl");
if (rtn != TCL_OK) {
printf("Failed Tcl_Eval: %d \n%s\n", rtn,
Tcl_GetVar(interp, "errorInfo", TCL_GLOBAL_ONLY));
exit(-1);
}
}
The Factorcount_Cmd function is mostly code to check that the Tcl command was called correctly. In a real application there would probably be more logic than checking that the script writer used the right parameters.
factor.c
#include "./factorcountInt.h"
int Factorcount_Cmd(ClientData factorcount,
Tcl_Interp *interp,
int objc,
Tcl_Obj * CONST *objv) {
/* ClientData factorcount; /* Not used. */
/* Tcl_Interp *interp; /* Current interpreter. */
/* int objc; /* Number of arguments. */
/* Tcl_Obj *CONST objv[]; /* Argument objects. */
// Tcl variables
int result;
Tcl_Obj *returnValue;
// function variables
int product; // Product to find factors for - from user
int count; // Count of factors
int i; // loop variable
int f2; // Temporary factor variable
// Assume everything will be ok
result = TCL_OK;
/*
* Initialize the return value
*/
returnValue = NULL;
/*
* Check that we have a command and number
*/
if (objc < 2) {
Tcl_WrongNumArgs(interp, 1, objv,
"factorCount number");
return TCL_ERROR;
}
if (TCL_OK !=
Tcl_GetIntFromObj(interp, objv[1], &product)) {
result = TCL_ERROR;
goto done;
}
count = 0;
for (i=1; i < product; i++) {
f2 = product/i;
if (f2*i == product) {
count++;
}
}
returnValue = Tcl_NewIntObj(count);
done:
/*
* Set the return value and return the status
*/
if (returnValue != NULL) {
Tcl_SetObjResult(interp, returnValue);
}
return result;
}
The factorcountInt.h file is mostly the previously described boilerplate with these lines to define the new command:
factorcountInt.h
/***
extern "C" {
***/
EXTERN EXPORT(int,Factorcount_Cmd) _ANSI_ARGS_
((ClientData, Tcl_Interp *, int, Tcl_Obj * CONST *));
/***
}
***/
The example script to go with this script will create a small GUI and invoke the factorcount command to evaluate the user input.
Script Example
set done 0
label .l1 -text "Enter Number:"
entry .e1 -textvar product
button .b1 -text "go" -command {set answer [factorcount $product]}
button .b2 -text "done" -command {set done 1}
label .l2 -text "Factors:"
label .l3 -textvar answer
grid .l1 .e1
grid .b1 .b2
grid .l2 .l3
vwait done
Script Output
The script uses vwait to force the script to run until the user exits. An application that intends to be GUI driven might follow Tcl_Eval with a call to Tcl_MainLoop to enter the event loop and stay there until the interpreter exits.
Syntax: Tk_MainLoop ()
Calls the event loop processing code repeatedly. Returns when the last window is destroyed.