【问题标题】:Tcl C API: redirect stdout of embedded Tcl interp to a file without affecting the whole programTcl C API:将嵌入式 Tcl interp 的 stdout 重定向到文件而不影响整个程序
【发布时间】:2013-04-10 09:47:33
【问题描述】:
#include <tcl.h>
int main(int argc, char** argv)
{
    Tcl_Interp *interp = Tcl_CreateInterp();

    Tcl_Channel stdoutChannel = Tcl_GetChannel(interp, "stdout", NULL);
    Tcl_UnregisterChannel(interp, stdoutChannel);

    Tcl_Channel myChannel = Tcl_OpenFileChannel(interp, "/home/aminasya/nlb_rundir/imfile", "w", 0744);

    Tcl_RegisterChannel(interp, myChannel);
    Tcl_Eval(interp, "puts hello");
}

在这段代码中,我尝试关闭标准输出通道并将其重定向到文件。 (如Get the output from Tcl C Procedures 所述)。运行后,“imfile”被创建但为空。做错了什么?

我也见过How can I redirect stdout into a file in tcl,但我需要使用 Tcl C API 来完成。

我也试过这种方法,但还是没有结果。

FILE *myfile = fopen("myfile", "W+");
Tcl_Interp *interp = Tcl_CreateInterp(); 
Tcl_Channel myChannel = Tcl_MakeFileChannel(myfile, TCL_WRITABLE);
Tcl_SetStdChannel(myChannel, TCL_STDOUT);

【问题讨论】:

    标签: tcl channel


    【解决方案1】:

    试试这个:

    FILE *myfile = fopen("myfile", "W+");
    Tcl_Interp *interp = Tcl_CreateInterp(); 
    Tcl_Channel myChannel = Tcl_MakeFileChannel(myfile, TCL_WRITABLE);
    Tcl_RegisterChannel(myChannel);
    Tcl_SetStdChannel(myChannel, TCL_STDOUT);
    

    您需要先向解释器注册通道,然后才能重置 std 通道以使用它。

    【讨论】:

      【解决方案2】:

      在您的情况下,困难在于 Tcl 解释器的标准通道与主程序(和 C 运行时)看到的标准流的文件描述符 (FD) 之间的交互,以及 @987654322 的语义@ 在 Unix 中。

      使所有输出重定向的过程如下所示:

      1. 操作系统确保在程序开始执行时三个标准文件描述符 (FD) 已打开(编号为 0、1 和 2,其中 1 为标准输出)。

        李>
      2. 一旦您创建的 Tcl 解释器初始化了它的三个标准通道(当您为“stdout”调用Tcl_GetChannel() 时会发生这种情况,如here 所述),它们就会与主程序中已经存在的三个 FD 相关联.

        请注意,底层的 FD 不是克隆的,它们只是从封闭的程序中“借来的”。事实上,我认为在 99% 的情况下这是明智的做法。

      3. 当您在 Tcl 解释器中关闭(在取消注册时发生)标准通道 stdout 时,底层 FD (1) 也会关闭。

      4. fopen(3) 的调用在内部调用open(2),它获取最低的空闲FD,即1,因此主程序(和C 运行时)理解的标准输出流现在已连接到那个打开的文件。

      5. 然后您从您的文件中创建一个 Tcl 通道并将其注册到解释器。对于解释器,频道确实变成了stdout

      最后,写入主程序中的标准输出流和写入 Tcl 解释器中的标准输出通道都发送执行相同的底层 FD,因此最终在同一个文件中。

      我可以看到两种处理这种行为的方法:

      • 玩一个巧妙的技巧,将 FD 1 “重新连接”到它最初打开的同一流,并使为 Tcl 解释器的 stdout 打开的文件使用大于 2 的 FD。
      • 与其先让 Tcl 解释器初始化其标准通道,然后再重新初始化其中一个,不如手动初始化它们,然后让自动激活机制启动。

      这两种方法各有利弊:

      • “保留 FD 1”通常更容易实现,如果您想在 Tcl 解释器中重定向 stdout,并让其他两个标准通道连接到相同的标准封闭程序使用的流,这种方法似乎很明智。可能的缺点是:

        • 涉及太多魔法(建议对代码进行大量注释)。
        • 不确定这在 Windows 上如何工作:那里没有 dup(2)(见下文),可能需要一些其他方法。
        • 使用来自封闭程序的stdinstderr 的标准流可能会有用。
      • 在 Tcl 解释器中手动初始化标准通道需要更多代码,应该保证正确的顺序(stdinstdoutstderr,按此顺序)。如果您希望 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);
      

      不过我还没有测试过这种方法。

      【讨论】:

        【解决方案3】:

        在 C API 级别,假设您使用的是基于 Unix 的操作系统(即,不是 Windows),您可以通过使用正确的操作系统调用更简单地做到这一点:

        #include <fcntl.h>
        #include <unistd.h>
        
        // ... now inside a function
        
            int fd = open("/home/aminasya/nlb_rundir/imfile", O_WRONLY|O_CREAT, 0744);
            // Important: deal with errors here!
        
            dup2(fd, STDOUT_FILENO);
            close(fd);
        

        您还可以使用dup() 保存旧的标准输出(保存到 Tcl 将忽略的任意数字),以便您以后可以根据需要恢复它。

        【讨论】:

        • 我认为这样所有的标准输出都会被重定向到那个文件,但我只需要重定向 tcl 解释器的输出
        • 事实上我需要得到解释器的输出。我将使用管道命令并传递它​​而不是文件描述符来完成它。这是正确的方法吗?
        • @Ashot,不,我认为要走的路是创建一个自定义“通道驱动程序”,然后将其“类型”传递给Tcl_CreateChannel(),并将通道注册为stdout使用我对这个问题的回答中解释的一种技术。现在,每当您的 interp 中的某些内容写入stdout 时,您的“通道类型”过程就会被调用,您可以随意处理传递给它的数据。请仔细阅读this
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2011-04-22
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多