问题
#include <tcl.h>
int main(int argc, char** argv)
{
Tcl_Interp *interp = Tcl_CreateInterp();
Tcl_Channel stdoutChannel = Tcl_GetChannel(interp, "stdout", NULL);
Tcl_UnregisterChannel(interp, stdoutChannel);
Tcl_Channel myChannel = Tcl_OpenFileChannel(interp, "/home/aminasya/nlb_rundir/imfile", "w", 0744);
Tcl_RegisterChannel(interp, myChannel);
Tcl_Eval(interp, "puts hello");
}
In this code I have tried to close stdout channel and redirect it to file. (As described Get the output from Tcl C Procedures). After running, "imfile" is created but empty. What am doing wrong?
I have seen How can I redirect stdout into a file in tcl too, but I need to do it using Tcl C API.
I have also tried this way, but again no result.
FILE *myfile = fopen("myfile", "W+");
Tcl_Interp *interp = Tcl_CreateInterp();
Tcl_Channel myChannel = Tcl_MakeFileChannel(myfile, TCL_WRITABLE);
Tcl_SetStdChannel(myChannel, TCL_STDOUT);
回答1:
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:
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.
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.
When you close (which happend when unregisteting) the standard channel
stdout
in your Tcl interpreter, the underlying FD (1) is closed as well.The call to
fopen(3)
internally callsopen(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.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
andstderr
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.
回答2:
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.
回答3:
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.
来源:https://stackoverflow.com/questions/16063303/tcl-c-api-redirect-stdout-of-embedded-tcl-interp-to-a-file-without-affecting-th