【问题标题】:Getting the output of Tcl Interpreter获取 Tcl 解释器的输出
【发布时间】:2013-05-08 00:09:40
【问题描述】:

我正在尝试获取 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;
        }

}

【问题讨论】:

  • @kostix 你能看看这个吗?
  • 这看起来非常复杂。在子流程中运行一些东西会更容易吗?

标签: c++ tcl


【解决方案1】:

我目前没有时间修改代码(以后可能会这样做),但我认为这种方法存在缺陷,因为我发现它存在两个问题:

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

    您可以通过将 Tcl 脚本更改为刷新 stdout 来测试这一点,如下所示:

    puts one
    flush stdout
    puts two
    

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

  2. 管道是通过缓冲区连接的两个 FD(具有已定义但取决于系统的大小)。一旦写入端(您的 Tcl 解释器)填满该缓冲区,将达到“缓冲区已满”条件的写入调用将阻塞写入过程,除非从读取端读取某些内容以释放缓冲区中的空间。由于阅读器是同一个进程,这种情况极有可能陷入死锁,因为一旦 Tcl 插入程序在尝试写入 stdout 时卡住,整个进程就会卡住。

现在的问题是:这是否可行?

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

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

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

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

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

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

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

还要注意,如果您决定使用管道,希望它可以用作一种“匿名文件”,那么这是错误的:管道假定双方并行工作。在您的代码中,您首先让 Tcl interp 编写它必须编写的所有内容,然后尝试读取它。正如我所描述的,这是自找麻烦,但如果这是为了不弄乱文件而发明的,那么你只是做错了,在 POSIX 系统上,操作过程可能是:

  1. 使用mkstemp() 创建并打开一个临时文件。
  2. 立即使用返回的名称mkstemp() 代替您传递的模板将其删除。

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

  3. 将此 FD 设为 interp 的 stdout。让 interp 写下它必须写的一切。
  4. interp 完成后,seek() FD 回到文件开头并从中读取。
  5. 完成后关闭 FD — 它在底层文件系统上占用的空间将被回收。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2011-05-31
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多