To return a result to Tcl the interp->result variable needs to be set.
This can be done in a variety of ways including setting the result manually or
using a function such as Tcl_SetResult
, Tcl_AppendResult
,
Tcl_ResetResult
or Tcl_DStringResult
.
However the choice of which to use is not as obvious as may first appear. A cautionary tale will illustrate some of the easy pitfalls. The following points are not made sufficiently clear in John Ousterhouts Tcl and Tk book. Additionally the problems are real and have been observed in the development of Gap4.
Consider the case where we have many commands registered with the interpreter. One such example could be:
int example(ClientData clientData, Tcl_Interp *interp, int argc, char **argv) { /* ... */ sprintf(interp->result, "%d", some_c_func()); return TCL_OK; }
Now deep within some_c_func
we have a Tcl_Eval
call which
happens to end with something like the following:
proc some_tcl_func {} { # ... set fred jim }
Due to the call of Tcl_Eval
in some_c_func
the
interp->result is now set to the last returned result, which is from the
set
command. In the above example interp->result points to 'jim'.
The sprintf
command in the example
function will overwrite this
string and hence change the value of the fred Tcl variable. This causes
confusion and in some cases may also cause memory corruption where data is
incorrectly freed.
The moral of this tale is to be extremely wary. As there is no knowledge of
what some_c_func
does (and remember it may get updated later) we seem
to trapped. One possible solution is to rewrite the example
function as
follows.
int example(ClientData clientData, Tcl_Interp *interp, int argc, char **argv) { int ret; /* ... */ ret = some_c_func(); Tcl_ResetResult(interp); sprintf(interp->result, "%d", ret); return TCL_OK; }
This leads to another pitfall. If we have 'sprintf(interp->result, "%d",
some_c_func(interp));
' and some_c_func
calls (possibly indirectly) the
Tcl_ResetResult
function then we'll be modifying the
interp->result address. This leads to undefined execution of code. (Is
sprintf
passed the original or final interp->result pointer?)
Therefore I'm inclined to think that we should never use
Tcl_ResetResult
except immediately before a modification of
interp->result in a separate C statement. My personal recommendation is
to never write directly to interp->result. Additionally never reset
interp->result to a new string unless interp->freeProc is also
updated correctly. In preference, use Tcl_SetResult
.
The Tcl_SetResult
function should always work fine, however it does not
take printf
style arguments. We have implemented a
vTcl_SetResult
which takes an interp argument and the standard
printf
format and additional arguments. For instance we would rewrite
the example function as the following
int example(ClientData clientData, Tcl_Interp *interp, int argc, char **argv) { int ret; /* ... */ vTcl_SetResult(interp, "%d", some_c_func()); return TCL_OK; }
As a final note on vTcl_SetResult
; the current implementation only
allows strings up to 8192 bytes. This should be easy to remedy if it causes
problems for other developers.