Tcl C API:将嵌入式Tcl interp的标准输出重定向到一个文件,而不影响整个程序

Tcl C API:将嵌入式Tcl interp的标准输出重定向到一个文件,而不影响整个程序,tcl,channel,Tcl,Channel,在C API级别,假设您使用的是基于Unix的操作系统(即,不是Windows),则可以通过使用正确的操作系统调用来实现这一点: #包括 #包括 // ... 现在在一个函数中 int fd=open(“/home/aminasysa/nlb_rundir/imfile”,O_WRONLY | O_CREAT,0744); //重要提示:在这里处理错误! dup2(fd,标准文件号); 关闭(fd); 您还可以使用dup()保存旧的标准输出(保存到Tcl将忽略的任意数字),以便以后可以根据需要

在C API级别,假设您使用的是基于Unix的操作系统(即,不是Windows),则可以通过使用正确的操作系统调用来实现这一点:

#包括
#包括
// ... 现在在一个函数中
int fd=open(“/home/aminasysa/nlb_rundir/imfile”,O_WRONLY | O_CREAT,0744);
//重要提示:在这里处理错误!
dup2(fd,标准文件号);
关闭(fd);

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

在C API级别,假设您使用的是基于Unix的操作系统(即,非Windows),则只需使用正确的操作系统调用即可:

#包括
#包括
// ... 现在在一个函数中
int fd=open(“/home/aminasysa/nlb_rundir/imfile”,O_WRONLY | O_CREAT,0744);
//重要提示:在这里处理错误!
dup2(fd,标准文件号);
关闭(fd);

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

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

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

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

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

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

  • 当您关闭Tcl解释器中的标准通道
    stdout
    时(在注销时发生),底层FD(1)也将关闭

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

  • 然后从文件中创建一个Tcl通道,并将其注册到解释器中。对于解释器,该通道实际上变成了
    stdout

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

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

    • 巧妙地将FD 1“重新连接”到最初打开的同一个流,并使为Tcl解释器的标准输出打开的文件使用大于2的FD
    • 与其先让Tcl解释器初始化其标准通道,然后再重新初始化其中一个通道,不如先手动初始化它们,然后再让自动激活机制启动
    这两种方法各有利弊:

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

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

    下面介绍如何保留fd1,使Tcl解释器中的stdout仅连接到文件;因为封闭程序fd1仍然连接到操作系统设置的相同流

    FILE *myfile = fopen("myfile", "W+");
    Tcl_Interp *interp = Tcl_CreateInterp(); 
    Tcl_Channel myChannel = Tcl_MakeFileChannel(myfile, TCL_WRITABLE);
    Tcl_SetStdChannel(myChannel, TCL_STDOUT);
    
    如您所见,
    put
    输出的字符串“test”进入文件,而字符串“before”和“after”进入终端,它们是
    write(2)
    n到封装程序中的FD 1(这就是
    put(3)
    最后所做的)

    手动初始化方法如下所示(类似于伪代码):


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

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

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

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

  • 一旦您创建的Tcl解释器初始化了它的三个标准通道(如前所述,当您为“stdout”调用
    Tcl\u GetChannel()
    时会发生这种情况),它们就会与这些通道相关联
    FILE *myfile = fopen("myfile", "W+");
    Tcl_Interp *interp = Tcl_CreateInterp(); 
    Tcl_Channel myChannel = Tcl_MakeFileChannel(myfile, TCL_WRITABLE);
    Tcl_SetStdChannel(myChannel, TCL_STDOUT);
    
    #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
    
    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);
    
    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);