Adding Tcl/Tk to a C application

Introduction

The following questions tend to be asked regularly on the comp.lang.tcl newsgroup:

  • I have a large C/C++ program- how do I make it scriptable with Tcl?
  • I have a large C/C++ program- how do I add a Tk GUI to it?

At a high level, there are three different solutions to consider.

  1. Embed Tcl calls in my C/C++ code.
  2. Wrap my C/C++ code to make it callable from Tcl.
    Replace the C/C++ main() with a tclsh/wish main program
  3. Something else

Embed Tcl Calls in C/C++ code

While the text "Why adding Tcl calls to a C/C++ application is a bad idea" is an interesting dialog concerning the appropriate approach to performing this task, where can one find specific coding examples and documentation regarding what one needs to do, in a C program, if one needs to create an interpreter and then execute tcl commands making use of that interpreter.

Well, one example comes out of the box with both Tcl and Tk. Take a look at the tcl/unix/tclAppInit.c (or tcl/win/tclAppInit.c) files in the tcl source distribution (which is the mainline C module for the tclsh command), and in the Tk source distribution, tk/unix/tkAppInit.c or tk/win/winMain.c (I don't understand why the name difference here...). For MacOS, see tk/mac/tkMacAppInit.c and tk/macosx/tkMacOSXAppInit.c (again, I don't understand the reasoning for file name changes). These provide at least a basic skeleton for initializing an interpreter. Unfortunately, they leave you in an interpretive mode which most people don't want to happen. So another example is needed.

I do know there are two kinds of Tcl actions one can invoke from C/C++ Since Tcl is just a C library, some Tcl actions can be invoked by calling the appropriate Tcl function call.

However, some things in Tcl must be done by invoking Tcl_Eval, after appropriately setting up a Tcl interpreter.

Can someone provide some sample C or C++ code that shows setting up the interpreter, then perhaps invoking things each way?

A dream scenario would do this for Tcl/Tk since that is a superset of the same kind of request for Tcl...


First Example of invoking Tcl

AM Here is some (massaged) code that I use in one application. The idea is:

  • Set up a basic interpreter
  • Register the specific commands
  • Make sure that the interpreter is available when a scripted task is called

It does not use the Tcl script library, only the basic built-in commands are available, but this is a very simple set-up after all.

The function InitScript() is used to set up a script library private to the application and to get a functioning Tcl interpreter. The function EvalScriptCommand() is used elsewhere and simply wraps the details so that I do not need to use Tcl routines everywhere.

 #include <stdlib.h>

 /*
  * Static global data
  */

 static Tcl_Interp * tcl_interp ; /* Script interpreter */

 int
 InitScript(
    void                         /* Nothing */
   )                               /* Return okay or not */
 {
    char       * pstr   ;
    FILE       * infile   ;
    int          retval   ;
    int          rc       ;
    size_t       filesize ;


    tcl_interp = Tcl_CreateInterp() ;

    if ( tcl_interp == NULL )
    {
       fprintf( stderr, "Could not create interpreter!\n" ) ;
       return 1 ;
    }
    /* Register the commands specific to my application


    Tcl_CreateObjCommand( tcl_interp, "session", GppSessionCmd,
       (ClientData) NULL, GppDummyDestroy ) ;

    /* Read the configuration file with specific script code
       (in reality, I use a function that searches for the file and opens it!
       Hence I need to do more work ... If you have the file name, then use
       Tcl_EvalFile()!)
    */
    infile = fopen( "scripts.conf", "r" ) ;

    if ( infile == NULL )
    {
      return 1 ;
    }

    /* Now, read the whole file ...
    */
    fseek( infile, 0L, SEEK_END ) ;
    filesize = ftell( infile ) ;
    pstr     = (char * ) malloc( (filesize+1) * sizeof(char) ) ;
    if ( pstr == NULL )
    {
       return 1;
    }
    fseek( infile, 0L, SEEK_SET ) ;

    fread( pstr, filesize, 1, infile ) ;
    pstr[filesize] = '\0' ;

    rc = Tcl_Eval( tcl_interp, pstr ) ;

    if ( rc != TCL_OK )
    {
       fprintf( stderr, "Error loading script library\n" ) ;
       return 1 ;
    }

    free( pstr ) ;

    return 0;
 }

 /* Function for encapsulating the details
 */
 EvalScriptCommand(
   char *  command )
 {
    if ( Tcl_Eval( tcl_interp, command ) == TCL_OK )
    {
       return 0 ;
    }
    else
    {
       return 1 ;
    }
 }
//compile with g++ and run using ./exec

Second Example of Invoking Tcl from a C Application

David Gravereaux writes, on comp.lang.tcl, this code fragment for invoking Tcl from a C program:

Tcl_Interp *interp; 

int Init (char *argv0) 
{ 
    Tcl_FindExecutable(argv0); 
    interp = Tcl_CreateInterp(); 
    if (Tcl_Init(interp) != TCL_OK) { 
        return TCL_ERROR; 
    } 
    return TCL_OK; 
} 

void TearDown () {Tcl_Finalize();} 

int EvalFile (char *fileName) 
{ 
    return Tcl_EvalFile(interp, fileName); 
} 

If EvalFile() returns TCL_ERROR, get the error with Tcl_GetStringResult() .


Wrap C/C++ code to make it callable from Tcl

Lectus I've beeing experimenting with "GUIfying" C/C++ programs using Tcl/Tk. Here is how to do it (I tried to make the example simple so it's easy to understand):

C++ code:

#include <iostream>
#include <sstream>
#include <string>

// The procedure to provide the add function
void add(string x, string y)
{        
        int ix=0;
        int iy=0;
// We use stringstream to convert from string to int.
        std::stringstream ssx(x);
        std::stringstream ssy(y);
        ssx >> ix;
        ssy >> iy;
// Output the result to stdout.
        std::cout << ix+iy << std::endl;        
}

// We'll access C or C++ code by passing parameters to the command line program
int main(int argc, char **argv)
{
// Test if we have all args
        if (argc < 4)
        {
                std::cout << "Error" << std::endl;
                return 1;        
        }
// Get the operator and check to see if it's +
        std::string opt = argv[1];
        if (opt == "+")
        {
// Yes, it is, so call add function.
                add(argv[2],argv[3]);
        }        
        return 0;
}

compiled with $ clang++ test.cpp -o test

And here is the Tcl/TK code with the GUI part:

package require Tk

ttk::label .x -text "X: "
ttk::entry .ex -textvar x
ttk::label .y -text "Y: "
ttk::entry .ey -textvar y

ttk::button .badd -text "Add"
ttk::label .res -text "Result: "

grid .x .ex -padx 5 -pady 2.5
grid .y .ey .badd -padx 5 -pady 2.5
grid .res -padx 5 -pady 2.5

focus .ex

.badd config -command {
        if {[catch {open [list "|./test" + $x $y] "r"} fd]} {
                puts "Couldn't open pipe."
                exit                
        }
        set z [gets $fd]
        close $fd         
        .res config -text "Result: $z"
}

Note: Tested on FreeBSD and Linux. On windows you probably need to replace "./test" for "test".


See also