您的情况的困难在于 Tcl 解释器的标准通道与主程序(和 C 运行时)所看到的标准流的文件描述符 (FD) 之间的交互,以及以下语义:open(2)
在Unix中。
使所有输出重定向的过程如下所示:
操作系统确保在程序开始执行时三个标准文件描述符 (FD) 已打开(编号为 0、1 和 2,其中 1 为标准输出)。
-
一旦您创建的 Tcl 解释器初始化其三个标准通道(当您调用Tcl_GetChannel()
对于“stdout”,如上所述here http://www.tcl.tk/man/tcl8.6/TclLib/StdChannels.htm),它们与主程序中已经存在的三个 FD 相关联。
请注意,底层 FD 不是克隆的,相反,它们只是从封闭程序“借用”的。事实上,我认为在 99% 的情况下这是明智的做法。
当您关闭(注销时发生)标准频道时stdout
在您的 Tcl 解释器中,底层 FD (1) 也已关闭。
致电给fopen(3)
内部调用open(2)
它获取最低的空闲 FD,即 1,因此主程序(和 C 运行时)所理解的标准输出流现在连接到该打开的文件。
然后,您从文件中创建一个 Tcl 通道并将其注册到解释器中。频道确实变成了stdout
对于口译员来说。
最后,对主程序中的标准输出流的写入和对 Tcl 解释器中的标准输出通道的写入都会发送到相同的底层 FD,因此最终会出现在同一个文件中。
我可以看到两种方法来处理这种行为:
- 使用一个巧妙的技巧将 FD 1“重新连接”到最初打开的同一流,并使文件为 Tcl 解释器打开
stdout
使用大于 2 的 FD。
- 不要先让 Tcl 解释器初始化其标准通道,然后重新初始化其中一个通道,而是在让自动激活机制启动之前手动初始化所有通道。
两种方法都有其优点和缺点:
-
“保留 FD 1”通常更容易实现,并且如果您想重定向only stdout
在你的 Tcl 解释器中,并让其他两个标准通道连接到封闭程序使用的相同标准流,这种方法似乎是明智的采用。可能的缺点是:
- 涉及太多魔法(建议对代码进行大量注释)。
- 不确定这在 Windows 上如何工作:没有
dup(2)
那里(见下文)并且可能需要一些其他方法。
-
Not使用标准流
stdin
and stderr
随附的程序可能会有用。
手动初始化 Tcl 解释器中的标准通道需要更多代码和据推测保证正确的排序(stdin
, stdout
, stderr
, 以该顺序)。如果您希望 Tcl 解释器中的其余两个标准通道连接到封闭程序的匹配流,则此方法需要更多工作;第一种方法是免费的。
以下是如何保存 FD 1 以仅使stdout
在 Tcl 解释器中连接到文件;对于封闭程序 FD 1 仍然连接到操作系统设置的同一流。
#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;
}
构建和运行(在 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
正如你所看到的,字符串“test”输出puts
转到文件,同时字符串“before”和“after”,它们是write(2)
n 到封闭程序中的 FD 1 (这就是puts(3)
最后)去终端。
手动初始化方法将是这样的(有点伪代码):
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);
不过我还没有测试过这种方法。