Tcl C API:将嵌入式Tcl interp的stdout重定向到文件而不影响整个程序

时间:2013-04-17 14:51:06

标签: tcl channel

#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");
}

在这段代码中,我试图关闭stdout频道并将其重定向到文件。 (如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);

3 个答案:

答案 0 :(得分:3)

您的案例中的难点是Tcl解释器的标准通道与标准流的文件描述符(FD)之间的交互,如主程序(和C运行时)所见,再加上{{的语义Unix中的1}}。

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

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

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

    请注意,基础FD不会被克隆,相反,它们只是从封闭程序中“借用”。事实上,我认为在99%的情况下这是明智之举。

  3. 当您关闭(取消注册时发生)Tcl解释器中的标准通道Tcl_GetChannel()时,基础FD(1)也将关闭。

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

  5. 然后从文件中创建一个Tcl通道并将其注册到解释器。对于翻译来说,频道确实变为open(2)

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

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

    • 播放一个巧妙的技巧,将FD 1“重新连接”到最初打开的同一个流中,并为Tcl解释器的stdout打开文件,使用大于2的FD。
    • 不是先让Tcl解释器初始化其标准通道然后重新初始化其中一个,而是在让自动生成机器启动之前手动初始化它们。

    这两种方法都有其优点和缺点:

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

      • 涉及太多魔法(建议广泛评论代码)。
      • 不确定这在Windows上是如何工作的:那里没有stdout(见下文),可能还需要其他方法。
      • 使用封闭程序中dup(2)stdin的标准流可能会有用。
    • 手动初始化Tcl解释器中的标准通道需要更多代码,据称保证正确排序(stderrstdinstdout , 以该顺序)。如果您希望Tcl解释器中剩余的两个标准通道连接到封闭程序的匹配流,则此方法更有效;第一种方法是免费的。

    以下是如何保留FD 1以使Tcl解释器中只有stderr连接到文件;对于封闭程序,FD 1仍然连接到由OS设置的相同流。

    stdout

    构建和运行(在Debian Wheezy中完成):

    #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;
    }

    正如您所看到的,$ gcc -W -Wall -I/usr/include/tcl8.5 -L/usr/lib/tcl8.5 -ltcl main.c $ ./a.out before after $ cat aaa.txt test 输出的字符串“test”进入文件,而字符串“before”和“after”,在封闭程序中为puts n到FD 1 (这是write(2)最终做的事情)去终端。

    手工初始化方法将是这样的(某种伪代码):

    puts(3)

    我没有测试过这种方法。

答案 1 :(得分:0)

在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将忽略的任意数字),以便您可以在以后恢复它,如果需要。

答案 2 :(得分:0)

试试这个:

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通道以使用它。