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

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

我也看到了如何将标准输出重定向到 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);
4

3 回答 3

3

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

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

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

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

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

  3. 当您关闭(在取消注册时发生)stdoutTcl 解释器中的标准通道时,底层 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 解释器中的标准通道需要更多的代码,并且应该保证正确的顺序(stdin, stdout, stderr, 按该顺序)。如果您希望 Tcl 解释器中剩余的两个标准通道连接到封闭程序的匹配流,则这种方法更有效;第一种方法是免费的。

以下是如何保存 FD 1 以使仅stdout在 Tcl 解释器中连接到文件;对于封闭程序 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

如您所见,字符串“test”输出puts到文件,而字符串“before”和“after”,它们是write(2)封闭程序中的 FD 1 的 n (这就是最后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);

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

于 2013-05-04T20:08:51.573 回答
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 将忽略的任意数字),以便您可以稍后恢复它,如果需要的话。

于 2013-04-18T05:09:12.093 回答
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 通道以使用它。

于 2013-05-07T01:27:46.967 回答