==============================================================================
C-Scene Issue #2
Using TCL as a scripting language for an application written in C
Ungod
==============================================================================
Okay, I won't give you the rundown why tcl is a good scripting language as I
am sure that Sun can do that more than adequately. One thing that they don't
cover in their slightly less than adequate documentation is how to use tcl as
a scripting language in your own program rather than as a shell to which you
add command interfaces in C. To this end i have to thank Robey Pointer for
what i have learnt here. If you want a good example of what i am to cover
here in working action, go take a look at the insides of the eggdrop 'bot
program. Okay, enough of the rambling, let's start getting down to some gritty
bits. :)
First things first we are going to need to initialise the tcl engine, but we
aren't going to use the standard way that sun tells us about, as that way is
as mentioned before. So first things first, to initialise the engine we set up
a global variable to save a pointer to our interpretor in. (Yes you can have
multiple interpretors running in the same program, just use a new pointer and
create a new interpretor as i will show you below)
Tcl_Interp *interp=NULL; /* this is the pointer variable where we store the
reference to our interpretor, initialised to NULL
for safety */
void someinitroutine()
{
interp = Tcl_CreateInterp();
Tcl_Init(interp);
/* todo here: initialize any add in's to tcl like [incr tcl] and the
like ie. Itcl_Init(interp); */
}
Thats all we have to do, we now have a working tcl interpretor, that at the
moment basically sits there and does nothing much at all. The next step we
have to do is work out how we want to get data from the user and send it to
the interpretor, being a scripting extension, having the ability to load a
pre-written script is usually a must, and to achieve that we use the following
code:
int load_script(char *filename)
{
return Tcl_EvalFile(interp,filename);
}
As with most Tcl commands, they will return a tcl return code being one of
TCL_OK, TCL_ERROR, TCL_RETURN, TCL_BREAK, TCL_CONTINUE. TCL_OK and TCL _ERROR
are the ones that you will be using most and are pretty self explanatory. If
you need more information ie. error string, then you use interp->result to
get the information in string form. Another way to get data from the
user to the interpretor is to write a function to read typed input from the
user via stdin or something like an edit box into a string variable, you can
then pass this string to Tcl_Eval(interp,stringofcmds); in the same way as
above.
Okay that's all fine and well you say, we've got a nice little language we can
use to script with, but what use is it to me, all i can do is give them the
built in commands. Well here comes one of the nice things of TCL, you can
create your own functions in C, tell the interpretor where they are and
wella, your new command looks exactly like a packaged one :) What better way
to implement program specific commands. This is a two stage process and we'll
look at each stage separately.
The first stage is writing the actual function that get's called to execute
your command. This function is declared in a very specific way and *CANNOT*
be a c++ class member function. The reasons it cant be a c++ class member
function come under another article entirely, but by the end of this article
you should hopefully have enough information to figure out how to wrap you c++
members in a routing function. Anyway, here's the definition that your
function must take in the form of an example:
int myhellofunc(Clientdata cdata, Tcl_Interp *irp, int argc, char *argv[])
{
if(argc!=2)
{
Tcl_SetResult(irp,"Error wrong # args: should be hello
<yourname>",TCL_STATIC);
return TCL_ERROR;
}
/* else */
Tcl_AppendResult(irp,"Hello ",argv[1],NULL);
/* or you could just print out the data or do whatever you want with
the data ie:
printf("Hello %s\n",argv[1]); */
return TCL_OK;
}
Okay, lets look at that last function. The first thing you will notice is the
argc/argv parameters, and yes they work in exactly the same way as main() does
:) so you shouldn't have a problem there. Clientdata is user defined data that
get's passed by TCL, this data is specified when you bind the command to the
interpretor and we will discuss it more later. The Tcl_Interp parameter is a
pointer to the interpretor that called your function and it is good practice
to use the irp pointer rather than your global pointer to the interpretor, the
reason for this being that if you have multiple interpretors running at once,
you aren't going to know beforehand which one called the function. Using irp
is a good way to ensure that if you do use multiple interpretors at a later
date you won't run into unforseen logic errors. You will notice that we used
Tcl_SetResult and Tcl_AppendResult functions in our function and may be
wondering what they are there for. Well the answer to that question is that
the return value of the function is not the actual result of the function but
an error code (as mentioned before), so we need some way to return a result to
our interpretor and these are two ways of achieving that, time for you to go
and RTFM a bit and find out about this group of functions.
On to the next step of implementing our own command. binding it to the
interpretor. This is a pretty easy step but doing some manual reading of your
own always leaves you more informed, tho a little helping example always goes
a long way :). The command you use to do this is
Tcl_CreateCommand(interp, cmdName, proc, clientData, deleteProc) and will be
discussed after the example.
void bindcommand()
{
Tcl_CreateCommand(interp, "hello", myhellofunc, NULL, NULL);
}
That's all there is to binding a command to the interpretor, pretty simple hey
:) Okay, now for an explanation of each of the parameters. The first
parameter being a pointer to the interpretor that you are binding the command
to - pretty straightforward stuff. Then second parameter is a string containing
the actual command name as TCL and your users will see it. The third command is
a pointer to the function we created earlier to execute our command. The fourth
function is where we come to the discussion of clientdata. Basically
clientdata is any sort of data that we want passed to the function whenever our
command get's interpreted. this can be either an integer value or a pointer to
basically any sort of data that we can think of. The last parameter of the lot
is a pointer to a deleteProc, this procedure get's executed when we unbind our
command from the interpretor, so that we can clean up any data that we have
left laying around, sort of like a destructor in c++. One thing to note here
is that when our program terminates we do not need to explicitly unbind any
commands we have bound or shutdown the interpretor as TCL is smart enough to
handle all this. We are given the ability to do these things if for example
we want to remove a command from the interpretor in the middle of our program
or even shutdown an interpretor and start up a new one. Once again, this is
left up to your study :).
The next step we are going to look at is reading and writing variables in C and
binding variables in TCL to functions in C. Binding a variable to get/set
functions can be a very handy way to do runtime checking of values stored in
our variable. The first command that we will look at is how to read a TCL
variable from a c function, this is pretty simple and uses the
Tcl_SetVar(interp, varName, newValue, flags) function. Please note that TCL
works with strings no matter what "type" of variable it is so before you set a
variable, convert it to it's string equivalent.
/*writing to a TCL variable*/
int set_somevar(int value)
{
char buffer[100];
sprintf(buffer,"%d",value);
return Tcl_SetVar(interp,"somevar",buffer,TCL_GLOBAL_ONLY);
}
/*reading from a TCL variable*/
int get_somevar(int *retval)
{
char *localstring;
localstring=Tcl_GetVar(interp,"somevar",TCL_GLOBAL_ONLY);
return Tcl_ExprLong(interp,localstring,retval);
}
Okay, some more reading for you there, about Tcl_ExprLong and the scoping
available with the flags variable, i won't go into them here as this would
become a very long document if i did so. Those above examples should give you
enough of a starting point anyway :) To Note these "variables" are actual
variables that can be seen by any tcl script which should give you plenty of
scope for implementing application specific variables. One other section for
you to read up on are the Tcl_GetVar2/Tcl_SetVar2 functions as these apply to
the unique array functionality available in TCL, a functionality that is
powerful enough to be used as a mapping array straight out of the box. Anyway,
on to the final section of this article, another two step implementation,
binding variables to functions in your program. The first step is creating
our read and write routines, like the command binding, these functions have
to have a particular definition style shown below.
int somevar;
char *write_somevar(Clientdata cdata, Tcl_Interp irp, char *name1, char *name2,
int flags)
{
int tmpstore;
get_somevar(&tmpstore); /*see above */
if(tmpstore >= 1000 && tmpstore =< 2000)
{
somevar=tmpstore;
return NULL;
}
else
{
set_somevar(somevar);
return "Error: value is not between 1000 & 2000";
}
}
char *read_somevar(Clientdata cdata, Tcl_Interp irp, char *name1, char *name2,
int flags)
{
if(set_somevar(somevar)==TCL_ERROR)
return "An error occured setting the variable";
else
return NULL;
}
There are a couple of things to notice here, the first is the char string that
is returned from the function, according to the TCL manual, if an error occurs,
then you return a string indicating to the user some idea of why the read/write
failed, otherwise returning a NULL string indicates success. The second is
that to actually implement the reading and writing, you have to use Tcl_GetVar
and Tcl_SetVar described earlier, even though tcl is executing your commands,
it needs to know what to return. The last stage of this process and the last
step in this little article is the actual binding of the commands to the
functions. Note: You can actually use one function to cater for reading and
writing of variables, you can achieve this by checking the flags variable to
see if it is either TCL_TRACE_READS for reading or TCL_TRACE_WRITES for
writing. Me myself i like to use two separate commands to aid in the
readability of the program, but this of course is entirely up to you. Binding
the command is done by using the
Tcl_TraceVar(interp, varName, flags, proc, clientData) function and an example
is:
void bind_somevar()
{
Tcl_TraceVar(interp,"somevar",TCL_TRACE_WRITES,write_somevar,NULL);
Tcl_TraceVar(interp,"somevar",TCL_TRACE_READS,read_somevar,NULL);
}
Phew. We have come to the end of this little tutorial, I hope it has given you
some ideas for adding scripting to your own programs, in a simple and quite
powerful way. I wish you the best of luck and fruitful coding.
C Scene Official Web Site :
http://cscene.oftheinter.net
C Scene Official Email :
cscene@mindless.com
This page is Copyright © 1997 By
C Scene. All Rights Reserved