Tcl C API: redirect stdout of embedded Tcl interp to a file without affecting the whole program

前端 未结 3 401
终归单人心
终归单人心 2021-01-26 00:07
#include 
int main(int argc, char** argv)
{
    Tcl_Interp *interp = Tcl_CreateInterp();

    Tcl_Channel stdoutChannel = Tcl_GetChannel(interp, \"stdout\",         


        
相关标签:
3条回答
  • 2021-01-26 00:28

    The difficulty in your case is the interaction between the standard channels of a Tcl interpreter and the file descriptors (FDs) of the standard streams as seen by the main program (and the C runtime), coupled with the semantics of open(2) in Unix.

    The process which makes all your output redirected rolls like this:

    1. The OS makes sure the three standard file descriptors (FDs) are open (and numbered 0, 1 and 2, with 1 being the standard output) by the time the program starts executing.

    2. As soon as the Tcl interpreter you create initializes its three standard channels (this happens when you call Tcl_GetChannel() for "stdout", as described here), they get associated with those already existing three FDs in the main program.

      Note that the underlying FDs are not cloned, instead, they are just "borrowed" from the enclosing program. In fact, I think in 99% of cases this is a sensible thing to do.

    3. When you close (which happend when unregisteting) the standard channel stdout in your Tcl interpreter, the underlying FD (1) is closed as well.

    4. The call to fopen(3) internally calls open(2) which picks up the lowest free FD, which is 1, and thus the standard output stream as understood by the main program (and the C runtime) is now connected to that opened file.

    5. You then create a Tcl channel out of your file and register it with the interpreter. The channel indeed becomes stdout for the interpreter.

    In the end, both writes to the standard output stream in your main program and writes to the standard output channel in your Tcl interpreter are sent do the same underlying FD and hence end up in the same file.

    I can see two ways to deal with this behaviour:

    • Play a neat trick to "reconnect" the FD 1 to the same stream it was initially opened to and make the file opened for the Tcl interpreter's stdout use an FD greater than 2.
    • Instead of first letting the Tcl interpreter initialize its standard channels and then reinitializing one of them, initialize them all manually before letting that auto-vivification machinery kick in.

    Both approaches have their pros and cons:

    • "Preserving FD 1" is generally simpler to implement, and if you want to redirect only stdout in your Tcl interpreter, and leave the two other standard channels to be connected to the same standard streams used by the enclosing program, this approach seems to be sensible to employ. The possible downsides are:

      • Too much magic involved (extensive commenting the code is advised).
      • Not sure how this would work on Windows: there's no dup(2) there (see below) and some other approach might be needed.
      • Not using the standard streams for stdin and stderr from the enclosing program might be useful.
    • Initializing the standard channels in the Tcl interpreter by hand requires more code and supposedly warrants the correct ordering (stdin, stdout, stderr, in that order). If you want the remaining two standard channels in your Tcl interpreter to be connected to the matching streams of the enclosing program, this approach is more work; the first approach does this for free.

    Here's how to preserve FD 1 to make only stdout in the Tcl interpreter be connected to a file; for the enclosing program FD 1 is still connected to the same stream as set up by the OS.

    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    
    #include <tcl.h>
    
    int redirect(Tcl_Interp *interp)
    {
            Tcl_Channel chan;
            int rc;
            int fd;
    
            /* Get the channel bound to stdout.
             * Initialize the standard channels as a byproduct
             * if this wasn't already done. */
            chan = Tcl_GetChannel(interp, "stdout", NULL);
            if (chan == NULL) {
                    return TCL_ERROR;
            }
    
            /* Duplicate the descriptor used for stdout. */
            fd = dup(1);
            if (fd == -1) {
                    perror("Failed to duplicate stdout");
                    return TCL_ERROR;
            }
    
            /* Close stdout channel.
             * As a byproduct, this closes the FD 1, we've just cloned. */
            rc = Tcl_UnregisterChannel(interp, chan);
            if (rc != TCL_OK)
                    return rc;
    
            /* Duplicate our saved stdout descriptor back.
             * dup() semantics are such that if it doesn't fail,
             * we get FD 1 back. */
            rc = dup(fd);
            if (rc == -1) {
                    perror("Failed to reopen stdout");
                    return TCL_ERROR;
            }
    
            /* Get rid of the cloned FD. */
            rc = close(fd);
            if (rc == -1) {
                    perror("Failed to close the cloned FD");
                    return TCL_ERROR;
            }
    
            /* Open a file for writing and create a channel
             * out of it. As FD 1 is occupied, this FD won't become
             * stdout for the C code. */
            chan = Tcl_OpenFileChannel(interp, "aaa.txt", "w", 0666);
            if (chan == NULL)
                    return TCL_ERROR;
    
            /* Since stdout channel does not exist in the interp,
             * this call will make our file channel the new stdout. */
            Tcl_RegisterChannel(interp, chan);
    
            return TCL_OK;
    }
    int main(void)
    {
            Tcl_Interp *interp;
            int rc;
    
            interp = Tcl_CreateInterp();
            rc = redirect(interp);
            if (rc != TCL_OK) {
                    fputs("Failed to redirect stdout", stderr);
                    return 1;
            }
            puts("before");
            rc = Tcl_Eval(interp, "puts stdout test");
            if (rc != TCL_OK) {
                    fputs("Failed to eval", stderr);
                    return 2;
            }
            puts("after");
    
            Tcl_Finalize();
    
            return 0;
    }

    Building and running (done in Debian Wheezy):

    $ gcc -W -Wall -I/usr/include/tcl8.5 -L/usr/lib/tcl8.5 -ltcl main.c
    $ ./a.out 
    before
    after
    $ cat aaa.txt 
    test
    

    As you can see, the string "test" output by puts goes to the file while the strings "before" and "after", which are write(2)n to FD 1 in the enclosing program (this is what puts(3) does in the end) go to the terminal.

    The hand-initialization approach would be something like this (sort of pseudocode):

    Tcl_Channel stdin, stdout, stderr;
    
    stdin = Tcl_OpenFileChannel(interp, "/dev/null", "r", 0666);
    stdout = Tcl_OpenFileChannel(interp, "aaa.txt", "w", 0666);
    stderr = Tcl_OpenFileChannel(interp, "/dev/null", "w", 0666);
    Tcl_RegisterChannel(interp, stdin);
    Tcl_RegisterChannel(interp, stdout);
    Tcl_RegisterChannel(interp, stderr);
    

    I have not tested this approach though.

    0 讨论(0)
  • 2021-01-26 00:30

    At the level of the C API, and assuming that you are on a Unix-based OS (i.e., not Windows), you can do this far more simply by using the right OS calls:

    #include <fcntl.h>
    #include <unistd.h>
    
    // ... now inside a function
    
        int fd = open("/home/aminasya/nlb_rundir/imfile", O_WRONLY|O_CREAT, 0744);
        // Important: deal with errors here!
    
        dup2(fd, STDOUT_FILENO);
        close(fd);
    

    You could also use dup() to save the old stdout (to an arbitrary number that Tcl will just ignore) so that you can restore it later, if desired.

    0 讨论(0)
  • 2021-01-26 00:37

    Try this:

    FILE *myfile = fopen("myfile", "W+");
    Tcl_Interp *interp = Tcl_CreateInterp(); 
    Tcl_Channel myChannel = Tcl_MakeFileChannel(myfile, TCL_WRITABLE);
    Tcl_RegisterChannel(myChannel);
    Tcl_SetStdChannel(myChannel, TCL_STDOUT);
    

    You need to register the channel with the interpreter before you can reset the std channel to use it.

    0 讨论(0)
提交回复
热议问题