将 printf 重定向到 fopencookie

Redirect printf to fopencookie

On Linux (Raspbian on a Raspberry Pi) 我想这样做,以便我的 C 应用程序使用 printf 打印的任何内容都在回调。

(不,我不是在谈论 shell 与 > some_file.txt 的重定向。我在谈论一个 C 程序 自己stdout(因此 printf)发送到同一程序中的回调。)

(是的,我真的很想这样做。我正在使用 OpenGL 制作一个全屏程序,并希望在该程序中使用我自己的 printf 向用户显示任何文本渲染代码。用其他东西替换所有 printf 调用是不可行的。)

我觉得这应该很容易。 Whosebug 上已经有这个问题的变体,但我发现 none 完全相同。

我可以使用 fopencookie 得到一个 FILE* 最终调用我的回调。到目前为止,一切都很好。挑战是让 stdoutprintf 去那里。

不能使用freopen,因为它需要一个字符串路径。我要重定向到的 FILE* 不是文件系统上的文件,而是仅存在于 运行 时间。

不能使用dup2因为fopencookieFILE*没有文件描述符(fileno returns-1).

glibc documentation 建议我可以简单地将 stdout 重新分配给我的新 FILE*"stdin, stdout, and stderr are normal variables which you can set just like any others."。这 几乎可以工作 。使用 fprintf (stdout, "whatever") 打印的任何内容都会 转到我的回调,任何具有任何格式说明符的 printf 也是如此。但是,使用完全没有格式说明符的字符串对 printf 的任何调用仍会转到 "original" 标准输出。

我怎样才能实现我想要做的事情?

PS:我不关心便携性。这只会在我当前的环境中 运行。

#define _GNU_SOURCE                                                         
#include <stdio.h>                                                          
#include <unistd.h>                                                         
#include <assert.h>                                                         
#include <stdarg.h>                                                         
#include <alloca.h>                                                         
#include <string.h>                                                         


static ssize_t my_write_func (void * cookie, const char * buf, size_t size) 
{                                                                           
    fprintf (stderr, "my_write_func received %d bytes\n", size);            
    char * copy = (char*) alloca (size + 1);                                
    assert (copy);                                                          
    copy[size] = 0;                                                         
    strncpy (copy, buf, size);                                              
    fprintf (stderr, "Text is: \"%s\"\n", copy);                            
    fflush (stderr);                                                        
    return size;                                                            
}                                                                           


static FILE * create_opencookie ()                                          
{                                                                           
    cookie_io_functions_t funcs;                                            
    memset (&funcs, 0, sizeof (funcs));                                     
    funcs.write = my_write_func;                                            
    FILE * f = fopencookie (NULL, "w", funcs);                              
    assert (f);                                                             
    return f;                                                               
}                                                                           


int main (int argc, char ** argv)                                           
{                                                                           
    FILE * f = create_opencookie ();                                        
    fclose (stdout);                                                        
    stdout = f;                                                             

    // These two DO go to my callback:                                                                        
    fprintf (stdout, "This is a long string, fprintf'd to stdout\n");                           
    printf ("Hello world, this is a printf with a digit: %d\n", 123);

    // This does not go to my callback.
    // If I omit the fclose above then it gets printed to the console.
    printf ("Hello world, this is plain printf.\n");           
    fflush (NULL);                                                          
    return 0;                                                               
}               

这似乎是 GLIBC 中的错误。

printf("simple string")printf("foo %d", 123) 工作方式不同的原因是 GCC 将前者转换为 puts,并认为它们是等价的。

据我所知,它们应该是等价的。 This man page 表示 puts 输出到 stdout,就像 printf 一样。

但是,在 GLIBC 中 printf 输出到 stdout here, but puts outputs to _IO_stdout here, and these are not equivalent. This has already been reported as a glibc bug (upstream bug).

要解决此错误,您可以使用 -fno-builtin-printf 标志进行构建。这会阻止 GCC 将 printf 转换为 puts,并在我的系统上生成:

$ ./a.out
my_write_func received 126 bytes
Text is: "This is a long string, fprintf'd to stdout
Hello world, this is a printf with a digit: 123
Hello world, this is plain printf.
"

此解决方法当然是不完整的:如果您直接调用 puts,或者在调用 printf("simple string") 并且 不是 [=50= 的对象文件中调用 link ] 使用 -fno-builtin-printf 编译(可能来自第 3 方库),那么你仍然会遇到问题。

不幸的是你不能分配给_IO_stdout(这是一个宏)。您唯一可以做的(我能想到的)是在您自己的 puts 中 link,也就是 returns printf("%s", arg)。如果您 link 反对 libc.so.6,这应该有效,但如果您 link 反对 libc.a

可能会造成麻烦

关于 debian 拉伸:

setvbuf (f, NULL, _IOLBF, 0);  // line buffered

在 create_opencookie 调用成功后。

您可以改为重定向到管道,并在单独的线程中处理写入的数据。

#include <pthread.h>
#include <ctype.h>
#include <unistd.h>
#include <stdio.h>

// this is the original program which you can't change
void print(void) {
    printf("Hello, %d\n", 123);
    puts("world");
    printf("xyz");
}

int p[2];

void *render(void *arg) {
    int nread;
    char buf[1];
    while((nread = read(p[0], buf, sizeof buf)) > 0) {
        // process the written data, in this case - make it uppercase and write to stderr
        for(int i = 0; i < nread; i++)
            buf[i] = toupper(buf[i]);
        write(2, buf, nread);
    }
    return NULL;
}

int main() {
    setvbuf(stdout, NULL, _IONBF, 0);
    pipe(p);
    dup2(p[1], 1);
    close(p[1]);
    pthread_t t;
    pthread_create(&t, NULL, render, NULL);
    print();
    close(1);
    pthread_join(t, NULL);
}