www.gibmonks.com


 вартиры в ќдинцово: купить куплю квартиру isk-zapad.ru.

  Previous section   Next section

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

Table of Contents
Chapter 44.  C Programming and Tcl


A C Command Procedure

Tcl 8.0 introduced a new interface for Tcl commands that is designed to work efficiently with its internal on-the-fly byte code compiler. The original interface to commands was string oriented. This resulted in a lot of conversions between strings and internal formats such as integers, double-precision floating point numbers, and lists. The new interface is based on the Tcl_Obj type that can store different types of values. Conversions between strings and other types are done in a lazy fashion, and the saved conversions help your scripts run more efficiently.

This section shows how to build a random number command using both interfaces. The string-based interface is simpler, and we start with that to illustrate the basic concepts. You can use it for your first experiments with command procedures. Once you gain some experience, you can start using the interfaces that use Tcl_Obj values instead of simple strings. If you have old command procedures from before Tcl 8.0, you need to update them only if you want extra efficiency. The string and Tcl_Obj interfaces are very similar, so you should find updating your command procedures straightforward.

The String Command Interface

The string-based interface to a C command procedure is much like the interface to the main program. You register the command procedure like this:

Tcl_CreateCommand(interp, data, "cmd", CmdProc, DeleteProc);

When the script invokes cmd, Tcl calls CmdProc like this:

CmdProc(data, interp, argc, argv);

The interp is type Tcl_Interp *, and it is a general handle on the state of the interpreter. Most Tcl C APIs take this parameter. The data is type ClientData, which is an opaque pointer. You can use this to associate state with your command. You register this state along with your command procedure, and then Tcl passes it back to you when the command is invoked. This is especially useful with Tk widgets, which are explained in more detail in Chapter 46. Our simple RandomCmd command procedure does not use this feature, so it passes NULL into Tcl_CreateCommand. The DeleteProc is called when the command is destroyed, which is typically when the whole Tcl interpreter is being deleted. If your state needs to be cleaned up, you can do it then. RandomCmd does not use this feature, either.

The arguments from the Tcl command are available as an array of strings defined by an argv parameter and counted by an argc parameter. This is the same interface that a main program has to its command line arguments. Example 44-2 shows the RandomCmd command procedure:

Example 44-2 The RandomCmd C command procedure.
/*
 * RandomCmd --
 * This implements the random Tcl command. With no arguments
 * the command returns a random integer.
 * With an integer valued argument "range",
 * it returns a random integer between 0 and range.
 */
int
RandomCmd(ClientData clientData, Tcl_Interp *interp,
      int argc, char *argv[])
{
   int rand, error;
   int range = 0;
   if (argc > 2) {
      interp->result = "Usage: random ?range?";
      return TCL_ERROR;
   }
   if (argc == 2) {
      if (Tcl_GetInt(interp, argv[1], &range) != TCL_OK) {
         return TCL_ERROR;
      }
   }
   rand = random();
   if (range != 0) {
      rand = rand % range;
   }
   sprintf(interp->result, "%d", rand);
   return TCL_OK;
}

The return value of a Tcl command is really two things: a result string and a status code. The result is a string that is either the return value of the command as seen by the Tcl script, or an error message that is reported upon error. For example, if extra arguments are passed to the command procedure, it raises a Tcl error by doing this:

interp->result = "Usage: random ?range?";
return TCL_ERROR;

The random implementation accepts an optional argument that is a range over which the random numbers should be returned. The argc parameter is tested to see if this argument has been given in the Tcl command. argc counts the command name as well as the arguments, so in our case argc == 2 indicates that the command has been invoked something like:

random 25

The procedure Tcl_GetInt converts the string-valued argument to an integer. It does error checking and sets the interpreter's result field in the case of error, so we can just return if it fails to return TCL_OK.

if (Tcl_GetInt(interp, argv[1], &range) != TCL_OK) {
    return TCL_ERROR;
}

Finally, the real work of calling random is done, and the result is formatted directly into the result buffer using sprintf. A normal return looks like this:

sprintf(interp->result, "%d", rand);
return TCL_OK;

Result Codes from Command Procedures

The command procedure returns a status code that is either TCL_OK or TCL_ERROR to indicate success or failure. If the command procedure returns TCL_ERROR, then a Tcl error is raised, and the result value is used as the error message. The procedure can also return TCL_BREAK, TCL_CONTINUE, TCL_RETURN, which affects control structure commands like foreach and proc. You can even return an application-specific code (e.g., 5 or higher), which might be useful if you are implementing new kinds of control structures. The status code returned by the command procedure is the value returned by the Tcl catch command, which is discussed in more detail on page 77.

Managing the String Result

There is a simple protocol that manages the storage for a command procedure's result string. It involves interp->result, which holds the value, and interp->freeProc, which determines how the storage is cleaned up. When a command is called, the interpreter initializes interp->result to a static buffer of TCL_RESULT_SIZE, which is 200 bytes. The default cleanup action is to do nothing. These defaults support two simple ways to define the result of a command. One way is to use sprintf to format the result in place:

sprintf(interp->result, "%d", rand);

Using sprintf is suitable if you know your result string is short, which is often the case. The other way is to set interp->result to the address of a constant string. In this case, the original result buffer is not used, and there is no cleanup required because the string is compiled into the program:

interp->result = "Usage: random ?random?";

In more general cases, the following procedures should be used to manage the result and freeProc fields. These procedures automatically manage storage for the result:

Tcl_SetResult(interp, string, freeProc)
Tcl_AppendResult(interp, str1, str2, str3, (char *)NULL)
Tcl_AppendElement(interp, string)

Tcl_SetResult sets the return value to be string. The freeProc argument describes how the result should be disposed of: TCL_STATIC is used in the case where the result is a constant string allocated by the compiler, TCL_DYNAMIC is used if the result is allocated with Tcl_Alloc, which is a platform- and compiler-independent version of malloc, and TCL_VOLATILE is used if the result is in a stack variable. In the TCL_VOLATILE case, the Tcl interpreter makes a copy of the result before calling any other command procedures. Finally, if you have your own memory allocator, pass in the address of the procedure that should free the result.

Tcl_AppendResult copies its arguments into the result buffer, reallocating the buffer if necessary. The arguments are concatenated onto the end of the existing result, if any. Tcl_AppendResult can be called several times to build a result. The result buffer is overallocated, so several appends are efficient.

Tcl_AppendElement adds the string to the result as a proper Tcl list element. It might add braces or backslashes to get the proper structure.

If you have built up a result and for some reason want to throw it away (e.g., an error occurs), then you can use Tcl_ResetResult to restore the result to its initial state. Tcl_ResetResult is always called before a command procedure is invoked.

The Tcl_Obj Command Interface

The Tcl_Obj command interface replaces strings with dual-ported values. The arguments to a command are an array of pointers to Tcl_Obj structures, and the result of a command is also of type Tcl_Obj. The replacement of strings by Tcl_Obj values extends throughout Tcl. The value of a Tcl variable is kept in a Tcl_Obj, and Tcl scripts are stored in a Tcl_Obj, too. You can continue to use the old string-based API, which converts strings to Tcl_Obj values, but this conversion adds overhead.

The Tcl_Obj structure stores both a string representation and a native representation. The native representation depends on the type of the value. Tcl lists are stored as an array of pointers to strings. Integers are stored as 32-bit integers. Floating point values are stored in double-precision. Tcl scripts are stored as sequences of byte codes. Conversion between the native representation and a string are done upon demand. There are APIs for accessing Tcl_Obj values, so you do not have to worry about type conversions unless you implement a new type. Example 44-3 shows the random command procedure using the Tcl_Obj interfaces:

Example 44-3 The RandomObjCmd C command procedure.
/*
 * RandomObjCmd --
 * This implements the random Tcl command from
 * Example 44? using the object interface.
 */
int
RandomObjCmd(ClientData clientData, Tcl_Interp *interp,
       int objc, Tcl_Obj *CONST objv[])
{
   Tcl_Obj *resultPtr;
   int rand, error;
   int range = 0;
   if (objc > 2) {
      Tcl_WrongNumArgs(interp, 1, objv, "?range?");
      return TCL_ERROR;
   }
   if (objc == 2) {
      if (Tcl_GetIntFromObj(interp, objv[1], &range) !=
             TCL_OK) {
         return TCL_ERROR;
      }
   }
   rand = random();
   if (range != 0) {
      rand = rand % range;
   }
   resultPtr = Tcl_GetObjResult(interp);
   Tcl_SetIntObj(resultPtr, rand);
   return TCL_OK;
}

Compare Example 44-2 with Example 44-3. You can see that the two versions of the C command procedures are similar. The Tcl_GetInt call is replaced with Tcl_GetIntFromObj call. This receives an integer value from the command argument. This call can avoid conversion from string to integer if the Tcl_Obj value is already an integer.

The result is set by getting a handle on the result object and setting its value. This is done instead of accessing the interp->result field directly:

resultPtr = Tcl_GetObjResult(interp);
Tcl_SetIntObj(resultPtr, rand);

The Tcl_WrongNumArgs procedure is a convenience procedure that formats an error message. You pass in objv, the number of arguments to use from it, and additional string. The example creates this message:

wrong # args: should be "random ?range?"

Example 44-3 does not do anything obvious about storage management. Tcl initializes the result object before calling your command procedure and takes care of cleaning it up later. It is sufficient to set a value and return TCL_OK or TCL_ERROR. In more complex cases, however, you have to worry about reference counts to Tcl_Obj values. This is described in more detail later.

If your command procedure returns a string, then you will use Tcl_SetStringObj. This command makes a copy of the string you pass it. The new Tcl interfaces that take strings also take length arguments so you can pass binary data in strings. If the length is minus 1, then the string is terminated by a NULL byte. A command that always returned "boring" would do this:

resultPtr = Tcl_GetObjResult(interp);
Tcl_SetStringObj(resultPtr, "boring", -1);

This is a bit too boring. In practice you may need to build up the result piecemeal. With the string-based API, you use Tcl_AppendResult. With the Tcl_Obj API you get a pointer to the result and use Tcl_AppendToObj or Tcl_AppendStringsToObj:

resultPtr = Tcl_GetObjResult(interp);
Tcl_AppendStringsToObj(resultPtr, "hello ", username, NULL);

Managing Tcl_Obj Reference Counts

The string-based interfaces copy strings when passing arguments and returning results, but the Tcl_Obj interfaces manipulate reference counts to avoid these copy operations. References come from Tcl variables, from the interpreter's result, and from sharing caused when a value is passed into a Tcl procedure. Constants are also shared. When a C command procedure is called, Tcl does not automatically increment the reference count on the arguments. However, each Tcl_Obj referenced by objv will have at least one reference, and it is quite common to have two or more references.

The C type definition for Tcl_Obj is shown below. There are APIs to access all aspects of an object, so you should refrain from manipulating a Tcl_Obj directly unless you are implementing a new type:

Example 44-4 The Tcl_Obj structure.
typedef struct Tcl_Obj {
   int refCount;       /* Counts number of shared references */
   char *bytes;        /* String representation */
   int length;         /* Number of bytes in the string */
   Tcl_ObjType *typePtr;/* Type implementation */
   union {
      long longValue;  /* Type data */
      double doubleValue;
      VOID *otherValuePtr;
      struct {
         VOID *ptr1;
         VOID *ptr2;
      } twoPtrValue;
   } internalRep;
} Tcl_Obj;

Each type implementation provides a few procedures like this:

Tcl_GetTypeFromObj(interp, objPtr, valuePtr);
Tcl_SetTypeObj(resultPtr, value);
objPtr = Tcl_NewTypeObj(value);

graphics/tip_icon.gif

The initial reference count is zero.


The Tcl_NewTypeObj allocates storage for a Tcl_Obj and sets its reference count to zero. Tcl_IncrRefCount and Tcl_DecrRefCount increment and decrement the reference count on an object. Tcl_DecrRefCount frees the storage for Tcl_Obj when it goes to zero. The initial reference count of zero was chosen because functions like Tcl_SetObjResult automatically increment the reference count on an object.

The Tcl_GetTypeFromObj and Tcl_SetTypeObj procedures just get and set the value; the reference count does not change. Type conversions are automatic. You can set a Tcl_Obj value to an integer and get back a string or double precision number later. The type implementations automatically take care of the storage for the Tcl_Obj value as it changes. Of course, if a Tcl_Obj stays the same type, then no string conversions are necessary and accesses are more efficient.

Modifying Tcl_Obj Values

It is not safe to modify a shared Tcl_Obj. The sharing is only for efficiency: Logically, each reference is a copy, and you must honor this model when creating and modifying Tcl_Obj values. Tcl_IsShared returns 1 if there is more than one reference to an object. If a command procedure modifies a shared object, it must make a private copy with Tcl_DuplicateObj. The new copy starts with a reference count of zero. You either pass this to Tcl_SetResultObj, which adds a reference, or you have to explicitly add a reference to the copy with Tcl_IncrRefCount.

Example 44-5 implements a plus1 command that adds one to its argument. If the argument is not shared, then plus1 can be implemented efficiently by modifying the native representation of the integer. Otherwise, it has to make a copy of the object before modifying it:

Example 44-5 The Plus1ObjCmd procedure.
/*
 * Plus1ObjCmd --
 * This adds one to its input argument.
 */
int
Plus1ObjCmd(ClientData clientData, Tcl_Interp *interp,
       int objc, Tcl_Obj *CONST objv[])
{
   Tcl_Obj *objPtr;
   int i;
   if (objc != 2) {
      Tcl_WrongNumArgs(interp, 1, objv, "value");
      return TCL_ERROR;
   }
   objPtr = objv[1];
   if (Tcl_GetIntFromObj(interp, objPtr, &i) != TCL_OK) {
      return TCL_ERROR;
   }
   if (Tcl_IsShared(objPtr)) {
      objPtr = Tcl_DuplicateObj(objPtr);   /* refCount 0 */
      Tcl_IncrRefCount(objPtr);            /* refCount 1*/
   }
   /*
    * Assert objPtr has a refCount of one here.
    * OK to set the unshared value to something new.
    * Tcl_SetIntObj overwrites the old value.
    */
   Tcl_SetIntObj(objPtr, i+1);
   /*
    * Setting the result object adds a new reference,
    * so we decrement because we no longer care about
    * the integer object we modified.
    */
   Tcl_SetObjResult(interp, objPtr);       /* refCount 2*/
   Tcl_DecrRefCount(objPtr);               /* refCount 1*/
   /*
    * Now only the interpreter result has a reference to objPtr.
    */
   return TCL_OK;
}

Pitfalls of Shared Tcl_Obj Values

You have to be careful when using the values from a Tcl_Obj structure. The Tcl C library provides many procedures like Tcl_GetStringFromObj, Tcl_GetIntFromObj, Tcl_GetListFromObj, and so on. These all operate efficiently by returning a pointer to the native representation of the object. They will convert the object to the requested type, if necessary. The problem is that shared values can undergo type conversions that may invalidate your reference to a particular type of the value.

graphics/tip_icon.gif

Value references are only safe until the next Tcl_Get*FromObj call.


Consider a command procedure that takes two arguments, an integer and a list. The command procedure has a sequence of code like this:

Tcl_GetListFromObj(interp, objv[1], &listPtr);
/* Manipulate listPtr */
Tcl_GetIntFromObj(interp, objv[2], &int);
/* listPtr may be invalid here */

If, by chance, both arguments have the same value, (e.g., 1 and 1), which is possible for a Tcl list and an integer, then Tcl will automatically arrange to share these values between both arguments. The pointers in objv[1] and objv[2] will be the same, and the reference count on the Tcl_Obj they reference will be at least 2. The first Tcl_GetListFromObj call ensures the value is of type list, and it returns a direct pointer to the native list representation. However, Tcl_GetInFromObj then helpfully converts the Tcl_Obj value to an integer. This deallocates the memory for the list representation, and now listPtr is a dang-ling pointer! This particular example can be made safe by reversing the calls because Tcl_GetIntFromObj copies the integer value:

Tcl_GetIntFromObj(interp, objv[2], &int);
Tcl_GetListFromObj(interp, objv[1], &listPtr);
/* int is still a good copy of the value */

By the way, you should always test your Tcl_Get* calls in case the format of the value is incompatible with the requested type. If the object is not a valid list, the following command returns an error:

if (Tcl_GetListFromObj(interp, obj[1], &listPtr) != TCL_OK) {
    return TCL_ERROR;
}

      Previous section   Next section
    Top