Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is there a format processor to write my own printf-like function and keep the %d style arguments, without using sprintf?

I'm writing a serial interface for an MCU, and I want to know how one would create a printf-like function to write to the serial UART. I can write to the UART, but to save memory and stack space, and avoid temp string buffers, I would prefer to do that write directly instead of doing sprintf() to a string and then writing the string via serial. There is no kernel and no file handling, so FILE* writes like those from fprintf() won't work (but sprintf() does).

Is there something that processes formatted strings for each char, so I can print char-by-char as it parses the format string, and applies the related arguments?

We are using newlib as part of the efm32-base project.

UPDATE

I would like to note that ultimately we implemented the _write() function because thats all newlib needs to light up printf.

like image 749
KJ7LNW Avatar asked Nov 30 '25 18:11

KJ7LNW


2 Answers

Standard C printf family of functions don't have a "print to a character callback" type of functionality. Most embedded platforms don't support fprintf either.

First try digging around the C runtime for your platform, it might have a built-in solution. For example, ESP-IDF has ets_install_putc1() which essentially installs a callback for printf (though its ets_printf already prints to UART0).

Failing that, there are alternative printf implementations designed specifically for embedded applications which you can adapt to your needs.

For example mpaland/printf has a function taking the character printer callback as the first argument:

int fctprintf(void (*out)(char character, void* arg), void* arg, const char* format, ...);

Also see this related question: Minimal implementation of sprintf or printf.

like image 79
rustyx Avatar answered Dec 02 '25 07:12

rustyx


You had said [in your top comments] that you had GNU, so fopencookie for the hooks [I've used it before with success].

Attaching to stdout may be tricky, but doable.

Note that we have: FILE *stdout; (i.e. it's [just] a pointer). So, simply setting it to the [newly] opened stream should work.

So, I think you can do, either (1):

FILE *saved_stdout = stdout;

Or (2):

fclose(stdout);

Then, (3):

FILE *fc = fopencookie(...);
setlinebuf(fc);  // and whatever else ...
stdout = fc;

You can [probably] adjust the order to suit (e.g. doing fclose first, etc.)

I had looked for something analogous to freopen or fdopen to fit your situation, but I didn't find anything, so doing stdout = ...; may be the option.

This works fine if you do not have any code that tries to write to fd 1 directly (e.g. write(1,"hello\n",6);).

Even in that case, there is probably a way.


UPDATE:

Do you know if FILE*stdout is a const? If so, I might need to do something crazy like FILE **p = &stdout and then *p = fopencookie(...)

You were right to be concerned, but not for quite the reason you think. Read on ...


stdout is writable but ...

Before I posted, I checked stdio.h, and it has:

extern FILE *stdout;        /* Standard output stream.  */

If you think about it, stdout must be writable.

Otherwise, we could never do:

fprintf(stdout,"hello world\n");
fflush(stdout);

Also, if we did a fork, then [in the child] if we wanted to set up stdout to go to a logfile, we'd need to be able to do:

freopen("child_logfile","w",stdout);

So, no worries ...


Trust but verify ...

Did I say "no worries"? I may have been premature ;-)

There is an issue.

Here is a sample test program:

#define _GNU_SOURCE
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>

#if 1 || DEBUG
#define dbgprt(_fmt...) \
    do { \
        fprintf(stderr,_fmt); \
        fflush(stderr); \
    } while (0)
#else
#define dbgprt(_fmt...) \
    do { } while (0)
#endif

typedef struct {
    int ioport;
} uartio_t;

char *arg = "argument";

ssize_t
my_write(void *cookie,const char *buf,size_t len)
{
    uartio_t *uart = cookie;
    ssize_t err;

    dbgprt("my_write: ENTER ioport=%d buf=%p len=%zu\n",
        uart->ioport,buf,len);

    err = write(uart->ioport,buf,len);

    dbgprt("my_write: EXIT err=%zd\n",err);

    return err;
}

int
my_close(void *cookie)
{
    uartio_t *uart = cookie;

    dbgprt("my_close: ioport=%d\n",uart->ioport);
    int err = close(uart->ioport);
    uart->ioport = -1;

    return err;
}

int
main(void)
{

    cookie_io_functions_t cookie = {
        .write = my_write,
        .close = my_close
    };
    uartio_t uart;

    printf("hello\n");
    fflush(stdout);

    uart.ioport = open("uart",O_WRONLY | O_TRUNC | O_CREAT,0644);
    FILE *fc = fopencookie(&uart,"w",cookie);

    FILE *saved_stdout = stdout;
    stdout = fc;

    printf("uart simple printf\n");
    fprintf(stdout,"uart fprintf\n");
    printf("uart printf with %s\n",arg);

    fclose(fc);
    stdout = saved_stdout;

    printf("world\n");

    return 0;
}

Program output:

After compiling, running with:

./uart >out 2>err

This should produce an expected result. But, we get (from head -100 out err uart):

==> out <==
hello
uart simple printf
world

==> err <==
my_write: ENTER ioport=3 buf=0xa90390 len=39
my_write: EXIT err=39
my_close: ioport=3

==> uart <==
uart fprintf
uart printf with argument

Whoa! What happened? The out file should just be:

hello
world

And, the uart file should have three lines instead of two:

uart printf
uart simple printf
uart printf with argument

But, the uart simple printf line went to out instead of [the intended] uart file.

Again, whoa!, what happened?!?!


Explanation:

The program was compiled with gcc. Recompiling with clang produces the desired results!

It turns out that gcc was trying to be too helpful. When compiling, it converted:

printf("uart simple printf\n");

Into:

puts("uart simple printf");

We see that if we disassemble the executable [or compile with -S and look at the .s file].

The puts function [apparently] bypasses stdout and uses glibc's internal version: _IO_stdout.

It appears that glibc's puts is a weak alias to _IO_puts and that uses _IO_stdout.

The _IO_* symbols are not directly accessible. They're what glibc calls "hidden" symbols--available only to glibc.so itself.


The real fix:

I discovered this after considerable hacking around. Those attempts/fixes are in an appendix below.

It turns out that glibc defines (e.g.) stdout as:

FILE *stdout = (FILE *) &_IO_2_1_stdout_;

Internally, glibc uses that internal name. So, if we change what stdout points to, it breaks that association.

In actual fact, only _IO_stdout is hidden. The versioned symbol is global but we have to know the name either from readelf output or by using some __GLIBC_* macros (i.e. a bit messy).

So, we need to modify the save/restore code to not change the value in stdout but memcpy to/from what stdout points to.

So, in a way, you were correct. It is [effectively] const [readonly].

So, for the above sample/test program, when we want to set a new stdout, we want:

FILE *fc = fopencookie(...);
FILE saved_stdout = *stdout;
*stdout = *fc;

When we want to restore the original:

*fc = *stdout;
fclose(fc);
*stdout = saved_stdout;

So, it really wasn't gcc that was the issue. The original save/restore we developed was incorrect. But, it was latent. Only when gcc called puts did the bug manifest itself.

Personal note: Aha! Now that I got this code working, it seems oddly familiar. I'm having a deja vu experience. I'm pretty sure that I've had to do the same in the past. But, it was so long ago, that I had completely forgotten about it.


Workarounds / fixes that semi-worked but are more complex:

Note: As mentioned, these workarounds are only to show what I tried before finding the simple fix above.

One workaround is to disable gcc's conversion from printf to puts.

The simplest way may be to [as mentioned] compile with clang. But, some web pages say that clang does the same thing as gcc. It does not do the puts optimization on my version of clang [for x86_64]: 7.0.1 -- YMMV

For gcc ...

A simple way is to compile with -fno-builtins. This fixes the printf->puts issue but disables [desirable] optimizations for memcpy, etc. It's also undocumented [AFAICT]

Another way is to force our own version of puts that calls fputs/fputc. We'd put that in (e.g.) puts.c and build and link against it:

#include <stdio.h>

int
puts(const char *str)
{
    fputs(str,stdout);
    fputc('\n',stdout);
}

When we just did: stdout = fc; we were deceiving glibc a bit [actually, glibc was deceiving us a bit] and that has now come back to haunt us.

The "clean" way would be to do freopen. But, AFAICT, there is no analogous function that works on a cookie stream. There may be one, but I haven't found it.

So, one of the "dirty" methods may be the only way. I think using the "custom" puts function method above would be the best bet.

Edit: It was after I reread the above "deceiving" sentence that I hit on the simple solution (i.e. It made me dig deeper into glibc source).

like image 44
Craig Estey Avatar answered Dec 02 '25 07:12

Craig Estey