Home / Resources / Tcl extension with libtcl: share C variables and functions

Introduction

Tcl is a high-level, general-purpose, interpreted, dynamic programming language. It was designed with the goal of being very simple but powerful. Tcl casts everything into the mold of a command, even programming constructs like variable assignment and procedure definition. Tcl supports multiple programming paradigms, including object-oriented, imperative, functional, and procedural styles.

tcllib is a collection of utility modules for Tcl. These modules provide a wide variety of functionality, from implementations of standard data structures to implementations of common networking protocols. The intent is to collect commonly used function into a single library, which users can rely on to be available and stable.

Tcl

Here we discuss the extension of Tcl with a shared variable and a custom procedure. We want to be able to access the value of the variable from both Tcl and C. We can use the Tcl trace add variable command to print a message every time this variable is accessed.

Let's imagine that we have compiled a package called example in a shared library example.so. The following Tcl code prints a message every time variable myVariable is written.

set dir [file dirname [file normalize [info script]]]
load "${dir}/example.so" example

proc trace_variable {
  theVarName
  theArrayName
  theAction
} {
  if { ${theAction} == "write" } {
    puts "Variable ${theVarName} is now set to [set ::${theVarName}]"
  }
}

trace add variable myVariable write trace_variable

We now need to develop the so called example.so shared library to provide a variable myVariable and a function to update it from C.

C code

Our C function receives an arbitrary one-word value when called by the Tcl interpreter. It is useful to pass a pointer to a custom structure so that we can store all our data in it. For instance, if the shared variable is an integer:

typedef struct {
  Tcl_Interp* theInterp;    //!< pointer to the Tcl interpreter

  int myVariable;           //!< a C variable we make available in Tcl
  char myVariableName[11];  //!< name of the Tcl variable

  Tcl_ObjCmdProc * myCommandHandler;  //!< a C function we make available in Tcl
  char myCommandName[10];             //!< name of the Tcl procedure
} theClientDataStruct;

Our C function only increments the variable. Its prototype matches Tcl_ObjCmdProc as described in man 3tcl Tcl_CreateObjCommand.

static int incrMyVar(
  ClientData clientData,  //!< pointer to the client data
  Tcl_Interp *interp,     //!< pointer to the Tcl interpreter
  int objc,               //!< number of argument values
  Tcl_Obj *const objv[]   //!< values of the arguments
) {
  theClientDataStruct * theClientData = (theClientDataStruct *)( clientData );

  if ( objc != 1 ) {
    Tcl_WrongNumArgs(interp, 1, objv, "");
    return TCL_ERROR;
  }

  theClientData->myVariable = theClientData->myVariable + 1;
  // so that Tcl trace can see it:
  Tcl_UpdateLinkedVar(interp, theClientData->myVariableName);
  return TCL_OK;
}

The last step is to initialize the shared library, the name of the function to do so is important. In our example, we must allocate one structure and set its content. Then we makes a C variable and a C function available in Tcl.

int Example_Init(
  Tcl_Interp *interp  //!< pointer to the Tcl interpreter
) {
  theClientDataStruct * theClientData;

  if ( Tcl_InitStubs(interp, "8.6", 0) == NULL ) {
    return TCL_ERROR;
  }

  // initializes the structure:
  theClientData = (theClientDataStruct *)(
                                      Tcl_Alloc(sizeof(theClientDataStruct)) );
  theClientData->theInterp  = interp;
  theClientData->myVariable = 0;
  strncpy(theClientData->myVariableName, "myVariable",
                                        sizeof(theClientData->myVariableName));
  theClientData->myCommandHandler = incrMyVar;
  strncpy(theClientData->myCommandName, "myCommand",
                                        sizeof(theClientData->myCommandName));
  Tcl_Preserve(theClientData);  // makes sure data is kept until Tcl_Release()

  // makes a C variable available in Tcl:
  Tcl_LinkVar(  interp,
                theClientData->myVariableName,
                (char *)( &theClientData->myVariable ),
                TCL_LINK_INT
              );

  // makes a C function available in Tcl:
  Tcl_CreateObjCommand( interp,
                        theClientData->myCommandName,
                        theClientData->myCommandHandler,
                        theClientData,
                        NULL
                      );

  // the package name must match the prefix of the name of this function:
  Tcl_PkgProvide(interp, "example", "1.0");

  return TCL_OK;
}

Makefile

Now, we must compile our shared library and execute the Tcl interpreter to observe the updates of our variable. We can access the value of variable myVariable from Tcl and call procedure myCommand to update it from C.

Here is a Makefile that provides the recipe (make sure to indent with tabulations):

all : example.tcl example.so
    tclsh $<

example.so : example.c
    gcc -shared -o $@ -I /usr/include/tcl/ $^ -ltclstub

clean:
    $(RM) example.so

Associated resources

Here is an archive which contains the discussed example and its Makefile.



no cookie, no javascript, no external resource, KISS!