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.