获取Tcl解释器的输出

Getting the output of Tcl Interpreter

本文关键字:输出 解释器 Tcl 获取      更新时间:2023-10-16

我正在尝试获得Tcl解释器的输出,如回答这个问题所描述的Tcl C API:将嵌入式Tcl接口的stdout重定向到一个文件而不影响整个程序。而不是写入数据到文件,我需要得到它使用管道。我将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;
        }
}

我目前没有时间修补代码(可能稍后会这样做),但我认为这种方法是有缺陷的,因为我看到了两个问题:

  1. 如果stdout连接到不是交互式控制台的东西(对isatty(2)的调用通常由运行时使用来检查),则可以(并且我认为将)使用完整的缓冲,因此,除非您在嵌入式解释器中调用puts输出如此多的字节以填充或溢出Tcl的通道缓冲区(8KiB, ISTR),然后下游系统的缓冲区(见下一点),其中,我认为,不小于4KiB(典型HW平台上单个内存页的大小),则在读取端不会出现任何内容。

    您可以通过更改Tcl脚本以刷新stdout来进行测试,如下所示:

    puts one
    flush stdout
    puts two
    

    应该能够从管道的读端读取第一个puts输出的四个字节。

  2. 管道是通过缓冲区连接的两个fd(缓冲区的大小已定义,但与系统有关)。一旦写端(您的Tcl接口)填满了缓冲区,将达到"buffer full"条件的写调用将阻塞写进程,除非有东西从读端读取以释放缓冲区中的空间。由于读取器是同一个进程,这样的条件很有可能会死锁,因为一旦Tcl接口在试图写stdout时卡住了,整个进程就会卡住。

现在的问题是:这能起作用吗?

第一个问题可以通过在Tcl端关闭该通道的缓冲来部分修复。这(假定)不会影响系统为管道提供的缓冲。

第二个问题更难,我只能想到两种可能来解决它:

  1. 创建一个管道,然后fork(2)子进程,确保其标准输出流连接到管道的写端。然后将Tcl解释器嵌入到该进程中,并且不对其中的stdout流做任何操作,因为它将隐式地连接到附加到管道的子进程标准输出流。然后从管道读取父进程,直到写端关闭。

    这种方法比使用线程更健壮(参见下一点),但它有一个潜在的缺点:如果您需要以某种方式影响嵌入式Tcl解释器,而这些方式在程序运行之前是不知道的(例如,响应用户的操作),您将不得不在父进程和子进程之间设置某种IPC。

  2. 使用线程并将Tcl接口嵌入到一个单独的线程中:然后确保从管道中的读取发生在另一个(我们称之为"控制")线程中。

    这种方法可能表面上看起来比fork进程简单,但随后就会遇到与线程常见的适当同步相关的所有麻烦。例如,Tcl解释器必须不能从创建该解释器的线程以外的线程直接访问。这不仅意味着并发访问(这本身就很明显),还意味着任何访问,包括同步访问,因为可能存在TLS问题。(我不确定这是否正确,但我有一种感觉,这是一个大麻烦。)

所以,说了这么多,我想知道为什么你似乎系统地拒绝了为你的实习实现自定义"通道驱动程序"的建议,只是用它来为你的实习提供stdout通道的实现?这将创建一个超级简单的单线程全同步实现。这种方法到底有什么问题?

还要注意,如果您决定使用管道,希望它将作为一种"匿名文件",那么这是错误的:管道假设双方并行工作。在你的代码中,你首先让Tcl接口写所有它必须写的东西,然后试着读这个。正如我所描述的那样,这是自找麻烦,但如果这样做只是为了不弄乱文件,那么您就做错了,在POSIX系统上,操作过程可能是:

  1. 使用mkstemp()创建并打开一个临时文件。
  2. 立即删除它,使用名称mkstemp()来代替你传递给它的模板。

    由于文件仍然有一个打开的FD(由mkstemp()返回),它将从文件系统中消失,但不会被解除链接,并且可能被写入和读取。

  3. 使此FD为interp的stdout
  4. 间隔结束后,seek()将FD返回到文件的开头并从中读取。
  5. 完成后关闭FD —它在底层文件系统上占用的空间将被回收。