[英]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.我正在为 MCU 编写串行接口,我想知道如何创建printf
类似 function 来写入串行 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.我可以写入 UART,但为了节省 memory 和堆栈空间,并避免临时字符串缓冲区,我宁愿直接写入而不是sprintf()
写入字符串,然后通过串行写入字符串。 There is no kernel and no file handling, so FILE*
writes like those from fprintf()
won't work (but sprintf()
does).没有 kernel 也没有文件处理,所以像fprintf()
那样的FILE*
写入将不起作用(但sprintf()
可以)。
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?是否有一些东西可以处理每个字符的格式化字符串,所以我可以在解析格式字符串时逐个打印字符,并应用相关的 arguments?
Standard C printf
family of functions don't have a "print to a character callback" type of functionality.标准 C printf
系列函数没有“打印到字符回调”类型的功能。 Most embedded platforms don't support fprintf
either.大多数嵌入式平台也不支持fprintf
。
First try digging around the C runtime for your platform, it might have a built-in solution.首先尝试为您的平台挖掘 C 运行时,它可能有一个内置的解决方案。 For example, ESP-IDF has ets_install_putc1()
which essentially installs a callback for printf
(though its ets_printf
already prints to UART0).例如,ESP-IDF 有ets_install_putc1()
,它本质上为printf
安装了回调(尽管它的ets_printf
已经打印到 UART0)。
Failing that, there are alternative printf
implementations designed specifically for embedded applications which you can adapt to your needs.如果做不到这一点,还有专门为嵌入式应用程序设计的替代printf
实现,您可以根据自己的需要进行调整。
For example mpaland/printf has a function taking the character printer callback as the first argument:例如mpaland/printf有一个 function 将字符打印机回调作为第一个参数:
int fctprintf(void (*out)(char character, void* arg), void* arg, const char* format, ...);
Also see this related question: Minimal implementation of sprintf or printf .另请参阅此相关问题: sprintf 或 printf 的最小实现。
You had said [in your top comments] that you had GNU, so fopencookie
for the hooks [I've used it before with success].你曾说过[在你的顶级评论中]你有 GNU,所以fopencookie
用于钩子 [我以前成功地使用过它]。
Attaching to stdout
may be tricky, but doable.附加到stdout
可能很棘手,但可行。
Note that we have: FILE *stdout;
请注意,我们有: FILE *stdout;
(ie it's [just] a pointer ). (即它[只是]一个指针)。 So, simply setting it to the [newly] opened stream should work.因此,只需将其设置为 [新] 打开的 stream 即可。
So, I think you can do, either (1):所以,我认为你可以做,或者(1):
FILE *saved_stdout = stdout;
Or (2):或(2):
fclose(stdout);
Then, (3):然后,(3):
FILE *fc = fopencookie(...);
setlinebuf(fc); // and whatever else ...
stdout = fc;
You can [probably] adjust the order to suit (eg doing fclose
first, etc.)您可以[可能]调整顺序以适应(例如先执行fclose
等)
I had looked for something analogous to freopen
or fdopen
to fit your situation, but I didn't find anything, so doing stdout =...;
我一直在寻找类似于freopen
或fdopen
的东西来适应你的情况,但我没有找到任何东西,所以做stdout =...;
may be the option.可能是选择。
This works fine if you do not have any code that tries to write to fd 1 directly (eg write(1,"hello\n",6);
).如果您没有任何尝试直接写入 fd 1 的代码(例如write(1,"hello\n",6);
),这可以正常工作。
Even in that case, there is probably a way.即使在那种情况下,也可能有办法。
UPDATE:更新:
Do you know if FILE*stdout is a const?你知道 FILE*stdout 是否是一个常量吗? If so, I might need to do something crazy like FILE **p = &stdout and then *p = fopencookie(...)如果是这样,我可能需要做一些疯狂的事情,比如 FILE **p = &stdout 然后 *p = fopencookie(...)
You were right to be concerned, but not for quite the reason you think.你的担心是对的,但并不是你想的那样。 Read on...继续阅读...
stdout
is writable but ... stdout
是可写的,但是......
Before I posted, I checked stdio.h
, and it has:在发布之前,我检查了stdio.h
,它有:
extern FILE *stdout; /* Standard output stream. */
If you think about it, stdout
must be writable.如果您考虑一下, stdout
必须是可写的。
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:此外,如果我们做了一个fork
,那么 [in the child] 如果我们想将stdout
设置为 go 到日志文件,我们需要能够做到:
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:程序 output:
After compiling, running with:编译后,运行:
./uart >out 2>err
This should produce an expected result.这应该会产生预期的结果。 But , we get (from head -100 out err uart
):但是,我们得到(从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: out
文件应该是:
hello
world
And, the uart
file should have three lines instead of two :而且, uart
文件应该有三行而不是两行:
uart printf
uart simple printf
uart printf with argument
But, the uart simple printf
line went to out
instead of [the intended] uart
file.但是, uart simple printf
out
代替了[预期的] uart
文件。
Again, whoa,?再次,哇,? what happened?!?!发生了什么?!?!
Explanation:解释:
The program was compiled with gcc
.该程序是用gcc
编译的。 Recompiling with clang
produces the desired results!使用clang
编译会产生所需的结果!
It turns out that gcc
was trying to be too helpful.事实证明, gcc
试图提供太多帮助。 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].如果我们反汇编可执行文件 [或使用-S
编译并查看.s
文件],我们会看到这一点。
The puts
function [apparently] bypasses stdout
and uses glibc's internal version: _IO_stdout
. puts
function [显然]绕过stdout
并使用 glibc 的内部版本: _IO_stdout
。
It appears that glibc's puts
is a weak alias to _IO_puts
and that uses _IO_stdout
.似乎 glibc 的puts
是_IO_puts
的弱别名,并且使用_IO_stdout
。
The _IO_*
symbols are not directly accessible. _IO_*
符号不可直接访问。 They're what glibc calls "hidden" symbols--available only to glibc.so
itself.它们是 glibc 所称的“隐藏”符号——仅对glibc.so
本身可用。
The real fix:真正的修复:
I discovered this after considerable hacking around.经过大量的黑客攻击后,我发现了这一点。 Those attempts/fixes are in an appendix below.这些尝试/修复在下面的附录中。
It turns out that glibc
defines (eg) stdout
as:事实证明, glibc
将(例如) stdout
定义为:
FILE *stdout = (FILE *) &_IO_2_1_stdout_;
Internally, glibc
uses that internal name.在内部, glibc
使用该内部名称。 So, if we change what stdout
points to, it breaks that association.因此,如果我们更改stdout
指向的内容,它就会破坏这种关联。
In actual fact, only _IO_stdout
is hidden.实际上,只有_IO_stdout
是隐藏的。 The versioned symbol is global but we have to know the name either from readelf
output or by using some __GLIBC_*
macros (ie a bit messy).版本化符号是全局的,但我们必须从readelf
output 或使用一些__GLIBC_*
宏(即有点混乱)知道名称。
So, we need to modify the save/restore code to not change the value in stdout
but memcpy
to/from what stdout
points to.因此,我们需要修改保存/恢复代码以不更改stdout
中的值,而是将memcpy
更改为/从stdout
指向的内容。
So, in a way, you were correct.所以,在某种程度上,你是对的。 It is [effectively] const
[readonly].它是[有效地] const
[只读]。
So, for the above sample/test program, when we want to set a new stdout
, we want:所以,对于上面的示例/测试程序,当我们想要设置一个新的stdout
时,我们想要:
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.所以,问题真的不是gcc
。 The original save/restore we developed was incorrect.我们开发的原始保存/恢复不正确。 But, it was latent.但是,它是潜伏的。 Only when gcc
called puts
did the bug manifest itself.只有当gcc
调用puts
时,错误才会显现出来。
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
.一种解决方法是禁用gcc
从printf
到puts
的转换。
The simplest way may be to [as mentioned] compile with clang
.最简单的方法可能是 [如前所述] 使用clang
编译。 But, some web pages say that clang
does the same thing as gcc
.但是,一些 web 页面说gcc
与clang
做同样的事情。 It does not do the puts
optimization on my version of clang
[for x86_64
]: 7.0.1 -- YMMV它没有对我的clang
[for x86_64
] 版本进行puts
优化:7.0.1 -- YMMV
For gcc
...对于gcc
...
A simple way is to compile with -fno-builtins
.一种简单的方法是使用-fno-builtins
进行编译。 This fixes the printf->puts
issue but disables [desirable] optimizations for memcpy
, etc. It's also undocumented [AFAICT]这修复了printf->puts
问题,但禁用了memcpy
等的 [desirable] 优化。它也没有记录[AFAICT]
Another way is to force our own version of puts
that calls fputs/fputc
.另一种方法是强制我们自己调用fputs/fputc
的puts
版本。 We'd put that in (eg) puts.c
and build and link against it:我们将其放入(例如) puts.c
并构建和链接它:
#include <stdio.h>
int
puts(const char *str)
{
fputs(str,stdout);
fputc('\n',stdout);
}
When we just did: stdout = fc;
当我们刚刚这样做时: stdout = fc;
we were deceiving glibc
a bit [actually, glibc was deceiving us a bit] and that has now come back to haunt us.我们有点欺骗了glibc
[实际上,glibc 有点欺骗了我们],现在又回来困扰我们了。
The "clean" way would be to do freopen
. “干净”的方法是做freopen
。 But, AFAICT, there is no analogous function that works on a cookie stream.但是,AFAICT,没有类似的 function 可用于 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.我认为使用上面的“自定义” puts
function 方法是最好的选择。
Edit: It was after I reread the above "deceiving" sentence that I hit on the simple solution (ie It made me dig deeper into glibc
source).编辑:在我重读上面的“欺骗”句子之后,我找到了简单的解决方案(即它让我更深入地研究了glibc
源代码)。
depending on your standard library implementation you need to write your own versions of fputc
or _write
functions.根据您的标准库实现,您需要编写自己的fputc
或_write
函数版本。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.