static data in command procedures

Tcl offers several techniques that can be used to implement static data in command procedures. This page is intended to present the pros and cons.


Example:

Here is a command procedure for a simple Tcl command that makes use of a "static" value. Roughly, it's a constant value that each invocation of the command will use without changing, but a value that need not be created at all if the command is never evaluated.

int
OneObjCommand(ClientData cd, Tcl_Interp *interp,
    int objc, Tcl_Obj *const objv[])
{
    Tcl_SetObjResult(interp, Tcl_NewIntObj(1));
    return TCL_OK;
}

Tcl_CreateObjCommand(interp, "one", OneObjCommand, NULL, NULL);

Note that each time one is evaluated, a new Tcl_Obj is created to hold the value 1. Since Tcl is capable of managing shared Tcl_Objs, other alternatives are possible that would set the result to be an additional reference to one shared Tcl_Obj rather than a new one each time. The alternatives differ mostly on where that shared Tcl_Obj is stored between calls.

Why might we want an alternative? Most compelling reason is memory efficiency. Consider the script:

for {set i 0} {$i < 1000000} {incr i} {
    set a($i) [one]
}

With the implementation of one above, 1 million Tcl_Obj structs have to be allocated.


Alternative 1: ClientData

int
OneObjCommand(ClientData cd, Tcl_Interp *interp,
    int objc, Tcl_Obj *const objv[])
{
    Tcl_SetObjResult(interp, (Tcl_Obj *)cd);
    return TCL_OK;
}
void
OneDelete(ClientData cd)
{
    Tcl_Obj *objPtr = (Tcl_Obj *)cd;
    Tcl_DecrRefCount(objPtr);
}

objPtr = Tcl_NewIntObj(1);
Tcl_IncrObjCount(objPtr);
Tcl_CreateObjCommand(interp, "one", OneObjCommand,
    (ClientData)objPtr, OneDelete);

Here the shared Tcl_Obj with the value 1 is stuffed in the ClientData of one. One disadvantage of this alternative is that the shared Tcl_Obj is created whether or not one is ever called. For larger amounts of static data, that might be a waste worth avoiding. Another possible disadvantage might be conflict with other uses of the ClientData word that a command might have.


Alternative 2: One field in ClientData

typedef struct OneData {
    Tcl_Obj *one;
} OneData;
int
OneObjCommand(ClientData cd, Tcl_Interp *interp,
    int objc, Tcl_Obj *const objv[])
{
    OneData *dataPtr = (OneData *)cd;
    if (dataPtr->one == NULL) {
      dataPtr->one = Tcl_NewIntObj(1);
      Tcl_IncrRefCount(dataPtr->one);
    }
    Tcl_SetObjResult(interp, dataPtr->one);
    return TCL_OK;
}
void
OneDelete(ClientData cd)
{
    OneData *dataPtr = (OneData *)cd;
    if (dataPtr->one != NULL) {
         Tcl_DecrRefCount(dataPtr->one);
    }
    Tcl_Free(dataPtr);
}

dataPtr = Tcl_Alloc((int)sizeof(OneData));
Tcl_CreateObjCommand(interp, "one", OneObjCommand,
    (ClientData)dataPtr, OneDelete);

This is a good choice when the command is already making use of ClientData.


Alternative 3: Interp AssocData

int
OneObjCommand(ClientData cd, Tcl_Interp *interp,
    int objc, Tcl_Obj *const objv[])
{
    Tcl_Obj *objPtr = Tcl_GetAssocData(interp, "one", NULL);
    if (objPtr == NULL) {
        objPtr = Tcl_NewIntObj(1);
        Tcl_IncrRefCount(objPtr);
        Tcl_SetAssocData(interp, "one",
            OneAssocDelete, (ClientData)objPtr);
    }
    Tcl_SetObjResult(interp, objPtr);
    return TCL_OK;
}
void
OneAssocDelete(ClientData cd, Tcl_Interp *interp)
{
    Tcl_Obj *objPtr = (Tcl_Obj *)cd;  
    Tcl_DecrRefCount(objPtr);
}
void
OneDelete(ClientData cd)
{
    Tcl_Interp *interp = (Tcl_Interp *)cd;
    Tcl_DeleteAssocData(interp, "one");
}

Tcl_CreateObjCommand(interp, "one", OneObjCommand,
    (ClientData)interp, OneDelete);

This alternative is best avoided on several grounds. First, it's vulnerable to a collision in the key name passed to the Tcl_*AssocData routines. Any other code that also wants to store data under the "one" key of this interp will interfere. Second, note that the ClientData of the command had to be set to interp just to enable OneDelete to call Tcl_DeleteAssocData. This ties up the ClientData and is a messy tangle for later code maintainers to understand. Third, it's likely that the performance cost of the Tcl_GetAssocData lookup (based on TCL_STRING_KEYS) will be at least as expensive as the original Tcl_NewIntObj.

This alternative could be simplified somewhat by dropping OneDelete and the command's ClientData. The price would be that the shared value would not be freed during command deletion, but would hang around until the entire interp is destroyed.


Alternative 4: ThreadData

int
OneObjCommand(ClientData cd, Tcl_Interp *interp,
    int objc, Tcl_Obj *const objv[])
{
    static Tcl_ThreadDataKey oneDatakey;
    Tcl_Obj **objPtrPtr = Tcl_GetThreadData(
        &oneDataKey, (int) sizeof (Tcl_Obj *));
    if (*objPtrPtr == NULL) {
      *objPtrPtr = Tcl_NewIntObj(1);
      Tcl_IncrRefCount(*objPtrPtr);
      Tcl_CreateThreadExitHandler(
          OneRelease, (ClientData)(*objPtrPtr));
    }
    Tcl_SetObjResult(interp, *objPtrPtr);
    return TCL_OK;
}
void
OneRelease(ClientData cd)
{
    Tcl_Obj *objPtr = (Tcl_Obj *)cd;
    Tcl_DecrRefCount(objPtr);
}

Tcl_CreateObjCommand(interp, "one", OneObjCommand, NULL, NULL);

In this alternative, the Tcl_Obj is shared by multiple interps in the same thread. For applications that use many interps in each thread, that can add up. In contrast to Alternative 3, the key value here is a Tcl_ThreadDataKey rather than a string, so it's much less likely to have any inadvertent collision in the key value with other code. Note, though, that after all [one] commands in all interps are deleted, the shared value will live on until the thread exits.

One nice feature of this alternative is that the ClientData and Tcl_CmdDeleteProc arguments to Tcl_CreateObjCommand are both NULL. That is, the technique is implemented entirely within the command procedure; no special initialization is required. This also means there's no conflict with other uses the command may have for those arguments.

This alternative could be modified to free the shared value when no more interps are using it (with the addition of a Tcl_CmdDeleteProc argument, etc.), but that would require storing in the ThreadData the set of interps currently sharing the shared value. For a single Tcl_Obj value, that additional overhead doesn't make sense, but the technique is available for cases where the shared, static data is more substantial, or when freeing an unused resource is otherwise of critical importance.


Comments on these, or other alternatives, are invited.

DGP

DKF: The AssocData solution is better when you have the same static value shared across multiple commands, especially when what you'd really like to do is to add an extra field to the Tcl_Interp structure but can't (we really don't want extensions directly mucking around in there!). When using AssocData, it's wisest to use names that appear to be in the namespace of your package; this minimizes the chance of inadvertent collision; i.e. if you're working in the foobar package, use names starting with foobar::...

See Also

static variables
a pure-Tcl solution