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;
}
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
.
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);
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With