Cplusplus streams and Tcl channels

KBK (8 January 2001) --

Purpose: Provide sample code for hooking up C++ iostreams and Tcl channels.

One thing that annoys C++ programmers who integrate their code with the Tcl system is that C++ stream buffers and Tcl I/O channels are totally divorced from one another. The C++ programmer naturally expects that

    cout << "Hello, world!" << endl;

will result in a message on the Wish console, for instance. This does not happen by default.

The problem is worst on Windows. On Windows, if Wish is launched in the usual way, the line shown above will cause a nasty program crash because there is no cout stream at all.

It turns out that the low-level iostream mechanisms can be programmed to use Tcl channels. The attached files, TclChannelStreambuf.h and TclChannelStreambuf.cpp, make this attachment. All that a C++ extension must do is to call TclConsoleStreambufSetup out of its initialization, and the cerr and cout streams will designate the Tcl stdout and stderr channels.

Alas, cin is more of a problem. One could develop an extension to TkCon to handle it, but I never did.

NOTE: I cut and pasted these files out of a larger app, and I haven't tried compiling them since; I haven't the time right now. Feel free to e-mail if you have trouble or to post a followup with fixes.


 // -*- C++ -*-

 //  NOTE: One leading blank has been added to each line of this file
 //        to cope with the demands of the Wiki.  This blank should
 //        be deleted before trying to compile.

 /*
 ## File: TclChannelStreambuf.h
 ##
 ## Synopsis:
 ##      Class to support redirection of a C++ stream to a Tcl channel
 ##      (particularly, routing cin/cout/cerr to the console).
 #@CDef
 */

 #ifndef __TclChannelStreambuf_h
 #define __TclChannelStreambuf_h 1

 extern "C" {
 #include <tcl.h>
 }

 #include <iostream.h>
 #ifdef _MSVC_VER
 #include <streamb.h>
 #else
 #include <streambuf.h>
 #endif

 class TclChannelStreambuf : public streambuf {
 private:
     Tcl_Channel channel_;
     Tcl_DString iBuffer_;
     int iOffset_;
 public:
     TclChannelStreambuf (Tcl_Channel);
     ~TclChannelStreambuf ();
     virtual int overflow (int);
     virtual int underflow ();
     virtual int sync ();
 };

 extern "C" void TclConsoleStreambufSetup _ANSI_ARGS_(( void ));

 #endif /* __TclChannelStreambuf_h */

 // -*- C++ -*-

 //  NOTE: One leading blank has been added to each line of this file
 //        to cope with the demands of the Wiki.  This blank should
 //        be deleted before trying to compile.

 /*
 ## File: TclChannelStreambuf.cpp
 ##
 ## Synopsis:
 ##     Class to support redirection of a C++ stream to a Tcl channel
 ##     (particularly, routing cin/cout/cerr to the console).
 */

 #include "TclChannelStreambuf.h"
 #include <memory.h>

 /*
 ## Function: TclChannelStreambuf::TclChannelStreambuf
 ##
 ## Synopsis:
 ##     Associate a Tcl channel with a streambuf.
 */

 TclChannelStreambuf::TclChannelStreambuf (Tcl_Channel channel)
     : channel_ (channel)
 {
     Tcl_DStringInit (&iBuffer_);
     iOffset_ = 0;
 }

 /*
 ## Function: TclChannelStreambuf::~TclChannelStreambuf
 ##
 ## Synopsis:
 ##     Destroy a streambuf associated with a Tcl channel.
 */

 TclChannelStreambuf::~TclChannelStreambuf ()
 {
     Tcl_DStringFree (&iBuffer_);
 }

 /*
 ## Function: TclChannelStreambuf::overflow
 ##
 ## Synopsis:
 ##     Handle a full output buffer on a streambuf writing to a Tcl channel.
 */

 int
 TclChannelStreambuf::overflow (int c)
 {
     int status;

     // Allocate reserve area if necessary

     if (base() == 0) {
        doallocate ();
     }

     // If there's no output buffer, allocate one.  Place it after the
     // end of the input buffer if there's input.

     if (!pbase ()) {
        if (egptr () > gptr ()) {
            setp (egptr (), ebuf ());
        } else {
            setp (base (), ebuf ());
        }
     }

     // If there's stuff in the output buffer, write it to the channel.

     if (pptr () > pbase ()) {
        status = Tcl_Write (channel_, pbase (), pptr () - pbase ());
        if (status < (pptr () - pbase ())) {
            return EOF;
        }
        setp (pbase (), ebuf ());
     }

     // Save the next character in the output buffer.  If there is none,
     // flush the channel

     if (c != EOF) {
        *(pptr()) = c;
        pbump (1);
        return c;
     } else {
        setp (0, 0);
        status = Tcl_Flush (channel_);
        if (status != TCL_OK) return EOF;
        return 0;
     }
 }

 /*
 ## Function: TclChannelStreambuf::underflow
 ##
 ## Synopsis:
 ##     Get a fresh input buffer from a Tcl channel for a streambuf
 */

 int
 TclChannelStreambuf::underflow ()
 {

     // Nothing to do if the buffer hasn't underflowed.

     if (egptr() > gptr()) {
        return *gptr ();
     }

     // Make sure we have a reserve area

     if (!base()) {
        doallocate ();
     }

     // Flush any pending output

     if (pptr () > pbase ()) {
        if (overflow (EOF) == EOF) {
            return EOF;
        }
     }

     // Get a fresh line of input if needed

     if (iOffset_ >= Tcl_DStringLength (&iBuffer_)) {
        if (Tcl_Gets (channel_, &iBuffer_)) {
            return EOF;
        }
        Tcl_DStringAppend (&iBuffer_, "\n", 1);
     }

     // Determine how much input to transfer.  Don't fill the reserve
     // area more than half full

     size_t xferlen = Tcl_DStringLength (&iBuffer_);
     if ((long) xferlen > (ebuf () - base ()) / 2) {
        xferlen = (ebuf () - base ()) / 2;
     }

     // Copy string into the buffer, and advance pointers

     memcpy ((void *) base (), (void *) Tcl_DStringValue (&iBuffer_), xferlen);
     iOffset_ += xferlen;
     setg (base (), base (), base () + xferlen);

     // Free the input string if we're finished with it

     if (iOffset_ >= Tcl_DStringLength (&iBuffer_)) {
        Tcl_DStringFree (&iBuffer_);
        iOffset_ = 0;
     }

     // Return the first character read.

     return *gptr ();
 }

 /*
 ## Function: TclChannelStreambuf::sync
 ##
 ## Synopsis:
 ##     Synchronize input and output on a Tcl channe associated with
 ##     a streambuf
 */

 int
 TclChannelStreambuf::sync () {
     // Flush output

     if (overflow (EOF) == EOF) {
        return EOF;
     }

     // Discard input
     setg (0, 0, 0);

     return 0;
 }

 /*
 ## Function: TclConsoleStreambufSetup
 ##
 ## Synopsis:
 ##     Bind the standard streams to the Tcl console
 */

 extern "C" void
 TclConsoleStreambufSetup () {
     cin = new TclChannelStreambuf (Tcl_GetStdChannel (TCL_STDIN));
     cout = new TclChannelStreambuf (Tcl_GetStdChannel (TCL_STDOUT));
     cerr = new TclChannelStreambuf (Tcl_GetStdChannel (TCL_STDERR));
     cerr << "C++ standard input and output now on the Tcl console" << endl;
 }

SLB The C++ streams interface changed during the ISO standardisation process. With modern streams implementations, statements such as:

     cout = new TclChannelStreambuf (Tcl_GetStdChannel (TCL_STDOUT));

probably won't compile. You can fix this by changing it to:

     cout.rdbuf(new TclChannelStreambuf (Tcl_GetStdChannel (TCL_STDOUT)));

Some variation on this will be needed for stdin, I haven't investigated that.


CJL - 2006/11/01 A question has been posted to comp.lang.tcl (titled "C++ streams to Tcl channels") asking for somebody with an understanding of the inner workings of Tcl and C++ streambuffers to update this.

KBK - Ugh.

I wrote that page nearly six years ago, but the code is several years older than that. I daresay that something similar could be made to work with std::iostream, but for *me* to do it, I'd have to relearn how the ISO streams work. It would most likely be several months before I could look at it; life gets in the way.

If someone else wants to take on the job, I'd be happy to advise.


CJL - Moved a general request for help to 'Ask #5' ([L1 ]) where it will get a bigger audience