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:
Introduce the Tcl++ library used by both NSCLDAQ and SpecTcl to provide a C++ class wrapper around elements of the Tcl API.
Introduce several key classes from that library that we'll need.
Define a command ensemble and describe the specific command ensemble that we'll create.
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:
Execute scripts.
Evaluate expressions.
Add and remove commands.
Set a command result.
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:
Provides a base class constructor that takes care of binding the command to an interpreter and registering it with it.
Provides a standard interface between Tcl and your code when the command is executed.
Provides several methods usable by derived classes which help check that the correct number of command line parameters are provided.
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:
Provide a mechanism to get the underlying Tcl_Obj* pointer.
Provide for assignment to the object from a variety of types.
Provide for conversion from the object to a variety of types.
Provide operations that treat the object like a list and supply several operations on lists.
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:
Saves the current list of event processors
Restores the current list of event processors from a previously saved set.
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;public: EVPSwitchCommand(CTCLInterpreter& interp, const char* command); int operator()(CTCLInterpreter& interp, std::vector<CTCLObject>& objv);
private: void save(CTCLInterpreter& interp, std::vector<CTCLObject>& objv); void restore(CTCLInterpreter& interp, std::vector<CTCLObject>& objv);
void filter(CTCLInterpreter& interp, std::vector<CTCLObject>& objv); }; #endif
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).
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.
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:
Ensure all command line word objects get bound to the interpreter.
Ensure there's a subcommand.
Dispatch to the appropriate subcommand handler.
Perform top level error handling.
Example 6-3. Implementation of EVPSwitchCommand::operator()
int EVPSwitchCommand::operator()(CTCLInterpreter& interp, std::vector<CTCLObject>& objv) { bindAll(interp, objv);try {
requireAtLeast(objv, 2, "Command requires a subcommand");
std::string subcommand = objv[1];
if (subcommand == "save") { save(interp, objv); } else if (subcommand == "restore") { restore(interp, objv);
} else if (subcommand == "filter") { filter(interp, objv); } else {
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) {
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");
return TCL_ERROR; } return TCL_OK;
}
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.
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.
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.
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.
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.
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:
requireExactly
throws an exception if
exaactly two command words are not present. This exception
is caught by the dispatching method, operator()
,
and turned into a TCL_ERROR return value.
There's no need for these methods to return a value. Either they throw an exception which is caught and turned into a TCL_ERROR, or they don't, in which case the dispatcher returns TCL_OK.
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:
This adds the command evpipeline to the interpreter passed in to the method.