Chapter 6. libTcl++ and adding commands to SpecTcl

One of the important features of the Tcl scripting language is that it is easily extensible. Application specific commands can be added, not only as procs but also as native, compiled commands. The SpecTcl framework provides hooks for adding commands as SpecTcl starts.

In this chapter we will revisit our EVPSwitcher class from The API and the event processing pipeline. We'll wrap a command ensemble around an instance of that class so that we can use a Tcl command to dynamically switch between processing raw event files and filter files.

We'll have to start off with a bunch of background so that the example is understandable. Specifically we need to:

The Tcl++ provides class wrappers around many Tcl objects. Reference information is provided in the NSCLDAQ reference pages. Here we'll summarize key classes.

CTCLInterpreter. Wraps a Tcl Interpreter. This class is defined in TCLInterpreter.h Note that the API provides a method to return the interpreter used to execute SpecTcl commands. Note also that applications can have more than one interpreter.

The interpreter provides facilities to among other things:

Many Tcl objects require that they be bound to an interpreter to function properly. For example, a variable only makes sense within an interpreter as does a command. The base class for such objects is a CTCLInterpreterObject, defined in TCLInterpreterObject.h.

This class provides mechiansms to bind the object to an intepreter after construction and to get the interpreter to which the object is bound. Subclassses can make use of AssertIfNotBound which fails an assertion if the object is not currently associated with an interpreter.

Commands themselves are objects. Modern Tcl commands are encapsulated by CTCLObjectProcessor objects. These use the modern Tcl_Obj* interface to commands rather than the old argc/argv interface.

CTTCLObjectProcessor. is defined in TCLObjectProcessor.h. This class is a base class for actual commands and:

CTCLobject. Tcl 8.0 introduced dual port objects. These attempt to solve an efficiency problem that is due to Tcl's core concept that evertyhing can be treated as a string.

Prior to 8.0, variables were, in fact, stored as strings by the interpreter. Dual port objects provide the ability to maintain a second representation. This prevents costly conversions from strings to other types as needed. It also allows for optimized representations for types such as lists and dicts.

The Tcl type Tcl_Obj* is a pointer to an opaque type that represents the dual ported object. Tcl API methods allow you to manipulate these copy on write, reference counted objects in many ways. These objects are wrapped by CTCLObject objects. This class is defined in TCLObject.h.

Reference counting is managed automatically in assignment and copy construction methods. In addition, these objects:

The CTCLObjectProcessor derived objects used to represent commands pass the command line words to the command processor as a reference to a vector of CTCLObject objects.

With that background, start looking at the nitty gritty of wrapping our EVPSwitcher class in a command ensemble.

A command ensemble can be thought of as an object. It has a command name and methods that are represented as sub-commands. The sub commands are the first argument followig the command word. We're going to do a very minimal mapping providing the following subcommands:

save

Saves the current list of event processors

restore

Restores the current list of event processors from a previously saved set.

filter

Sets the event pipeline to be a filter processor.

This quite naturally gives us the following definition for our command processor:

Example 6-1. Definition file for event processor switcher.


#ifndef EVPSWITCHCOMMAND_H
#define EVPSWITCHCOMMAND_H
#include <TCLObjectProcessor.h>
#include "EVPSwitcher.h"
class CTCLInterpreter;
class CTCLObject;


class EVPSwitchCommand : public CTCLObjectProcessor
{
private:
  EVPSwitcher  m_switcher;                         (1)
public:
  EVPSwitchCommand(CTCLInterpreter& interp, const char* command);

  int operator()(CTCLInterpreter& interp, std::vector<CTCLObject>& objv); (2)

private:
  void save(CTCLInterpreter& interp, std::vector<CTCLObject>& objv);
  void restore(CTCLInterpreter& interp, std::vector<CTCLObject>& objv);  (3)
  void filter(CTCLInterpreter& interp, std::vector<CTCLObject>& objv);
};

#endif
            
        
(1)
This object is going to be manipulated by the command. This pattern is very common for Tcl extensions; First develop an API for the functionality we want implemented. In this case, that functionality is encapsulated in an object of type EVPSwitcher. Next develop a Tcl command that manipulates that API.

This pattern separates the concerns of what has to be done from processing the commands that do it. A clean separation of these concerns would allow, for example, SpecTcl and this extension to be migrated to a different host scripting language (such as Python for example).

(2)
CTCLObjectProcessor objects must implement a function call operator (operator()). This method is invoked by the Tcl++ library when the command registered by the object has been invoked.

The method is passed a reference to the interpreter that is executing the command and a vector of CTCLObject objects. The vector represents the command words that make up the Tcl command being executed. Note that element 0 of this vector is the command itself.

(3)
A common pattern in command ensembles is for top level processing (in this case operator()) to validate there's a subcommand and dispatch to a specific handler for each subcommand. We're going to follow that approach. These methods are the handlers for each subcommand.

Let's look at the constructor. The only responsibility it has is to register the command with the SpecTcl interpreter. The interpreter is passed in. We're also passing in the command name string in case circumstances force us to use a different command name.

Here's the constructor implementation, therefore:

Example 6-2. EVPSwitchCommand constructor implementation


#include "EVPSwitchCommand.h"
#include <TCLInterpreter.h>
#include <TCLObject.h>


EVPSwitchCommand::EVPSwitchCommand(CTCLInterpreter& interp, const char* command) :
  CTCLObjectProcessor(interp, command, true)
{}
        

As you can see the base class constructor does all the work. m_switcher is constructed by the default constructor for EVPSwitcher.

The function call operator must:

Example 6-3. Implementation of EVPSwitchCommand::operator()


int
EVPSwitchCommand::operator()(CTCLInterpreter& interp, std::vector<CTCLObject>& objv)
{
  bindAll(interp, objv);                                       (1)
  try {                                                        (2)
    requireAtLeast(objv, 2, "Command requires a subcommand");  (3)

    std::string subcommand = objv[1];                          (4)
    if (subcommand == "save") {
      save(interp, objv);
    } else if (subcommand == "restore") {
      restore(interp, objv);                                   (5)
    } else if (subcommand == "filter") {
      filter(interp, objv);
    } else {                                                  (6)
      throw std::string("Invalid subcommand must be one of 'save', 'restore', or 'filter'");
    }
  }
  catch (const char* msg) {
    interp.setResult(msg);
    return TCL_ERROR;
  }
  catch (std::string msg) {
    interp.setResult(msg);
    return TCL_ERROR;
  }
  catch (CException& e) {                                 (7)
    interp.setResult(e.ReasonText());
    return TCL_ERROR;
  }
  catch (std::exception& e) {
    interp.setResult(e.what());
    return TCL_ERROR;
  }
  catch (...) {
    interp.setResult("unanticipated exception type caught"); (8)
    return TCL_ERROR;
  }

  return TCL_OK;                                            (9)
}        
            
        
(1)
This operation bind the interpreter (first parameter) to all CTCLObject objects in the vector. Some operations performed on CTCLObject objects require the help of an interpreter. This operation ensures that an interpreter is always available for those operations.
(2)
Exception handling is used to simplify error detection and handling. All errors will just be thrown as one of several exception types. These exceptions will be caught, turned into error messages in the command result and result in a return value of TCL_ERROR signalling failure to the interpreter.
(3)
This operation ensures there's at least a subcommand present. If the objv vector does not have at least two elements, an exception is thrown (std::string) that carries along with it the error string that was passed in.

CTCLObjectProcessor provides a number of requireXXXX methods. These have a common parameter signature and will succeed if the requirement is met or throw an std::string if not. If the error string is not provided a generic default error string is used instead.

(4)
Extracts the subcommand string from the first parameter to the command. This makes use of one of the many conversion operations in CTCLObject that can access the string or non-string representations of the underlying Tcl_Object* encapsulated by CTCLObject.

If any of these type conversions fail, a CTCLException is thrown, which is derived from CException, and therefore will be caught by try/catch block.

(5)
This if/else chain does the actual dispatch of the subcommand to the proper handler. The handlers are expected to signal their errors via an exception. That allows them to be declared as type void and makes it unecessary to explicitly check if they've succeeded.
(6)
This defends against the user providing a bad subcommand. We throw an exception which is caught below, turned into the command result and an unsuccessful return value.
(7)
These catch blocks handle various types of exceptions we might expect during command processing. In each case, error text is extracted from the exception and turned into the interpreter result and TCL_ERROR is returned. This tells the interpreter the command failed. The return value of a failed command becomes the error message the interpreter will emit.

In the event the command is executed with the Tcl catch command, this will be the message returned when the value of the catch is nonzero.

(8)
This catch will get control if an exception we've not explicitly caught is emitted (note that CException and std::exception are base classes that can catch a wide variety of actual exceptions). In that case we fabricate a return value and still return TCL_ERROR.
(9)
If control drops all the way through the try/catch block, the command succeeded and TCL_OK is returned to indicate that to the interpreter. The individual command processors are responsible for setting the command result in that case, if appropriate.

In your case, the subcommand handlers are all pretty simple. Therefore we'll show them all at once.

Example 6-4. Implementation of subcommand processors


void
EVPSwitchCommand::save(CTCLInterpreter& interp, std::vector<CTCLObject>& objv)
{
  requireExactly(objv, 2);
  m_switcher.save();
}

void
EVPSwitchCommand::restore(CTCLInterpreter& interp, std::vector<CTCLObject>& objv)
{
  requireExactly(objv, 2);
  m_switcher.restore();
}

void
EVPSwitchCommand::filter(CTCLInterpreter& interp, std::vector<CTCLObject>& objv)
{
  requireExactly(objv, 2);
  m_switcher.useFilterProcessor();
}

        

There are really only two points of interest in these method implementations:

Once we have written this class we need to make an instance of this class in a way that adds it to SpecTcl's interpreter.

This is done by editing the AddCommands method in MySpecTclApp.cpp. At the end of this method add the line:


             new EVPSwitchCommand(rInterp, "evpipline");
        

This adds the command evpipeline to the interpreter passed in to the method.