nanoprintf é uma implementação livre de snprintf e vsnprintf para sistemas embarcados que, quando totalmente habilitados, visam a conformidade com o padrão C11. As principais exceções são ponto flutuante, notação científica ( %e
, %g
, %a
) e as conversões que exigem a existência de wcrtomb
. A saída de número inteiro binário C23 é opcionalmente suportada conforme N2630. Extensões de segurança para snprintf e vsnprintf podem ser configuradas opcionalmente para retornar strings cortadas ou totalmente vazias em eventos de buffer overflow.
Além disso, nanoprintf pode ser usado para analisar strings de formato no estilo printf para extrair os vários parâmetros e especificadores de conversão, sem fazer qualquer formatação de texto real.
nanoprintf não faz alocações de memória e usa menos de 100 bytes de pilha. Ele compila entre aproximadamente 740-2640 bytes de código objeto em uma arquitetura Cortex-M0, dependendo da configuração.
Todo o código é escrito em um dialeto mínimo de C99 para máxima compatibilidade do compilador, compila de forma limpa nos níveis de aviso mais altos em clang + gcc + msvc, não levanta problemas de UBsan ou Asan e é exaustivamente testado em arquiteturas de 32 e 64 bits. . nanoprintf inclui cabeçalhos padrão C, mas os utiliza apenas para tipos C99 e listas de argumentos; nenhuma chamada é feita em stdlib/libc, com exceção de quaisquer chamadas aritméticas internas de números inteiros grandes que seu compilador possa emitir. Como de costume, alguns cabeçalhos específicos do Windows serão necessários se você estiver compilando nativamente para msvc.
nanoprintf é um arquivo de cabeçalho único no estilo das bibliotecas stb. O resto do repositório são testes e andaimes e não são necessários para uso.
nanoprintf é estaticamente configurável para que os usuários possam encontrar um equilíbrio entre tamanho, requisitos do compilador e conjunto de recursos. Conversão de ponto flutuante, modificadores de comprimento "grandes" e write-back de tamanho são todos configuráveis e compilados apenas se solicitados explicitamente, consulte Configuração para obter detalhes.
Adicione o seguinte código a um dos seus arquivos de origem para compilar a implementação do nanoprintf:
// define your nanoprintf configuration macros here (see "Configuration" below) #define NANOPRINTF_IMPLEMENTATION #include "path/to/nanoprintf.h"
Então, em qualquer arquivo onde você queira usar o nanoprintf, basta incluir o cabeçalho e chamar as funções npf_:
#include "nanoprintf.h" void print_to_uart(void) { npf_pprintf(&my_uart_putc, NULL, "Hello %s%c %d %u %fn", "worl", 'd', 1, 2, 3.f); } void print_to_buf(void *buf, unsigned len) { npf_snprintf(buf, len, "Hello %s", "world"); }
Consulte os exemplos "Usar nanoprintf diretamente" e "Wrap nanoprintf" para obter mais detalhes.
Eu queria um printf drop-in de domínio público de arquivo único que chegasse a menos de 1 KB na configuração mínima (bootloaders, etc.) e menos de 3 KB com os sinos e assobios de ponto flutuante habilitados.
No trabalho de firmware, geralmente quero a formatação de string do stdio sem os requisitos de syscall ou camada de descritor de arquivo; eles quase nunca são necessários em sistemas minúsculos onde você deseja fazer login em buffers pequenos ou emitir diretamente para um barramento. Além disso, muitas implementações de stdio incorporadas são maiores ou mais lentas do que o necessário - isso é importante para o trabalho do bootloader. Se você não precisa de nenhum syscalls ou stdio bells + assobios, você pode simplesmente usar nanoprintf e nosys.specs
e reduzir sua construção.
Este código é otimizado para tamanho, não para legibilidade ou estrutura. Infelizmente, a modularidade e a "limpeza" (seja lá o que isso signifique) adicionam sobrecarga nessa pequena escala, portanto, a maior parte da funcionalidade e da lógica é reunida em npf_vpprintf
. Não é assim que o código normal de sistemas embarcados deveria ser; é uma sopa #ifdef
e difícil de entender, e peço desculpas se você tiver que vasculhar a implementação. Esperançosamente, os vários testes servirão como guias se você os hackear.
Alternativamente, talvez você seja um programador significativamente melhor do que eu! Nesse caso, ajude-me a tornar este código menor e mais limpo, sem aumentar a área ocupada, ou me indique na direção certa. :)
nanoprintf tem 4 funções principais:
npf_snprintf
: Use como snprintf.
npf_vsnprintf
: Use como vsnprintf (suporte va_list
).
npf_pprintf
: Use como printf com um retorno de chamada de gravação por caractere (semihosting, UART, etc).
npf_vpprintf
: Use como npf_pprintf
mas leva um va_list
.
As variações pprintf
recebem um retorno de chamada que recebe o caractere a ser impresso e um ponteiro de contexto fornecido pelo usuário.
Passe NULL
ou nullptr
para npf_[v]snprintf
para não escrever nada e retornar apenas o comprimento da string formatada.
nanoprintf não fornece printf
ou putchar
; eles são vistos como serviços de nível de sistema e nanoprintf é uma biblioteca utilitária. Esperançosamente, nanoprintf é um bom alicerce para lançar seu próprio printf
.
Todas as funções nanoprintf retornam o mesmo valor: o número de caracteres que foram enviados para o retorno de chamada (para npf_pprintf) ou o número de caracteres que teriam sido gravados no buffer forneceu espaço suficiente. O byte 0 do terminador nulo não faz parte da contagem.
O padrão C permite que as funções printf retornem valores negativos caso a string ou a codificação de caracteres não possam ser executadas ou se o fluxo de saída encontrar EOF. Como o nanoprintf ignora recursos do sistema operacional, como arquivos, e não suporta o modificador de comprimento l
para suporte wchar_t
, quaisquer erros de tempo de execução são bugs internos (por favor, informe!) ou uso incorreto. Por causa disso, nanoprintf retorna apenas valores não negativos que representam quantos bytes a string formatada contém (novamente, menos o byte do terminador nulo).
nanoprintf possui os seguintes sinalizadores de configuração estática.
NANOPRINTF_USE_FIELD_WIDTH_FORMAT_SPECIFIERS
: Defina como 0
ou 1
. Ativa especificadores de largura de campo.
NANOPRINTF_USE_PRECISION_FORMAT_SPECIFIERS
: Defina como 0
ou 1
. Ativa especificadores de precisão.
NANOPRINTF_USE_FLOAT_FORMAT_SPECIFIERS
: Defina como 0
ou 1
. Habilita especificadores de ponto flutuante.
NANOPRINTF_USE_LARGE_FORMAT_SPECIFIERS
: Defina como 0
ou 1
. Ativa modificadores superdimensionados.
NANOPRINTF_USE_BINARY_FORMAT_SPECIFIERS
: Defina como 0
ou 1
. Habilita especificadores binários.
NANOPRINTF_USE_WRITEBACK_FORMAT_SPECIFIERS
: Defina como 0
ou 1
. Ativa %n
para write-back.
NANOPRINTF_VISIBILITY_STATIC
: definição opcional. Marca protótipos como static
para sandbox nanoprintf.
Se nenhum sinalizador de configuração for especificado, nanoprintf assumirá como padrão valores incorporados "razoáveis" em uma tentativa de ser útil: flutuadores estão habilitados, mas writeback, binários e formatadores grandes estão desabilitados. Se algum sinalizador de configuração for especificado explicitamente, nanoprintf exigirá que todos os sinalizadores sejam especificados explicitamente.
Se um recurso de especificador de formato desabilitado for usado, nenhuma conversão ocorrerá e a string do especificador de formato simplesmente será impressa.
nanoprintf tem as seguintes configurações específicas de ponto flutuante definidas.
NANOPRINTF_CONVERSION_BUFFER_SIZE
: Opcional, o padrão é 23
. Define o tamanho de um buffer de caracteres usado para armazenar o valor convertido. Defina um número maior para permitir a impressão de números de ponto flutuante com mais caracteres. O tamanho do buffer inclui a parte inteira, a parte fracionária e o separador decimal, mas não inclui o sinal e os caracteres de preenchimento. Se o número não couber no buffer, um err
será impresso. Tenha cuidado com tamanhos grandes, pois o buffer de conversão é alocado na memória da pilha.
NANOPRINTF_CONVERSION_FLOAT_TYPE
: Opcional, o padrão é unsigned int
. Define o tipo inteiro usado para o algoritmo de conversão flutuante, que determina a precisão da conversão. Pode ser definido como qualquer tipo inteiro sem sinal, como por exemplo uint64_t
ou uint8_t
.
Por padrão, npf_snprintf e npf_vsnprintf se comportam de acordo com o padrão C: o buffer fornecido será preenchido, mas não ultrapassado. Se a string ultrapassar o buffer, um byte terminador nulo será gravado no byte final do buffer. Se o buffer for null
ou de tamanho zero, nenhum byte será gravado.
Se você definir NANOPRINTF_SNPRINTF_SAFE_EMPTY_STRING_ON_OVERFLOW
e sua string for maior que seu buffer, o primeiro byte do buffer será substituído por um byte terminador nulo. Isso é semelhante em espírito ao snprintf_s da Microsoft.
Em todos os casos, o nanoprintf retornará o número de bytes que teriam sido gravados no buffer, se houvesse espaço suficiente. Este valor não leva em conta o byte terminador nulo, de acordo com o padrão C.
nanoprintf usa apenas memória de pilha e nenhuma primitiva de simultaneidade, portanto, internamente, ele ignora seu ambiente de execução. Isso torna seguro chamar de vários contextos de execução simultaneamente ou interromper uma chamada npf_
com outra chamada npf_
(digamos, um ISR ou algo assim). Se você usar npf_pprintf
simultaneamente com o mesmo destino npf_putc
, cabe a você garantir a correção em seu retorno de chamada. Se você npf_snprintf
de vários threads para o mesmo buffer, terá uma corrida de dados óbvia.
Assim como printf
, nanoprintf
espera uma string de especificação de conversão no seguinte formato:
[flags][field width][.precision][length modifier][conversion specifier]
Bandeiras
Nenhum ou mais dos seguintes:
0
: Preencha o campo com caracteres zero à esquerda.
-
: Justifique à esquerda o resultado da conversão no campo.
+
: As conversões assinadas sempre começam com caracteres +
ou -
.
: (espaço) Um caractere de espaço será inserido se o primeiro caractere convertido não for um sinal.
#
: Grava caracteres extras ( 0x
para hexadecimal, .
para carros flutuantes vazios, '0' para octais vazios, etc).
Largura do campo (se habilitado)
Um número que especifica a largura total do campo para a conversão e adiciona preenchimento. Se a largura do campo for *
, a largura do campo será lida no próximo vararg.
Precisão (se habilitada)
Prefixado com um .
, um número que especifica a precisão do número ou string. Se a precisão for *
, a precisão será lida no próximo vararg.
Modificador de comprimento
Nenhum ou mais dos seguintes:
h
: Use short
para largura vararg integral e write-back.
L
: Use long double
para float vararg width (nota: ele será então reduzido para double
)
l
: Use largura vararg long
, double
ou larga.
hh
: Use char
para largura vararg integral e write-back.
ll
: (especificador grande) Use long long
para largura vararg integral e write-back.
j
: (especificador grande) Use os tipos [u]intmax_t
para largura vararg integral e write-back.
z
: (especificador grande) Use os tipos size_t
para largura vararg integral e write-back.
t
: (especificador grande) Use os tipos ptrdiff_t
para largura vararg integral e write-back.
Especificador de conversão
Exatamente um dos seguintes:
%
: literal de sinal de porcentagem
c
: Personagem
s
: strings terminadas em nulo
i
/ d
: inteiros assinados
u
: inteiros sem sinal
o
: inteiros octais sem sinal
x
/ X
: inteiros hexadecimais sem sinal
p
: Ponteiros
n
: Escreva o número de bytes gravados no ponteiro vararg
f
/ F
: Decimal de ponto flutuante
e
/ E
: Científico de ponto flutuante (não implementado, imprime decimal flutuante)
g
/ G
: Menor ponto flutuante (não implementado, imprime decimal flutuante)
a
/ A
: hexadecimal de ponto flutuante (não implementado, imprime decimal flutuante)
b
/ B
: inteiros binários
A conversão de ponto flutuante é realizada extraindo as partes inteiras e fracionárias do número em duas variáveis inteiras separadas. Para cada parte, o expoente é então dimensionado da base 2 para a base 10, multiplicando e dividindo iterativamente a mantissa por 2 e 5 apropriadamente. A ordem das operações de escalonamento é selecionada dinamicamente (dependendo do valor) para reter o máximo possível dos bits mais significativos da mantissa. Quanto mais longe o valor estiver do separador decimal, mais erros a escala acumulará. Com uma largura de tipo inteiro de conversão de N
bits em média, o algoritmo retém N - log2(5)
ou N - 2.322
bits de precisão. Além disso, partes inteiras até 2 ^^ N - 1
e partes fracionárias com até N - 2.322
bits após o separador decimal são convertidas perfeitamente sem perder nenhum bit.
Como o código float -> fixo opera nos bits de valor flutuante bruto, nenhuma operação de ponto flutuante é executada. Isso permite que o nanoprintf formate floats com eficiência em arquiteturas soft-float como Cortex-M0, para funcionar de forma idêntica com ou sem otimizações como "matemática rápida" e para minimizar a pegada do código.
Os especificadores %e
/ %E
, %a
/ %A
e %g
/ %G
são analisados, mas não formatados. Se usado, a saída será idêntica a se %f
/ %F
fosse usado. Solicitações pull são bem-vindas! :)
Não existe suporte para caracteres largos: os campos %lc
e %ls
exigem que o argumento seja convertido em um array char como se fosse uma chamada para wcrtomb. Quando as conversões de localidade e conjunto de caracteres estão envolvidas, é difícil manter o nome "nano". Conseqüentemente, %lc
e %ls
se comportam como %c
e %s
, respectivamente.
Atualmente, as únicas conversões flutuantes suportadas são as formas decimais: %f
e %F
. Solicitações pull são bem-vindas!
A construção do CI está configurada para usar gcc e nm para medir o tamanho compilado de cada solicitação pull. Consulte a saída do trabalho "Relatórios de tamanho" de verificações de pré-envio para execuções recentes.
As seguintes medidas de tamanho são feitas em relação à construção Cortex-M0.
Configuration "Minimal": arm-none-eabi-gcc -c -x c -Os -I/__w/nanoprintf/nanoprintf -o npf.o -mcpu=cortex-m0 -DNANOPRINTF_IMPLEMENTATION -DNANOPRINTF_USE_FIELD_WIDTH_FORMAT_SPECIFIERS=0 -DNANOPRINTF_USE_PRECISION_FORMAT_SPECIFIERS=0 -DNANOPRINTF_USE_FLOAT_FORMAT_SPECIFIERS=0 -DNANOPRINTF_USE_LARGE_FORMAT_SPECIFIERS=0 -DNANOPRINTF_USE_BINARY_FORMAT_SPECIFIERS=0 -DNANOPRINTF_USE_WRITEBACK_FORMAT_SPECIFIERS=0 - arm-none-eabi-nm --print-size --size-sort npf.o 00000046 00000002 t npf_bufputc_nop 00000048 00000010 t npf_putc_cnt 00000032 00000014 t npf_bufputc 00000270 00000016 T npf_pprintf 000002cc 00000016 T npf_snprintf 00000000 00000032 t npf_utoa_rev 00000286 00000046 T npf_vsnprintf 00000058 00000218 T npf_vpprintf Total size: 0x2e2 (738) bytes Configuration "Binary": arm-none-eabi-gcc -c -x c -Os -I/__w/nanoprintf/nanoprintf -o npf.o -mcpu=cortex-m0 -DNANOPRINTF_IMPLEMENTATION -DNANOPRINTF_USE_FIELD_WIDTH_FORMAT_SPECIFIERS=0 -DNANOPRINTF_USE_PRECISION_FORMAT_SPECIFIERS=0 -DNANOPRINTF_USE_FLOAT_FORMAT_SPECIFIERS=0 -DNANOPRINTF_USE_LARGE_FORMAT_SPECIFIERS=0 -DNANOPRINTF_USE_BINARY_FORMAT_SPECIFIERS=1 -DNANOPRINTF_USE_WRITEBACK_FORMAT_SPECIFIERS=0 - arm-none-eabi-nm --print-size --size-sort npf.o 00000046 00000002 t npf_bufputc_nop 00000048 00000010 t npf_putc_cnt 00000032 00000014 t npf_bufputc 000002a8 00000016 T npf_pprintf 00000304 00000016 T npf_snprintf 00000000 00000032 t npf_utoa_rev 000002be 00000046 T npf_vsnprintf 00000058 00000250 T npf_vpprintf Total size: 0x31a (794) bytes Configuration "Field Width + Precision": arm-none-eabi-gcc -c -x c -Os -I/__w/nanoprintf/nanoprintf -o npf.o -mcpu=cortex-m0 -DNANOPRINTF_IMPLEMENTATION -DNANOPRINTF_USE_FIELD_WIDTH_FORMAT_SPECIFIERS=1 -DNANOPRINTF_USE_PRECISION_FORMAT_SPECIFIERS=1 -DNANOPRINTF_USE_FLOAT_FORMAT_SPECIFIERS=0 -DNANOPRINTF_USE_LARGE_FORMAT_SPECIFIERS=0 -DNANOPRINTF_USE_BINARY_FORMAT_SPECIFIERS=0 -DNANOPRINTF_USE_WRITEBACK_FORMAT_SPECIFIERS=0 - arm-none-eabi-nm --print-size --size-sort npf.o 00000046 00000002 t npf_bufputc_nop 00000048 00000010 t npf_putc_cnt 00000032 00000014 t npf_bufputc 000004fe 00000016 T npf_pprintf 0000055c 00000016 T npf_snprintf 00000000 00000032 t npf_utoa_rev 00000514 00000048 T npf_vsnprintf 00000058 000004a6 T npf_vpprintf Total size: 0x572 (1394) bytes Configuration "Field Width + Precision + Binary": arm-none-eabi-gcc -c -x c -Os -I/__w/nanoprintf/nanoprintf -o npf.o -mcpu=cortex-m0 -DNANOPRINTF_IMPLEMENTATION -DNANOPRINTF_USE_FIELD_WIDTH_FORMAT_SPECIFIERS=1 -DNANOPRINTF_USE_PRECISION_FORMAT_SPECIFIERS=1 -DNANOPRINTF_USE_FLOAT_FORMAT_SPECIFIERS=0 -DNANOPRINTF_USE_LARGE_FORMAT_SPECIFIERS=0 -DNANOPRINTF_USE_BINARY_FORMAT_SPECIFIERS=1 -DNANOPRINTF_USE_WRITEBACK_FORMAT_SPECIFIERS=0 - arm-none-eabi-nm --print-size --size-sort npf.o 00000046 00000002 t npf_bufputc_nop 00000048 00000010 t npf_putc_cnt 00000032 00000014 t npf_bufputc 00000560 00000016 T npf_pprintf 000005bc 00000016 T npf_snprintf 00000000 00000032 t npf_utoa_rev 00000576 00000046 T npf_vsnprintf 00000058 00000508 T npf_vpprintf Total size: 0x5d2 (1490) bytes Configuration "Float": arm-none-eabi-gcc -c -x c -Os -I/__w/nanoprintf/nanoprintf -o npf.o -mcpu=cortex-m0 -DNANOPRINTF_IMPLEMENTATION -DNANOPRINTF_USE_FIELD_WIDTH_FORMAT_SPECIFIERS=0 -DNANOPRINTF_USE_PRECISION_FORMAT_SPECIFIERS=1 -DNANOPRINTF_USE_FLOAT_FORMAT_SPECIFIERS=1 -DNANOPRINTF_USE_LARGE_FORMAT_SPECIFIERS=0 -DNANOPRINTF_USE_BINARY_FORMAT_SPECIFIERS=0 -DNANOPRINTF_USE_WRITEBACK_FORMAT_SPECIFIERS=0 - arm-none-eabi-nm --print-size --size-sort npf.o 00000046 00000002 t npf_bufputc_nop 00000048 00000010 t npf_putc_cnt 00000032 00000014 t npf_bufputc 00000618 00000016 T npf_pprintf 00000674 00000016 T npf_snprintf 00000000 00000032 t npf_utoa_rev 0000062e 00000046 T npf_vsnprintf 00000058 000005c0 T npf_vpprintf Total size: 0x68a (1674) bytes Configuration "Everything": arm-none-eabi-gcc -c -x c -Os -I/__w/nanoprintf/nanoprintf -o npf.o -mcpu=cortex-m0 -DNANOPRINTF_IMPLEMENTATION -DNANOPRINTF_USE_FIELD_WIDTH_FORMAT_SPECIFIERS=1 -DNANOPRINTF_USE_PRECISION_FORMAT_SPECIFIERS=1 -DNANOPRINTF_USE_FLOAT_FORMAT_SPECIFIERS=1 -DNANOPRINTF_USE_LARGE_FORMAT_SPECIFIERS=1 -DNANOPRINTF_USE_BINARY_FORMAT_SPECIFIERS=1 -DNANOPRINTF_USE_WRITEBACK_FORMAT_SPECIFIERS=1 - arm-none-eabi-nm --print-size --size-sort npf.o 0000005a 00000002 t npf_bufputc_nop 0000005c 00000010 t npf_putc_cnt 00000046 00000014 t npf_bufputc 000009da 00000016 T npf_pprintf 00000a38 00000016 T npf_snprintf 00000000 00000046 t npf_utoa_rev 000009f0 00000048 T npf_vsnprintf 0000006c 0000096e T npf_vpprintf Total size: 0xa4e (2638) bytes
Para obter o ambiente e executar testes:
Clone ou bifurque este repositório.
Execute ./b
da raiz (ou py -3 build.py
da raiz, para usuários do Windows)
Isso criará todos os testes de unidade, conformidade e compilação para seu ambiente host. Quaisquer falhas de teste retornarão um código de saída diferente de zero.
O ambiente de desenvolvimento nanoprintf usa cmake e ninja. Se você os tiver em seu caminho, ./b
os usará. Caso contrário, ./b
irá baixá-los e implantá-los em path/to/your/nanoprintf/external
.
nanoprintf usa GitHub Actions para todas as compilações de integração contínua. As compilações do GitHub Linux usam esta imagem Docker do meu repositório Docker.
A matriz compila [Debug, Release] x [32 bits, 64 bits] x [Mac, Windows, Linux] x [gcc, clang, msvc], menos as configurações do clang Mac de 32 bits.
Um conjunto de testes é um fork do conjunto de testes printf, que é licenciado pelo MIT. Ele existe como um submódulo para fins de licenciamento – o nanoprintf é de domínio público, portanto, este conjunto de testes específico é opcional e excluído por padrão. Para construí-lo, recupere-o atualizando os submódulos e adicione o sinalizador --paland
à sua invocação ./b
. Não é necessário usar o nanoprintf.
A ideia básica da conversão float-to-int foi inspirada no algoritmo fixo float -> 64:64 de Wojciech Muła e estendida ainda mais pela adição de escala dinâmica e largura inteira configurável por Oskars Rubenis.
Transferi o conjunto de testes printf para nanoprintf. Foi originalmente da base de código do projeto mpaland printf, mas foi adotado e aprimorado por Eyal Rozenberg e outros. (Nanoprintf tem muitos testes próprios, mas também são muito completos e muito bons!)
A implementação binária é baseada nos requisitos especificados pela proposta N2630 de Jörg Wunsch, que esperançosamente será aceita no C23!