Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Redirect printf to fopencookie

On Linux (Raspbian on a Raspberry Pi) I would like to make it so that anything my C application prints using printf is sent back to me in a callback.

(No, I'm not talking about shell redirection with > some_file.txt. I'm talking about a C program making the decision by itself to send stdout (and therefore printf) to a callback within that same program.)

(Yes, I really do want to do this. I'm making a full-screen program using OpenGL and want to present any printf'd text to the user within that program, using my own rendering code. Replacing all printf calls with something else is not feasible.)

I feel like this should be easy. There are variations of this question on StackOverflow already, but none that I could find are exactly the same.

I can use fopencookie to get a FILE* that ends up calling my callback. So far, so good. The challenge is to get stdout and printf to go there.

I can't use freopen because it takes a string path. The FILE* I want to redirect to is not a file on the filesystem but rather just exists at runtime.

I can't use dup2 because the FILE* from fopencookie does not have a file descriptor (fileno returns -1).

The glibc documentation suggests that I can simply reassign stdout to my new FILE*: "stdin, stdout, and stderr are normal variables which you can set just like any others.". This does almost work. Anything printed with fprintf (stdout, "whatever") does go to my callback, and so does any printf that has any format specifiers. However, any call to printf with a string with no format specifiers at all still goes to the "original" stdout.

How can I achieve what I'm trying to do?

PS: I don't care about portability. This will only ever run on my current environment.

#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;                                                               
}               
like image 876
peterpi Avatar asked Mar 04 '23 19:03

peterpi


2 Answers

This appears to be a bug in GLIBC.

The reason that printf("simple string") works differently from printf("foo %d", 123) is that GCC transforms the former into a puts, with the notion that they are equivalent.

As far as I can tell, they should be equivalent. This man page states that puts outputs to stdout, just like printf does.

However, in GLIBC printf outputs to 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).

To work around this bug, you could build with -fno-builtin-printf flag. That prevents GCC from transforming printf into puts, and on my system produces:

$ ./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.
"

This workaround is of course incomplete: if you call puts directly, or link in object files that call printf("simple string") and were not compiled with -fno-builtin-printf (perhaps from 3rd-party library), then you'll still have a problem.

Unfortunately you can't assign to _IO_stdout (which is a macro). The only other thing you could do (that I can think of) is link in your own puts, which just returns printf("%s", arg). That should work if you are linking against libc.so.6, but may cause trouble if you link against libc.a.

like image 100
Employed Russian Avatar answered Mar 17 '23 05:03

Employed Russian


You can redirect to a pipe instead and process the written data in a separate thread.

#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);
}
like image 41
PhilipRoman Avatar answered Mar 17 '23 05:03

PhilipRoman