The easiest way to explain the concept and use of Tcl-Cpp is probably through an example. This example is actually drawn from a working project that employs Tcl-Cpp; hopefully, its motivation will be credible even if it is not the `cutest' example in the world (sorry, no breakfast food).
This is the situation: in an audio-processing application, I need to store and pass around short sequences of floating-point numbers - vectors of coefficients or attributes over an array of frequency bands. Many of the more complex objects in the C++ system accept or return data in the form of these basic vectors of floats.
However, these basic vectors occur in two forms - the floatVec, which is a full encapsulation of the data vector, including looking after the allocation and destrution of the memory to hold the values, and the lightweight floatRef, which merely points to a block of memory, but does not take responsibility for allocating it. floatRefs can be used to pass a subset of longer vector without copying the data, and can also be used to present portions of larger data structures (such as arrays) as vectors of floats.
When building a Tcl user-interface for this package, it is useful to be able to manipulate these objects directly from Tcl - for instance, to transfer results of one operation to the input of another - thus we will used Tcl-Cpp to construct an interface for us.
First let's look at the underlying C++ definition of floatRef and floatVec:
// floatRef.H class floatRef { // A reference to a vector of floating point values protected: float *data; // pointer to the actual float values public: int len; // the number of elements in this vector reference int step; // spacing between values (e.g. for array slices) floatRef(int l = 0, float *d = 0, int s = 1) : len(l), data(d), step(s) { }; floatRef(const floatRef &fr) { len = fr.len; step = fr.step; data = fr.data; } ~floatRef(void) { } float Val(int ix) const { return *(data+step*ix); } void setVal(int ix, double v) { *(data+step*ix) = v; } void Copy(const floatRef &fr) // copy (part of) a floatRef { int i, x = min(len, fr.len); for(i = 0; i<x; ++i) setVal(i, fr.Val(i)); } void Add(const floatRef &fr) // add 2nd vector to this one { int i, x = min(len, fr.len); for(i = 0; i<x; ++i) setVal(i, Val(i)+fr.Val(i)); } void Scale(double factor) // scale every value by factor { int i; for(i = 0; i<x; ++i) setVal(i, factor*Val(i)); } double Max(void) // return the largest value { double r = Val(0); int i; for(i = 0; i<len; ++i) if(Val(i)>r) r = Val(i); return r; } }; class floatVec : public floatRef { // a vector of floats including allocation public: floatVec(int l) : floatRef(l) { data = new float[len]; } floatVec(const floatVec& fv) : floatRef(fv.len) { data = new float[len]; Copy(fv); } floatVec(const floatRef& fr) : floatRef(fr.len) { data = new float[len]; Copy(fr); } ~floatVec(void) { delete [] data; } void CopyVec(const floatRef& fr) // Copy a vector including its size { len = fr.len; delete [] data; data = new float[len]; Copy(fr); } }; class BlackBox { // The main application object public: int debug; // public instance variable BlackBox(char *filename); ~BlackBox(void); floatRef& queryFn(int arg); int controlFn(floatRef& params); };
We see that the floatRef class defines the basic accessors for the vector (Val and setVal), as well as operations to process or modify the data referred to, perhaps in conjunction with another floatRef object. (note: I have not used operator overloading, such as operator[], because these special cases are not handled by Tcl-Cpp). The len and step instance variables are public to allow code to access their values directly.
The floatVec class inherits all the operations of the floatRef, but includes store allocation and destruction statements, a constructor to make a copy of the data in an existing floatRef, and a method to copy the data from a reference into an existing floatVec, resizing it first. The len and step variables are still publicly accessible, although it is the programmer's responsibility only to read, and not to modify, these values.
The BlackBox object represents the larger application that is the real focus of the development and the Tcl front-end to be constructed. It has a constructor that takes the name of a datafile to process, and a couple of token functions either to return float vector data from the processing (queryFn), or to supply vectors of arguments to control the computation (controlFn).
In order to get Tcl-Cpp to write our interface code for us, we must create a simple interface description file, called CDL files in Dean's original implementation. The CDL file for the above example is as follows:
# floatRef_cdl.cdl pass { // Created automatically from floatRef_cdl.cdl #include "floatRef.H" #include "floatRef_cdl.H" } class floatRef -castable { method Val {int} {float} method setVal {int; float} {void} method Copy {obref floatRef} {void} method Add {obref floatRef} {void} method Max {void} {float} slot len {int} slot step {int} } class floatVec -castable -isA floatRef { constructor {int -default 0} method CopyVec {obref floatRef} {void} } class BlackBox { constructor {str} method queryFn {int -default 123} {obref -new floatRef} method controlFn {obref floatRef} {int} slot debug {int} }
These statements merely define the C++ classes for which Tcl interfaces will be constructed, and the slots and methods to include in those interfaces. The pass block is used to specify verbatim code to include in the automatically-generated C++ source files. The class block introduces a new C++ class definition; within a class block, the constructor statement specifies the arguments to the one version of the constructor that will be accessible from within Tcl, and the method statement defines a particular class method to be supported by the Tcl wrappers, specifying the input arguments and the return value type in the next two blocks. The CDL file is actually processed by a modified version of the Tcl parser, so braces ("{", "}") are used to enclose blocks, and semicolons (";") are used to separate statements within a block, such as multiple entries in an argument list. So far, this is identical to the functionality provided in Dean's original ObjectTcl.
The slot declarations within the class block define fields of the C++ object which will be accessible from Tcl using config and cget commands.
Directly after the class name definition, the -castable option indicates that this particular class should be constructed in such a way that pure C++ returns of this type (as references or pointers) will be coercable into Tcl wrappers. This statement is provided for backwards compatibility; Classes without this modification are handled in the 'traditional' ObjectTcl manner, with the object underlying the Tcl implementation a subclass of its C++ version. Since this derived class cannot be created retrospectively, objects that are to be 'wrapped' on-the-fly must be declared -castable, indicating that their Tcl realizations are objects that merely contain a pointer to the C++ original, which can thus be created as needed whenever a suitable C++ object appears.
The -isA declaration can be used to name base classes of this object. A derived class inherits all the Tcl-interfaced functions and slots of its named base classes.
No constructor is defined for the floatRef class, meaning that objects of this type cannot be created within Tcl, but can only occur as the result of other interfaced C++ methods.
The return from the queryFn is declared -new because, as it happens, this function returns a temporary object by value, which must therefore be copied into a new floatRef object straight away. The interface definition must explicitly label these situations to avoid code that tries to take the address of such a volatile object. queryFn takes a single argument which is optional; if it is not specified, it defaults to 123.
A new Tcl interpreter shell that includes the interface to the C++ objects is constructed by the following steps:
cdl -h floatRef_cdl.cdl # generates floatRef_cdl.H cdl -s floatRef_cdl.cdl # generates floatRef_cdl.C
CC -g -I../tclcpp floatRef_cdl.cdl
CC -g -I../tclcpp tclCppAppInit.C floatRef_cdl.o floatRef.o -o fresh -ltclcpp -ltcl -lm
The basic Tcl-Cpp interface machinery is provided by the tclcpp library included in the final link. The application-specific tclCppAppInit.C file actually only has to include the standard Tcl-Cpp initialization call, and is included as part of the package. Both this file and the interface files generated by cdl will need to include files from the Tcl-Cpp source directory, hence the -I../tclcpp lines. The new, floatRef-savvy Tcl shell interpreter is created under the name fresh.
Note that the tclcpp library and the wrapped C++ functions may be built as shared libraries for dynamic loading under Tcl 7.5 (and higher). For more details on how to do and use this, see the Details page.
Executing the new floatRef shell lands us at the familiar Tcl "%" prompt. First, we initialize our underlying C++ application by creating an instance of the BlackBox class (or possibly some other startup, perhaps using static class methods, which are also supported within ObjectTcl):
% BlackBox bb somefile.dat -debug 1 bb %
Note the Tk-like creation syntax, classname instancename arguments [config args]. A new BlackBox object has been created, called bb as we specified. Its mandatory formal argument (filename in the C++ constructor) was set to "somefile.dat"; after creation, the Tcl-Cpp interface code also set the debug instance variable of the new C++ object to 1. Now we can extract data from this object by using its name as a Tcl command, again Tk-style:
% bb queryFn floatRef1 %
(Since we did not specify the optional parameter to queryFn, it took on its default value of 123). The floatRef return from queryFn has been automatically wrapped by the Tcl-Cpp interface into a new Tcl object named floatRef1. We can manipulate the data in this vector via the floatRef methods for which interfaces were specified:
% floatRef1 Val 0 0.000 % floatRef1 Val 1 1.414 % floatRef1 cget -len 46 % floatRef1 Max 17.33 %
Notice that the method syntax is instancename methodname args, and access to instance slots is obtained via the special cget method, and prefixing the slot name with a dash - again, both analogous to objects created within Tk.
Let's say we want to save the values in this vector before it gets modified by the BlackBox object. We can create a new floatVec and copy the floatRef's values into it:
% floatVec myfv myfv % myfv CopyVec floatRef1 % myfv cget -len 46 % myfv Max 17.33 %
Note how the floatVec instance, myfv, has inherited the floatRef function, Max. We are able to change the values in myfv without affecting our BlackBox instance, since the floatVec was created by the Tcl-Cpp interface and is not known to BlackBox. However, we can use it as a control input to BlackBox:
% myfv setVal 1 0.0 % bb controlFn myfv 99 %
The TclCpp interface automatically casts myfv to its floatRef base class before passing it to the BlackBox's controlFn routine.
[Still to add: explain how the -from argument modifier allows full Tk-like ClassName instanceName -param val constructor syntax based on dummy slots]
Go on to the reference page.
$Header: /n/crab/da/dpwe/src/otcl/html/RCS/tclcppex.html,v 1.2 1997/03/13 02:55:14 dpwe Exp $