Invoking Tcl commands from Cplusplus

RS: The mangled language name in the title is because spelling it C++ resulted in a misparse... See Wikit problems


BGE (9 January 2001) — Fixed problems with formatting so people can copy-paste code more-or-less directly.


KBK (8 January 2001) — If you're integrating Tcl and C++, then Cplusplus streams and Tcl channels may also be of interest.


willdye (2004-12-27) See also: cpptcl, C++, C++/Tcl, and many others.


 Date: 01/06/2001 (on comp.lang.tcl)
 Author: Benoit Goudreault-Emond

Just thought I'd share the following small bit of code I've had lying around for quite some time. It's an adaptation of Brent Welch's “TclFastInvoke” routine from his book into C++ land. In a nutshell, it allows direct invocation of Tcl and Tk library routines without going through the interpreter. This has two advantages:

  1. It's faster (direct function call instead of indirect through interpreter)
  2. It totally eliminates quoting issues. Sometimes, you just want to pass a plain string to the command without needing to add backslashes everywhere; this is where this can be handy.

It's part of the Voodoo UML Class Diagram Editor ( http://voodoo.sourceforge.net ) and may be useful to people who want to use Tcl/Tk the same way it's done in there.

It requires an ANSI C++ compliant compiler; if you're compiling under VC++, you should #define WIN32 before compilation because VC++ is broken...

Example of usage:

     Tcl_Interp interp;
     // ...
  
     TclCommander cmd(interp);
     // build command list
     cmd("font") << "metrics" << "Times 12" << "-fixed";
     if(cmd.invoke() != TCL_OK)
         return TCL_ERROR;
     if(*Tcl_GetStringResult(interp) == '1')
         // ...

The above code snippet is the equivalent of calling

 [font metrics {Times 12} -fixed] 

in Tcl and testing the return value. Not very useful, but you can see how useful it could be in general.

The << operator “pushes” arguments in the argument list; you can pass strings, doubles and ints at the moment. It should be relatively easy to extend to other types as well. The invoke() member function calls the command with the given arguments.

This was roughly twice as fast as going through Tcl_Eval() in my informal tests.

Hope this is of some use! Feel free to improve on it — the code below can be considered Public Domain. I don't expect to improve it further at this time, as it does enough for what I use it for, but it could no doubt become a more generic Tcl/C++ binding.


tclbuf.h:

/* -*- C++ -*- */
#ifndef TCLBUF_H
#define TCLBUF_H
  
/*
 * tclbuf.h -- Houses a fast TCL invocation engine.
 *
 * $Id: 1133,v 1.7 2006-01-19 07:00:09 jcw Exp $
 */
  
/** 
  This class allows fast invokes -- without having to convert all
  your junk to const char*.
 */
  
#include <string>
using std::string;
  
#include <tcl.h>
  
#ifdef WIN32
#define defaultCapacity 10
#endif // WIN32
  
 class TclCommander
 {
#if !defined(WIN32)
     static const size_t defaultCapacity = 10;
#endif // !defined(WIN32)
 public:
     TclCommander(Tcl_Interp* interp) :
         argc(0), objv(objv1), objvSize(defaultCapacity),
         isCmdCached(false),
         interp_(interp)
     { }
     ~TclCommander()
     {
         reset();
         if(objv != objv1)
             delete[] objv;
     }
  
     /// sets command and checks if it exists at the same time. Returns false
     /// if the command is nonexistent.
     bool command(const char* cmd);
     bool command(const string& cmd);
  
     /// append different arguments.
     void push(const char* arg)
     {
         // the const_cast is ugly, but it's required only because of
         // the stoopid way Tcl has its header files done.  Normally,
         // the string is not really modified.
         push(Tcl_NewStringObj(const_cast<char*>(arg), strlen(arg)));
     }
  
     void push(const string& arg)
     {
         // const_cast required for reasons given above.
         push(Tcl_NewStringObj(const_cast<char*>(arg.data()), arg.length()));     
     }
  
     void push(int arg)
     {
         push(Tcl_NewIntObj(arg));
     }
  
     void push(long arg)
     {
         push(Tcl_NewLongObj(arg));
     }
  
     void push(double arg)
     {
         push(Tcl_NewDoubleObj(arg));
     }
  
     void push()
     {
         push(Tcl_NewObj());
     }
  
     void push(Tcl_Obj* arg);
     /// invokes the command.  Returns the result from the command.
     int invoke();
     /// just a convenience binding to use the commander as a function
     /// object.
     int operator() () 
     {
         return invoke();
     }
  
     /// resets the current arguments for a new run.  This also nulls the
     /// command out, so you'll have to call command() again.
     void reset();
  
     //**************** OPERATORS ****************
     /// append different types of objects
     /// I'd use templates here, but it would slow compilation
     TclCommander& operator<< (const char* arg)
     {
         push(arg);
         return *this;
     }
     TclCommander& operator<< (const string& arg)
     {
         push(arg);
         return *this;
     }
     TclCommander& operator<< (int arg)
     {
         push(arg);
         return *this;
     }
     TclCommander& operator<< (long arg)
     {
         push(arg);
         return *this;
     }
     TclCommander& operator<< (double arg)
     {
         push(arg);
         return *this;
     }
     /// a shortcut for command
     TclCommander& operator() (const char* cmd)
     {
         ASSERT(command(cmd));
         return *this;
     }
     TclCommander& operator() (const string& cmd)
     {
         ASSERT(command(cmd));
         return *this;
     }
 private:
     /// default vector, to avoid ugly calls to new[] in most cases 
     Tcl_Obj* objv1[defaultCapacity];
     int argc;
  
     Tcl_Obj** objv;
     int objvSize;
  
     bool isCmdCached;
     Tcl_CmdInfo cmdInfo;
  
     Tcl_Interp* interp_;
  
     bool command(const char* cmd, size_t length);
 };
  
#endif

tclbuf.cc:

#include <iostream>
#include <vector>
#include <algorithm>
#include <string>
#include <cstring>
#include <stdarg.h>
#include "tclbuf.h"
  
using namespace std;
  
 bool TclCommander::command(const char* cmd, size_t length)
 {
     char* temp = new char[length+1];
     copy(cmd, cmd+length, temp);
     temp[length] = 0;
     bool retval = command(temp);
     delete[] temp;
     return retval;
 }
  
 bool TclCommander::command(const char* cmd)
 {
     // make sure we kill previous commands if necessary
     if(isCmdCached)
         reset();
     // get info; we must allocate a temporary for that
     if(!Tcl_GetCommandInfo(interp_, const_cast<char*>(cmd), &cmdInfo))
         return false;
     objv[argc++] = Tcl_NewStringObj(const_cast<char*>(cmd),
                                     strlen(cmd));
     isCmdCached = true;
     return true;
 }
  
 bool TclCommander::command(const string& cmd)
 {
     return command(cmd.c_str());
 }
  
 void TclCommander::push(Tcl_Obj* arg)
 {
     if(argc >= objvSize)
     {
         // we just ran out of space in the array.  Alloc 2*objvSize.
         // this makes array grow exponentially; it's the usual
         // tradeoff also found in most implementations of std::vector<>
         Tcl_Obj** temp = new Tcl_Obj* [objvSize*2];
         copy(objv, objv+objvSize, temp);
         objvSize *= 2;
         if(objv != objv1)
             delete[] objv;
         objv = temp;
     }
     Tcl_IncrRefCount(arg);
     objv[argc++] = arg;
 }
  
 int TclCommander::invoke()
 {
 // NOTE: you should *not* use cout in normal Tcl programs--
 // output through channels instead.  Following code only
 // illustrates how to put a trace on the system.
#ifdef DEBUG
     cout << "=> " << Tcl_GetStringFromObj(objv[0],NULL) << endl;
     for(int i = 1;i < argc;++i)
         cout << "--> " << Tcl_GetStringFromObj(objv[i],NULL) << endl; 
#endif
     return (*cmdInfo.objProc)(cmdInfo.objClientData, interp_, argc, objv);
 }
  
 void TclCommander::reset()
 {
     for(int i = 0;i < argc;++i)
         Tcl_DecrRefCount(objv[i]);
     argc = 0; isCmdCached = false;
 }

Benoit Goudreault-Emond -- WWW: http://www.crosswinds.net/~bge CoFounder, KMS Group ; Programmer, Silanis Technology (http://www.silanis.com )

Note: the "From:" address is not correct to protect myself against spam. My actual e-mail address is: bge AT crosswinds DOT net


AMG: The above code is LGPL, as is Voodoo. Wait, on second thought, maybe it isn't, since it's derived from Brent Welch's book.


RFox: May 31, 2012 - A C++ class library that encapsulates a pretty good chunk of Tcl's API has been part of a few products I've written for NSCL for quite some years. A summary of the library is: http://docs.nscl.msu.edu/daq/ringbuffer/c2407.html Detailed reference pages are at http://docs.nscl.msu.edu/daq/ringbuffer/index.html - search for CTCL and you'll find the manpages for the various classes that make up the beast. I can make this more generally available (GPL sorry but that's what the MSU board of trustees and I agreed on) if there's a desirement.