我正在尝试获取Tcl Interpreter的输出,如回答此问题Tcl C API: redirect stdout of embedded Tcl interp to a file without affecting the whole program中所述。而不是将数据写入文件我需要使用管道获取它。我将Tcl_OpenFileChannel
更改为Tcl_MakeFileChannel
并将管道的写入端传递给它。然后我用一些看法打电话给Tcl_Eval
。管道的读端没有数据。
#include <sys/wait.h>
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
#include <tcl.h>
#include <iostream>
int main() {
int pfd[2];
if (pipe(pfd) == -1) { perror("pipe"); exit(EXIT_FAILURE); }
/*
int saved_flags = fcntl(pfd[0], F_GETFL);
fcntl(pfd[0], F_SETFL, saved_flags | O_NONBLOCK);
*/
Tcl_Interp *interp = Tcl_CreateInterp();
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;
}
chan = Tcl_MakeFileChannel((void*)pfd[1], TCL_WRITABLE | TCL_READABLE);
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);
rc = Tcl_Eval(interp, "puts test");
if (rc != TCL_OK) {
fputs("Failed to eval", stderr);
return 2;
}
char buf;
while (read(pfd[0], &buf, 1) > 0) {
std::cout << buf;
}
}
答案 0 :(得分:1)
我现在没有时间修改代码(可能会在以后这样做),但我认为这种方法存在缺陷,因为我发现它存在两个问题:
如果stdout
连接到不是交互式控制台的东西(运行时通常会使用isatty(2)
调用来检查),那么完全缓冲可能是(和我认为将参与,所以除非您在嵌入式解释器中调用puts
输出如此多的字节,以填满或溢出Tcl的通道缓冲区(8KiB,ISTR),然后填充下游系统的缓冲区(请参阅下一点) ),我认为,它不会小于4KiB(典型硬件平台上单个内存页面的大小),读取端不会出现任何内容。
您可以通过将Tcl脚本更改为刷新stdout
来测试,如下所示:
puts one
flush stdout
puts two
然后能够读取管道读取端的第一个puts
输出的四个字节。
管道是通过缓冲区连接的两个FD(具有已定义但与系统相关的大小)。一旦写入端(您的Tcl interp)填满该缓冲区,写入调用将达到“缓冲区已满”条件将阻止写入过程,除非从读取端读取内容以释放缓冲区中的空格。由于读者是相同的过程,这样的条件有很好的死锁机会,因为一旦Tcl interp卡住试图写入stdout
,整个过程就会被卡住。 / p>
现在的问题是:这可以起作用吗?
第一个问题可能是通过在Tcl端关闭该通道的缓冲来部分修复。这(假设)不会影响系统为管道提供的缓冲。
第二个问题更难,我只能想到两种可能性来解决它:
创建一个管道然后fork(2)
子进程,确保其标准输出流连接到管道的写入端。然后将Tcl解释器嵌入到该进程中并且不对其中的stdout
流执行任何操作,因为它将隐式连接到附加到管道的子进程标准输出流。然后,您从管道中读取父进程,直到写入端关闭。
这种方法比使用线程更强大(参见下一点)但它有一个潜在的缺点:如果你需要以某种方式影响嵌入式Tcl解释器,这些方法在程序运行之前就不知道了(比如说) ,为了响应用户的操作,您必须在父进程和子进程之间设置某种IPC。
使用线程并将Tcl interp嵌入到一个单独的线程中:然后确保管道中的读取发生在另一个(让我们称之为“控制”)线程中。
这种方法可能表面上看起来比分支过程简单,但是你得到了与线程常见的正确同步相关的所有麻烦。例如,不能直接从创建interp的线程之外的线程访问Tcl解释器。由于可能存在TLS问题,这不仅意味着并发访问(这本身就很明显),而且包括任何访问,包括同步访问。 (我不确定这是否成立,但我觉得这是一大堆蠕虫。)
所以,说了这么多,我想知道为什么你似乎系统地拒绝为你的interp实现自定义“频道驱动程序”的建议,只是用它来为你的interp中的stdout
频道提供实现?这将创建一个超简单的单线程完全同步的实现。这种方法有什么问题,真的吗?
另外请注意,如果您决定使用管道,希望它将作为一种“匿名文件”,那么这是错误的:管道假定两侧并行工作。在你的代码中,你首先让Tcl interp写下它必须编写的所有内容,然后尝试阅读它。正如我所描述的那样,这就是在寻找麻烦,但如果这是为了不弄乱文件而发明的,那么你只是做错了,而在POSIX系统上,行动的过程可能是:
mkstemp()
创建并打开临时文件。立即使用返回的名称mkstemp()
删除它,代替您传递的模板。
由于文件仍然有一个打开的FD(由mkstemp()
返回),它将从文件系统中消失但不会被取消链接,并且可能被写入和读取。
stdout
。让interp写下它所拥有的一切。seek()
FD返回文件的开头并从中读取。