==============================================================================
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