在您的情况下,困难在于 Tcl 解释器的标准通道与主程序(和 C 运行时)看到的标准流的文件描述符 (FD) 之间的交互,以及 @987654322 的语义@ 在 Unix 中。
使所有输出重定向的过程如下所示:
-
操作系统确保在程序开始执行时三个标准文件描述符 (FD) 已打开(编号为 0、1 和 2,其中 1 为标准输出)。
李>
-
一旦您创建的 Tcl 解释器初始化了它的三个标准通道(当您为“stdout”调用Tcl_GetChannel() 时会发生这种情况,如here 所述),它们就会与主程序中已经存在的三个 FD 相关联.
请注意,底层的 FD 不是克隆的,它们只是从封闭的程序中“借来的”。事实上,我认为在 99% 的情况下这是明智的做法。
当您在 Tcl 解释器中关闭(在取消注册时发生)标准通道 stdout 时,底层 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 以使 Tcl 解释器中的 stdout 仅连接到文件;对于封闭程序 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
如您所见,puts 输出的字符串“test”进入文件,而字符串“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);
不过我还没有测试过这种方法。