Persistent incr-Tcl objects

by Paul Welton

Introduction

An incr Tcl program typically contains many objects forming a structure through both composition and inheritance. It is often useful to be able to:

  1. save the state of the program to a file so that the program can be restarted later and the state restored. For example, if the program is an editor, then that file would be one form of "native source" for the program. Because a permanent record of the objects is created, they are refered to as persistent objects.
  2. duplicate parts of the hierarchy keeping the references between objects consistent.
  3. Transmit the program state, or part of it, between incr Tcl programs, possibly across a network. This application leads to the terminology serialisation of objects.

The serialization of objects refers to the generation of a script of incr Tcl commands that can be saved to disk, executed in a different namespace context, or transmitted to another machine across a network.

This page describes how to use a utility class "DeepCopy" to do this. It relates to version 1.7 of the file: http://tsippwb.cvs.sourceforge.net/tsippwb/TSIPPwb/tcl/DeepCopy.itcl which is part of the source code for the TSIPP Work Bench, an interactive editor for 3D pictures and animations. This is an incr Tcl program which uses persistent objects created with DeepCopy as its native source.

Overview

In essence, classes to be serialized should inherit DeepCopy, and will then export a method "putObject" to serialize the object to a specified filehandle. The arguments to putObject are:

    fp              - pointer to a file which must be open for writing.
    varList         - List of scalar public variables of this object which
                      the code written to "fp" will restore when executed.
    constructorArgs - list of parameters to be passed to the constructor.
                      The special value {-} is equivalent to the empty
                      list, implying no parameters.  The special value
                      means that the list can always be made non-empty,
                      which signifies that specializations should use it
                      in place of constructing their own set.

A simple example

Suppose the following class Foo is declared and an object foo is created:

    package require Itcl

    source DeepCopy.itcl

    itcl::class Foo {
        inherit DeepCopy

        constructor {p1} {
            set v1 $p1
        }

        public  variable baz
        private variable v1

        public method setBaz {_baz} {
            set baz $_baz
        }

        public method putObject {fp {varList {}}} {
            chain $fp [concat $varList baz] $v1
        }
    }

    Foo foo "bar"
    foo setBaz "this is its value"

If the following is executed, a file will be generated which will be a valid Tcl script that can reconstruct the object:

    DeepCopy::init

    set fd [open "/tmp/storage" w]
    foo putObject $fd
    close $fd

The generated file is:

    ::Foo foo bar
    foo configure -baz {this is its value}

In general, putObject will need to be overloaded in each class to add the specialization required by the class. The following sections describe how to deal with all of those special cases:

Public Scalar Variables

Include the name of each public variable that is to be saved in the list passed as the argument "varList". The intention is that the specialization of putObject for each class will call chain in which this parameter is the concatenation of the received argument of that name and the list of public variable that are to be saved from that class.

Private Scalar Variables

To save the state of private variables, it is necessary to (i) write a public method which will set the variable to a specified value and (ii) in the specialization of putObject code should be written out to call this method when the object is restored. The specialization of putObject can access the private variable directly.

Arrays

Whether an array is public or private, these must be handled in the same way as private scalar variables. Typically, "array get" would be used to capture the array in the specialization of putObject and "array set" would be used in the code written out by putObject.

Other Attributes of the Object

An object may have state information associated with it other than that contained in variables. For example, it may have a screen representation associated with it. The specialization of putObject should capture it and generate code to recreate it. For example, if the screen representation is a rectangle item on a canvas, then canvas methods such as "coords" and "itemcget" can be used to recover the information, and the code generated would contain corresponding calls to "coords" and "itemconfigure".

Composition

When an object A is being serialized that makes a reference to another object B, then the specialization of putObject for A should make a recursive call to putObject for B, in addition to the action taken to save and restore the variable containing the object name.

This would not apply to any objects created by the constructor. In such a case, special treatment must be devised.

Typically, a structure of objects will contain multiple references to an object, and the policy outlined above would result in the section of code which saves and restores an object being duplicated, or even of infinite loops developing. For this reason, putObject remembers the names of the objects which have been saved, and suppresses any duplicates.

Specializations of putObject should implement this and the following structure is recommended:

    public method putObject {fp varList} {
        if {[chain $fp $varList]} {
            #  Since the base version returned 1, this is not a duplicate,
            #  so this specialization should continue with the additional
            #  code required.

             <extra operations for this specialization>

            #  Return 1 to any further specialization that should also
            #  perform the necessary extra operations.
            return 1
        } else {

            #  The base version has determined that this object has already
            #  been serialized, so this specialization must take no further
            #  action and should return 0 to make any further specialization
            #  do likewise.
            return 0
        }
    }

Prior to saving the state of a hierarchy, call DeepCopy::init, which resets the memory of saved objects. If this is not done then any objects which may previously have been saved will be assumed to be duplicates and will not be saved.

Inheritance

In order for a class to have the ability to save itself to disk it should inherit DeepCopy, and if necessary, as is usually the case, a specialization of the method "putObject" should be written. The specialization should call the base method using "chain". If a further class inherits this class it should do likewise, and chain will automatically make sure that each specialization is called in turn.

Parameters to Constructors

If objects have constructors which take parameters, then they can be saved and restored only if it is possible to determine the appropriate values for the parameters from the current state of the object. If for example, the parameters are copied directly into variables then this is easy. However, if the constructor uses the parameter to index into some database to generate variable values, for which no reverse translation is available, then redesign of the class may be advisable.

Assuming that this issue is sucessfully addressed, putObject accepts an optional third argument which is a list to be concatenated with the object creation command that is generated. By default, this list is an empty string, which would be appropriate for objects which have no arguments to their constructors.

A complexity is that constructors may have different parameters at each level in the inheritance chain. How is putObject to decide whether to use the supplied argument "constructorArgs" which has been passed in by chaining, or to construct the arguments for its own level? One scheme would be that an empty argument (the default) implies the latter, and a non-empty argument implies the former. However, note that it is quite possible that in the former case the argument will be the empty string. Therefore, a special value of {-} is recognised which will mean use an empty list as the set of arguments to the constructor.

A Complex Example

The following example illustrates most of the special cases described above:

    package require Itcl

    source DeepCopy.itcl

    itcl::class Foo {
        inherit DeepCopy

        constructor {p1} {
            set v1 $p1
        }

        public  variable baz
        private variable v1

        public method setBaz {_baz} {
            set baz $_baz
        }

        public method putObject {fp {varList {}} {constructorArgs {}}} {
            if {[llength $constructorArgs] > 0} {
                chain $fp [concat $varList baz] $constructorArgs
            } else {
                chain $fp [concat $varList baz] $v1
            }
        }
    }

    itcl::class Foo_complex {
        inherit Foo

        constructor {} {
            chain v1_contents
        } {}

        private variable myFoo
        private variable activeFoo1
        private variable activeFoo2

        public method elaborate {} {
            for {set i 0} {$i < 3} {incr i} {
                set myFoo($i) [Foo #auto bar$i]
                $myFoo($i) setBaz $i
            }
            set activeFoo1 $myFoo(0)
            set activeFoo2 [Foo #auto bar$i]
        }

        public method setLocalArray {nameValuePairs _activeFoo1 _activeFoo2} {
            array set myFoo $nameValuePairs
            set activeFoo1 $_activeFoo1
            set activeFoo2 $_activeFoo2
        }

        public method putObject {fp {varList {}}} {
            if {[chain $fp $varList -]} {
                puts $fp [list [namespace tail $this] setLocalArray [array get myFoo] $activeFoo1 $activeFoo2]
                foreach i [array names myFoo] {
                    $myFoo($i) putObject $fp
                }
                $activeFoo1 putObject $fp
                $activeFoo2 putObject $fp

                return 1
            } else {
                return 0
            }
        }
    }

    Foo foo "bar"
    foo setBaz "this is its value"

    Foo_complex fred
    fred setBaz "value of baz in fred"
    fred elaborate

    DeepCopy::init

    set fd [open "/tmp/storage" w]
    foo  putObject $fd
    fred putObject $fd
    close $fd

The resulting code is:

    ::Foo foo bar
    foo configure -baz {this is its value}
    ::Foo_complex fred
    fred configure -baz {value of baz in fred}
    fred setLocalArray {0 foo0 1 foo1 2 foo2} foo0 foo3
    ::Foo foo0 bar0
    foo0 configure -baz 0
    ::Foo foo1 bar1
    foo1 configure -baz 1
    ::Foo foo2 bar2
    foo2 configure -baz 2
    ::Foo foo3 bar3
    foo3 configure -baz <undefined>

CMcC This is good stuff, but I wonder how hard/possible it would be to delve/introspect into the structure of an itcl object and pull out the values for all of its constituents, serialise them, and reconstruct a copy.

I've created another page: persistent itcl to contain the abstraction-breaking code


Category Example | Category Itcl | Category Object Orientation | Category Package