www.gibmonks.com




  Previous section   Next section

Practical Programming in Tcl & Tk, Third Edition
By Brent B. Welch

Table of Contents
Chapter 44.  C Programming and Tcl


The blob Command Example

This section illustrates some standard coding practices with a bigger example. The example is still artificial in that it doesn't actually do very much. However, it illustrates a few more common idioms you should know about when creating Tcl commands.

The blob command creates and manipulates blobs. Each blob has a name and some associated properties. The blob command uses a hash table to keep track of blobs by their name. The hash table is an example of state associated with a command that needs to be cleaned up when the Tcl interpreter is destroyed. The Tcl hash table implementation is nice and general, too, so you may find it helpful in a variety of situations.

You can associate a Tcl script with a blob. When you poke the blob, it invokes the script. This shows how easy it is to associate behaviors with your C extensions. Example 44-6 shows the data structures used to implement blobs.

Example 44-6 The Blob and BlobState data structures.
/*
 * The Blob structure is created for each blob.
 */
typedef struct Blob {
   int N;              /* Integer-valued property */
   Tcl_Obj *objPtr;    /* General property */
   Tcl_Obj *cmdPtr;    /* Callback script */
} Blob;
/*
 * The BlobState structure is created once for each interp.
 */
typedef struct BlobState {
   Tcl_HashTable hash;        /* List blobs by name */
   int uid;                   /* Used to generate names */
} BlobState;

Creating and Destroying Hash Tables

Example 44-7 shows the Blob_Init and BlobCleanup procedures. Blob_Init creates the command and initializes the hash table. It registers a delete procedure, BlobCleanup, that will clean up the hash table.

The Blob_Init procedure allocates and initializes a hash table as part of the BlobState structure. This structure is passed into Tcl_CreateObjCommand as the ClientData, and gets passed back to BlobCmd later. You might be tempted to have a single static hash table structure instead of allocating one. However, it is quite possible that a process has many Tcl interpreters, and each needs its own hash table to record its own blobs.

When the hash table is initialized, you specify what the keys are. In this case, the name of the blob is a key, so TCL_STRING_KEYS is used. If you use an integer key, or the address of a data structure, use TCL_ONE_WORD_KEYS. You can also have an array of integers (i.e., a chunk of data) for the key. In this case, pass in an integer larger than 1 that represents the size of the integer array used as the key.

The BlobCleanup command cleans up the hash table. It iterates through all the elements of the hash table and gets the value associated with each key. This value is cast into a pointer to a Blob data structure. This iteration is a special case because each entry is deleted as we go by the BlobDelete procedure. If you do not modify the hash table, you continue the search with Tcl_NextHashEntry instead of calling Tcl_FirstHashEntry repeatedly.

Example 44-7 The Blob_Init and BlobCleanup procedures.
/*
 * Forward references.
 */

int BlobCmd(ClientData data, Tcl_Interp *interp,
      int objc, Tcl_Obj *CONST objv[]);
int BlobCreate(Tcl_Interp *interp, BlobState *statePtr);
void BlobCleanup(ClientData data);

/*
 * Blob_Init --
 *
 *     Initialize the blob module.
 *
 * Side Effects:
 *     This allocates the hash table used to keep track
 *     of blobs. It creates the blob command.
 */
int
Blob_Init(Tcl_Interp *interp)
{
    BlobState *statePtr;
   /*
    * Allocate and initialize the hash table. Associate the
    * BlobState with the command by using the ClientData.
    */
   statePtr = (BlobState *)Tcl_Alloc(sizeof(BlobState));
   Tcl_InitHashTable(&statePtr->hash, TCL_STRING_KEYS);
   statePtr->uid = 0;
   Tcl_CreateObjCommand(interp, "blob", BlobCmd,
          (ClientData)statePtr, BlobCleanup);
   return TCL_OK;
}
/*
 * BlobCleanup --
 *     This is called when the blob command is destroyed.
 *
 * Side Effects:
 *     This walks the hash table and deletes the blobs it
*      contains. Then it deallocates the hash table.
 */
void
BlobCleanup(ClientData data)
{
   BlobState *statePtr = (BlobState *)data;
   Blob *blobPtr;
   Tcl_HashEntry *entryPtr;
   Tcl_HashSearch search;

   entryPtr = Tcl_FirstHashEntry(&statePtr->hash, &search);
   while (entryPtr != NULL) {
      blobPtr = Tcl_GetHashValue(entryPtr);
      BlobDelete(blobPtr, entryPtr);
      /*
       * Get the first entry again, not the "next" one,
       * because we just modified the hash table.
       */
      entryPtr = Tcl_FirstHashEntry(&statePtr->hash, &search);
   }
   Tcl_Free((char *)statePtr);
}

Tcl_Alloc and Tcl_Free

Instead of using malloc and free directly, you should use Tcl_Alloc and Tcl_Free in your code. Depending on compilation options, these procedures may map directly to the system's malloc and free, or use alternate memory allocators. The allocators on Windows and Macintosh are notoriously poor, and Tcl ships with a nice efficient memory allocator that is used instead. In general, it is not safe to allocate memory with Tcl_Alloc and free it with free, or allocate memory with malloc and free it with Tcl_Free.

When you look at the Tcl source code you will see calls to ckalloc and ckfree. These are macros that either call Tcl_Alloc and Tcl_Free or Tcl_DbgAlloc and Tcl_DbgFree depending on a compile-time option. The second set of functions is used to debug memory leaks and other errors. You cannot mix these two allocators either, so your best bet is to stick with Tcl_Alloc and Tcl_Free everywhere.

Parsing Arguments and Tcl_GetIndexFromObj

Example 44-8 shows the BlobCmd command procedure. This illustrates a basic framework for parsing command arguments. The Tcl_GetIndexFromObj procedure is used to map from the first argument (e.g., "names") to an index (e.g., NamesIx). This does error checking and formats an error message if the first argument doesn't match. All of the subcommands except "create" and "names" use the second argument as the name of a blob. This name is looked up in the hash table with Tcl_FindHashEntry, and the corresponding Blob structure is fetched using Tcl_GetHashValue. After the argument checking is complete, BlobCmd dispatches to the helper procedures to do the actual work:

Example 44-8 The BlobCmd command procedure.
/*
 * BlobCmd --
 *
 *    This implements the blob command, which has these
 *    subcommands:
 *       create
 *       command name ?script?
 *       data name ?value?
 *       N name ?value?
 *       names ?pattern?
 *       poke name
 *       delete name
 *
 * Results:
 *    A standard Tcl command result.
 */
int
BlobCmd(ClientData data, Tcl_Interp *interp,
   int objc, Tcl_Obj *CONST objv[])
{
   BlobState *statePtr = (BlobState *)data;
   Blob *blobPtr;
   Tcl_HashEntry *entryPtr;
   Tcl_Obj *valueObjPtr;

   /*
    * The subCmds array defines the allowed values for the
   * first argument. These are mapped to values in the
   * BlobIx enumeration by Tcl_GetIndexFromObj.
   */

   char *subCmds[] = {
      "create", "command", "data", "delete", "N", "names",
      "poke", NULL
   };
   enum BlobIx {
      CreateIx, CommandIx, DataIx, DeleteIx, NIx, NamesIx,
      PokeIx
   };
   int result, index;

   if (objc == 1 || objc > 4) {
      Tcl_WrongNumArgs(interp, 1, objv, "option ?arg ...?");
      return TCL_ERROR;
   }
   if (Tcl_GetIndexFromObj(interp, objv[1], subCmds,
          "option", 0, &index) != TCL_OK) {
      return TCL_ERROR;
   }
   if (((index == NamesIx || index == CreateIx) &&
          (objc > 2)) ||
      ((index == PokeIx || index == DeleteIx) &&
          (objc == 4))) {
      Tcl_WrongNumArgs(interp, 1, objv, "option ?arg ...?");
      return TCL_ERROR;
   }
   if (index == CreateIx) {
      return BlobCreate(interp, statePtr);
   }
   if (index == NamesIx) {
      return BlobNames(interp, statePtr);
   }
   if (objc < 3) {
      Tcl_WrongNumArgs(interp, 1, objv,
         "option blob ?arg ...?");
      return TCL_ERROR;
   } else if (objc == 3) {
      valueObjPtr = NULL;
   } else {
      valueObjPtr = objv[3];
   }
   /*
    * The rest of the commands take a blob name as the third
    * argument. Hash from the name to the Blob structure.
    */
   entryPtr = Tcl_FindHashEntry(&statePtr->hash,
          Tcl_GetString(objv[2]));
   if (entryPtr == NULL) {
      Tcl_AppendResult(interp, "Unknown blob: ",
             Tcl_GetString(objv[2]), NULL);
      return TCL_ERROR;
   }
   blobPtr = (Blob *)Tcl_GetHashValue(entryPtr);
   switch (index) {
      case CommandIx: {
         return BlobCommand(interp, blobPtr, valueObjPtr);
      }
      case DataIx: {
         return BlobData(interp, blobPtr, valueObjPtr);
      }
      case NIx: {
         return BlobN(interp, blobPtr, valueObjPtr);
      }
      case PokeIx: {
         return BlobPoke(interp, blobPtr);
      }
      case DeleteIx: {
         return BlobDelete(blobPtr, entryPtr);
      }
   }
}

Creating and Removing Elements from a Hash Table

The real work of BlobCmd is done by several helper procedures. These form the basis of a C API to operate on blobs as well. Example 44-9 shows the BlobCreate and BlobDelete procedures. These procedures manage the hash table entry, and they allocate and free storage associated with the blob.

Example 44-9 BlobCreate and BlobDelete.
int
BlobCreate(Tcl_Interp *interp, BlobState *statePtr)
{
   Tcl_HashEntry *entryPtr;
   Blob *blobPtr;
   int new;
   char name[20];
   /*
    * Generate a blob name and put it in the hash table
    */
   statePtr->uid++;
   sprintf(name, "blob%d", statePtr->uid);
   entryPtr = Tcl_CreateHashEntry(&statePtr->hash, name, &new);
   /*
    * Assert new == 1
    */
   blobPtr = (Blob *)Tcl_Alloc(sizeof(Blob));
   blobPtr->N = 0;
   blobPtr->objPtr = NULL;
   blobPtr->cmdPtr = NULL;
   Tcl_SetHashValue(entryPtr, (ClientData)blobPtr);
   /*
    * Copy the name into the interpreter result.
    */
   Tcl_SetStringObj(Tcl_GetObjResult(interp), name, -1);
   return TCL_OK;
}
int
BlobDelete(Blob *blobPtr, Tcl_HashEntry *entryPtr)
{
   Tcl_DeleteHashEntry(entryPtr);
   if (blobPtr->cmdPtr != NULL) {
      Tcl_DecrRefCount(blobPtr->cmdPtr);
   }
   if (blobPtr->objPtr != NULL) {
      Tcl_DecrRefCount(blobPtr->objPtr);
   }
   /*
    * Use Tcl_EventuallyFree because of the Tcl_Preserve
    * done in BlobPoke. See page 626.
    */
   Tcl_EventuallyFree((char *)blobPtr, Tcl_Free);
   return TCL_OK;
}

Building a List

The BlobNames procedure iterates through the elements of the hash table using Tcl_FirstHashEntry and Tcl_NextHashEntry. It builds up a list of the names as it goes along. Note that the object reference counts are managed for us. The Tcl_NewStringObj returns a Tcl_Obj with reference count of zero. When that object is added to the list, the Tcl_ListObjAppendElement procedure increments the reference count. Similarly, the Tcl_NewListObj returns a Tcl_Obj with reference count zero, and its reference count is incremented by Tcl_SetObjResult:

Example 44-10 The BlobNames procedure.
int
BlobNames(Tcl_Interp *interp, BlobState *statePtr)
{
   Tcl_HashEntry *entryPtr;
   Tcl_HashSearch search;
   Tcl_Obj *listPtr;
   Tcl_Obj *objPtr;
   char *name;
   /*
    * Walk the hash table and build a list of names.
    */
   listPtr = Tcl_NewListObj(0, NULL);
   entryPtr = Tcl_FirstHashEntry(&statePtr->hash, &search);
   while (entryPtr != NULL) {
      name = Tcl_GetHashKey(&statePtr->hash, entryPtr);
      if (Tcl_ListObjAppendElement(interp, listPtr,
             Tcl_NewStringObj(name, -1)) != TCL_OK) {
          return TCL_ERROR;
      }
      entryPtr = Tcl_NextHashEntry(&search);
   }
   Tcl_SetObjResult(interp, listPtr);
   return TCL_OK;
}

Keeping References to Tcl_Obj Values

A blob has two simple properties: an integer N and a general Tcl_Obj value. You can query and set these properties with the BlobN and BlobData procedures. The BlobData procedure keeps a pointer to its Tcl_Obj argument, so it must increment the reference count on it:

Example 44-11 The BlobN and BlobData procedures.
int
BlobN(Tcl_Interp *interp, Blob *blobPtr, Tcl_Obj *objPtr)
{
   int N;
   if (objPtr != NULL) {
      if (Tcl_GetIntFromObj(interp, objPtr, &N) != TCL_OK) {
         return TCL_ERROR;
      }
      blobPtr->N = N;
   } else {
      N = blobPtr->N;
   }
   Tcl_SetObjResult(interp, Tcl_NewIntObj(N));
   return TCL_OK;
}
int
BlobData(Tcl_Interp *interp, Blob *blobPtr, Tcl_Obj *objPtr)
{
   if (objPtr != NULL) {
      if (blobPtr->objPtr != NULL) {
         Tcl_DecrRefCount(blobPtr->objPtr);
      }
      Tcl_IncrRefCount(objPtr);
      blobPtr->objPtr = objPtr;
   }
   if (blobPtr->objPtr != NULL) {
      Tcl_SetObjResult(interp, blobPtr->objPtr);
   }
   return TCL_OK;
}

Using Tcl_Preserve and Tcl_Release to Guard Data

The BlobCommand and BlobPoke operations let you register a Tcl command with a blob and invoke the command later. Whenever you evaluate a Tcl command like this, you must be prepared for the worst. It is quite possible for the command to turn around and delete the blob it is associated with! The Tcl_Preserve, Tcl_Release, and Tcl_EventuallyFree procedures are used to handle this situation. BlobPoke calls Tcl_Preserve on the blob before calling Tcl_Eval. BlobDelete calls Tcl_EventuallyFree instead of Tcl_Free. If the Tcl_Release call has not yet been made, then Tcl_EventuallyFree just marks the memory for deletion, but does not free it immediately. The memory is freed later by Tcl_Release. Otherwise, Tcl_EventuallyFree frees the memory directly and Tcl_Release does nothing. Example 44-12 shows BlobCommand and BlobPoke:

Example 44-12 The BlobCommand and BlobPoke procedures.
int
BlobCommand(Tcl_Interp *interp, Blob *blobPtr,
   Tcl_Obj *objPtr)
{
   if (objPtr != NULL) {
      if (blobPtr->cmdPtr != NULL) {
         Tcl_DecrRefCount(blobPtr->cmdPtr);
      }
      Tcl_IncrRefCount(objPtr);
      blobPtr->cmdPtr = objPtr;
   }
   if (blobPtr->cmdPtr != NULL) {
      Tcl_SetObjResult(interp, blobPtr->cmdPtr);
   }
   return TCL_OK;
}
int
BlobPoke(Tcl_Interp *interp, Blob *blobPtr)
{
   int result = TCL_OK;
   if (blobPtr->cmdPtr != NULL) {
      Tcl_Preserve(blobPtr);
      result = Tcl_EvalObj(interp, blobPtr->cmdPtr);
      /*
       * Safe to use blobPtr here
       */
      Tcl_Release(blobPtr);
      /*
       * blobPtr may not be valid here
       */
   }
   return result;
}

It turns out that BlobCmd does not actually use the blobPtr after calling Tcl_EvalObj, so it could get away without using Tcl_Preserve and Tcl_Release. These procedures do add some overhead: They put the pointer onto a list of preserved pointers and have to take it off again. If you are careful, you can omit these calls. However, it is worth noting the potential problems caused by evaluating arbitrary Tcl scripts!


      Previous section   Next section
    Top