Tcl was designed from the beginning to be easy to embed in compiled programs. But sometimes -- particularly in event-driven GUI programs -- it is desirable to capture the output Tcl normally sends to the stdout and stderr I/O channels. The channel IO system in Tcl is described in exhaustive detail in the Tcl manual https://www.tcl-lang.org/man/tcl8.5/TclLib/CrtChannel.htm but it's one of those manual pages that can't be understood until you know enough about the subject that you don't need the manual page.
This C++ program demonstrates the minimum work necessary to properly catch Tcl stdout and stderr channels. This is only one small part of a huge topic, but since non one else has pulled this information together in a clear example elsewhere, I think people should find it to be of some use.
#include <iostream>
#include <tcl.h>
//
// Example class to catch stdout and stderr channel output.
//
// In the real world, this would be a GUI class (in Qt, KWWidgets etc)
// that makes the proper API calls to display the output in the right
// widget.
class TclIOCatcher
{
public:
void outputText(const char *buf, int toWrite)
{
std::cout << "-----TclIOCatcher--------------" << std::endl;
std::cout.write(buf,toWrite);
std::cout << std::endl << "---------------------"
<< std::endl;
}
};
//
// Tcl is pure C, and this is a C++ program; to ensure proper
// calling linkage, encapsulate callbacks in a extern "C" section.
extern "C"
{
// outputproc is callback used by channel to handle data to outpu
static int outputproc(ClientData instanceData,
CONST84 char *buf, int toWrite, int *errorCodePtr)
{
// instanceData in this case is a pointer to a class instance
TclIOCatcher *qd =
reinterpret_cast<TclIOCatcher *>(instanceData);
qd->outputText(buf,toWrite);
return toWrite;
}
// inputproc doesn't do anything in an output-only channel.
static int
inputproc(ClientData instancedata, char *buf, int toRead, int *errorCodePtr)
{
return TCL_ERROR;
}
// nothing to do on close
static int
closeproc(ClientData instancedata, Tcl_Interp *interp)
{
return 0;
}
// no options for this channel
static int
setoptionproc(ClientData instancedata, Tcl_Interp *interp,
CONST84 char *optionname, CONST84 char *value)
{
return TCL_OK;
}
// for non-blocking I/O, callback when data is ready.
static void
watchproc(ClientData instancedata, int mask)
{
/* not much to do here */
return;
}
// gethandleproc -- retrieves device-specific handle, not applicable here.
static int
gethandleproc(ClientData instancedata, int direction, ClientData *handlePtr)
{
return TCL_ERROR;
}
// Tcl Channel descriptor type.
// many procs can be left NULL, and for our purposes
// are left so.
Tcl_ChannelType TclChan = {
"tclIOTestChan", /* typeName */
TCL_CHANNEL_VERSION_4, /* channel type version */
closeproc, /* close proc */
inputproc, /* input proc */
outputproc, /* output proc */
NULL, /* seek proc - can be null */
setoptionproc, /* set option proc - can be null */
NULL, /* get option proc - can be null */
watchproc, /* watch proc */
gethandleproc, /* get handle proc */
NULL, /* close 2 proc - can be null */
NULL, /* block mode proc - can be null */
NULL, /* flush proc - can be null */
NULL, /* handler proc - can be null */
NULL, /* wide seek proc - can be null if seekproc is*/
NULL /* thread action proc - can be null */
};
}
int
main(int argc, char **argv)
{
// create instance of the Tcl interpreter
Tcl_Interp *interp(Tcl_CreateInterp());
Tcl_Init(interp);
// class object to catch output
TclIOCatcher test;
// create a new channel for stdout
Tcl_Channel m_Out =
Tcl_CreateChannel(&TclChan,
"testout",
&test,TCL_WRITABLE);
//
// IMPORTANT -- tcl Channels do buffering, so
// the output catcher won't get called until a buffer
// is filled (default 4K bytes).
// These settings are stolen from TkWish.
Tcl_SetChannelOption(NULL,m_Out,
"-translation", "lf");
Tcl_SetChannelOption(NULL,m_Out,
"-buffering", "none");
Tcl_SetChannelOption(NULL,m_Out,
"-encoding", "utf-8");
//
// make this new channel the standard output channel.
Tcl_SetStdChannel(m_Out,TCL_STDOUT);
//
// I'm not sure why this is necessary, but apparently it has
// something to do with how reference counting inside the interpeter works.
Tcl_RegisterChannel(0,m_Out);
//
// do all the same stuff for stderr. In our case, we push the
// output all to the same place, but you could handle it seperately.
Tcl_Channel m_Err =
Tcl_CreateChannel(&TclChan,
"testerr",
&test,TCL_WRITABLE);
Tcl_SetChannelOption(NULL,m_Err,
"-translation", "lf");
Tcl_SetChannelOption(NULL,m_Err,
"-buffering", "none");
Tcl_SetChannelOption(NULL,m_Err,
"-encoding", "utf-8");
Tcl_SetStdChannel(m_Err,TCL_STDERR);
Tcl_RegisterChannel(0,m_Err);
//
// run one command to demonstrate how it works
const char testcommand[] = "puts $tcl_version";
int result = Tcl_EvalEx(interp,testcommand,strlen(testcommand),0);
// show the result, should be zero.
std::cout << "Result = " << result << std::endl;
exit(result);
}ajmilford - 2012-07-03 07:51:08
I found the lines
Tcl_RegisterChannel(0,m_Out); : Tcl_RegisterChannel(0,m_Err);
should be
Tcl_RegisterChannel(interp,m_Out); : Tcl_RegisterChannel(interp,m_Err);
to get this to work