在您的情况下,困难在于 Tcl 解释器的标准通道与主程序(和 C 运行时)看到的标准流的文件描述符(FD)之间的交互,以及open(2)
Unix 中的语义。
使所有输出重定向的过程如下所示:
操作系统确保在程序开始执行时三个标准文件描述符 (FD) 已打开(编号为 0、1 和 2,其中 1 是标准输出)。
一旦您创建的 Tcl 解释器初始化了它的三个标准通道(当您调用Tcl_GetChannel()
“stdout”时就会发生这种情况,如此处所述),它们就会与主程序中已经存在的三个 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”通常更易于实现,如果您只想在您的 Tcl 解释器中重定向, stdout
并让其他两个标准通道连接到封闭程序使用的相同标准流,这种方法似乎是明智的聘请。可能的缺点是:
- 涉及太多魔法(建议对代码进行大量注释)。
- 不确定这将如何在 Windows 上工作:那里没有
dup(2)
(见下文)并且可能需要一些其他方法。
- 不使用封闭程序的标准流
stdin
和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)
封闭程序中的 FD 1 的 n (这就是最后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);
我还没有测试过这种方法。