TODL: Objects in pure TCL. ====================================================================== Introduction. TCL, the Tool Command Language, is a popular scripting language supported by a large programmer community. The basic language can be extended in many ways, for example by linking new C or C++ code to the interpreter, thus making new primitives available in the scripting language. TCL itself is not object-oriented. Many extensions exist that add objects to TCL. Some of those extensions are written in C and require you to recompile the TCL interpreter. Other solutions use TCL itself to provide simple object access. The todl tool belongs to the second category: it works with your installed TCL interpreter and does not require any compilation. Scripting languages are typically very dynamic: they allow you to change everything at runtime. This makes them very flexible and powerful. Some object extensions of TCL allow you to change the class hierarchy at runtime. The todl tool, on the other hand, works with a static class hierarchy. I must admit that this is not entirely in the spirit of dynamic programming, but it makes the implementation of the tool much easier. Maybe I will provide a more dynamic version in the future (any ideas on this topic are more than welcome). The most important feature of todl is simplicity. Many classical problems in object-oriented technology, such as object persistence or named method parameters, have *extremely* simple and elegant solutions in TCL. I try to reflect that in todl. Whatever problem I try to solve, I never accept a solution until I have the feeling that it can not be further simplified. If you find a simpler solution for any of the things I implemented, please let me know. ------- Overview. The idea is that you start by writing a schema. This is a specification of all the classes you want to support, with their attributes and methods. From that schema, the todl tool generates code. Code generation is a technique that allows very precise, fine-tuned code to be created from a high-level description (the schema in this case). The generated code is pure TCL, and allows you to create, delete, and manipulate instances of the classes you specified in the schema. It does not allow you to add new classes or change anything else in the schema (the schema is static). The code generator is itself written in TCL. After generating the code, you need to implement the methods of your classes. Obviously, this must be done manually. The tool only helps you on the way by generating a skeleton for each method, with an empty body. After that, you can start using the objects in TCL. No compilation of any C or C++ are required. At all times, you can use the standard TCL shell which is probably already installed on your system. ------- An example schema. To get an idea of how it all works, we will start with an example. Suppose we want to have shapes in TCL, such as rectangles and squares. We want these shapes to remember their own width, height and other attributes. We want to give them methods such as 'draw yourself' or 'print the values of your attributes'. We will keep it simple and implement only two shapes: 'rect' and 'square'. They will both inherit from base class 'shape', which stores the common attributes and provides common methods. The example code is in directory '../test_shapes'. All references to files in the following text, are files in that directory. To start, we need to write a schema file. It looks like this: [see 'shapes.sch']. This schema introduces three classes, grouped in a module. I based the syntax of the schema vaguely on the standard Object Definition Language (ODL). I had to keep the syntax simple though, because I want it to be easily parsable by TCL itself (more on that later). First, we specify the classes. Note that this schema is static, so that the class structure cannot be changed by the running TCL script. Using the keyword 'class', we can introduce new classes. The class name is followed by the list of parent classes. For example, 'rect' inherits from 'shape', which does not inherit from any other class. The keyword 'attr' introduces a new attribute in a class. The attribute's name and default value must be given. The default value is a TCL literal, so it could be a list enclosed in [], or any other TCL construct (even a piece of TCL code). The keyword 'method' is used to declare a new method in the class. We specify the name of the method and its list of arguments. Again, the argument list is just like for TCL procs, so you can provide default values if you want (as you can see for the 'y' coordinate of the 'draw' method). ( Remark: before the method name you will see an empty string in the schema, e.g. method "" draw { x {y 0} } ^^^^ This string is supposed to contain modifiers for the method. It is currently not yet used by the tool, so just set the empty string after each occurrence of the 'method' keyword. Maybe I'll remove this in the future. ) After defining all classes, we can specify some of the module's parameters. For example, 'all' is a list of methods that all classes in this module must have. This is a shortcut for specifying the method in each of the classes separately. I will explain the other module parameters later, by showing you the resulting generated code. ------- Code generation. From the schema, the todl tool generates TCL code. For example, if we invoke the tool like this: tcl_odl.tcl shapes.sch shapes.tcl shapes.skl the file 'shapes.sch' is used as input, and two new files are generated. The first generated file, 'shapes.tcl', contains TCL procs to create and delete objects of the specified classes, plus some additional helper functions. For example, it contains 'rect', a TCL proc that instantiates a new object of class 'rect': proc rect {handle args} You can call this proc as follows: rect a -w 10 -h 50 which creates a new instance of class 'rect' named 'a', and initializes some of its attributes. You can find a more elaborate example in directories ../test_shapes and ../test_persist. The generated 'rect' proc is implemented in pure TCL. This is true for all other generated code. As a consequence, no compilation of any C or C++ code is required to make the objects available to your TCL scripts. The second generated file, 'shapes.skl', is just a convenience: it contains skeletons for all the methods that you need to implement manually. This includes the methods 'perimeter', 'area' and 'draw', which we specified in the schema. ------- Implementation of methods. Now that the code is generated, we need to implement some of the methods manually. I copied the generated skeleton file 'shapes.skl' to a new file 'shapes.imp' and added the implementations in that file. (Changing the skeleton file is not a good idea because it is overwritten when you run the tool again). The method implementations are very simple for this example. The 'print' methods just print out the class name and the values of all attributes. The 'draw' methods just output a message to the screen; in an actual application, they would draw a shape on a canvas, or print postscript data to a file. As you can see by comparing the schema with the manual implementation in 'shapes.imp', the name of each method is prefixed by the class name. This is necessary because TCL does not allow overloading: when you have two classes with a method called 'area', you need two distinct names for the corresponding procs in TCL. You probably also noticed that each method gets an additional argument called 'handle'. This is the name of the object for which the method is being invoked. You can obtain attributes of the object by calling 'cget' on the handle, similar to the configuration of widget attributes in Tk. As an alternative, you can invoke the 'mem' function to make instance attributes available as local variables in your scope (see for example the implementation of 'rect_perimeter'). ------- Using the generated code. We are now ready to instantiate objects in TCL. The file 'try.tcl' contains a simple example: it creates a rectangle and a square, configuring some of their attributes. Then it prints them and draws them. As you see, calling a method of an object is as simple as stating the object's name, followed by the method name, followed by parameters (if any). This is entirely similar to the calling conventions of widgets in Tk. The constructor for each class has the same name as the class itself. That's because in our schema, we specified 'new_name' as the empty string. If you set 'new_name' to some other value, that value is prefixed before each class name. For example, if we had specified 'new_name' to be 's', the constructors would have been called 's_rect', 's_square' etc. Note that 'rect' and 'square' are constructors that allocate some resources for the new objects. To free those resources, you must call 'delete' at the appropriate time. Just like in C++, every object that you create must also be deleted. It is probably not very hard to implement a rudimentary form of garbage collection, I just haven't come around to doing it yet. The 'delete' proc gets its name from the 'delete_name' parameter in the schema. If you set this to "-", no 'delete' proc is generated (which implies that you will almost certainly have memory leaks). The example shows that you can delete more than one object with a single call. Let's have a look at the resulting output of this simple script: [see 'try.out'] The lines starting with CTOR and DTOR are printed by the constructors and destructors of the objects, because we set the 'debug' parameter to 1 in our schema. This allows you to see exactly when each object is created and destroyed. Set 'debug' to 0 to switch this off. (In fact, save the generated file 'shapes.tcl' first, then set 'debug' to 0 in the schema, then generate the code again and compare it to what you saved. It's an interesting exercise that gives you a hint on how the code generation works). The lines starting with an exclamation mark are printed by the methods that we manually implemented. You also see the 'print' method at work. ------- Adding code to the constructor and destructor. In C++, you can implement the constructor and destructor of an object. The todl tool allows you to do something similar with the 'init_name' and 'exit_name' parameters. In the example schema, you'll find them commented out. By removing the comments, and running the tool again, each class in the generated code gets the 'init' and 'exit' methods. We must implement those in 'shapes.imp' (you can cut/paste the skeletons from 'shapes.skl'). I have provided an example implementation in 'reg.imp'. It shows how each object, in its constructor, can register itself into a global list or other data structure that keeps track of all existing objects. Conversely, in its destructor, the object unregisters itself. You can see how this works by running 'reg.tcl'. ------- How to contact me: If you have questions, remarks, simplifications and especially improvements, you can send them to .