nanoprintf es una implementación sin trabas de snprintf y vsnprintf para sistemas integrados que, cuando están completamente habilitados, apuntan al cumplimiento del estándar C11. Las principales excepciones son la notación científica de punto flotante ( %e
, %g
, %a
) y las conversiones que requieren la existencia wcrtomb
. La salida de entero binario C23 se admite opcionalmente según N2630. Las extensiones de seguridad para snprintf y vsnprintf se pueden configurar opcionalmente para devolver cadenas recortadas o completamente vacías en eventos de desbordamiento del búfer.
Además, nanoprintf se puede utilizar para analizar cadenas de formato de estilo printf para extraer los diversos parámetros y especificadores de conversión, sin realizar ningún formato de texto real.
nanoprintf no realiza asignaciones de memoria y utiliza menos de 100 bytes de pila. Compila entre ~740-2640 bytes de código objeto en una arquitectura Cortex-M0, según la configuración.
Todo el código está escrito en un dialecto mínimo de C99 para una máxima compatibilidad con el compilador, se compila limpiamente en los niveles de advertencia más altos en clang + gcc + msvc, no genera problemas con UBsan o Asan y se prueba exhaustivamente en arquitecturas de 32 y 64 bits. . nanoprintf incluye encabezados estándar de C pero solo los usa para tipos C99 y listas de argumentos; no se realizan llamadas a stdlib/libc, con la excepción de cualquier llamada aritmética interna de números enteros grandes que su compilador pueda emitir. Como es habitual, se requieren algunos encabezados específicos de Windows si estás compilando de forma nativa para msvc.
nanoprintf es un archivo de encabezado único al estilo de las bibliotecas stb. El resto del repositorio son pruebas y andamios y no es necesario para su uso.
nanoprintf se puede configurar estáticamente para que los usuarios puedan encontrar un equilibrio entre el tamaño, los requisitos del compilador y el conjunto de funciones. La conversión de punto flotante, los modificadores de longitud "grandes" y la reescritura de tamaño son configurables y solo se compilan si se solicita explícitamente; consulte Configuración para obtener más detalles.
Agregue el siguiente código a uno de sus archivos fuente para compilar la implementación de nanoprintf:
// define your nanoprintf configuration macros here (see "Configuration" below) #define NANOPRINTF_IMPLEMENTATION #include "path/to/nanoprintf.h"
Luego, en cualquier archivo donde desee utilizar nanoprintf, simplemente incluya el encabezado y llame a las funciones 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 los ejemplos "Usar nanoprintf directamente" y "Envolver nanoprintf" para obtener más detalles.
Quería un printf directo de dominio público de un solo archivo que tuviera menos de 1 KB en la configuración mínima (cargadores de arranque, etc.) y menos de 3 KB con las campanas y silbidos de punto flotante habilitados.
En el trabajo de firmware, generalmente quiero el formato de cadena de stdio sin los requisitos de capa de descriptor de archivo o llamada al sistema; casi nunca son necesarios en sistemas pequeños donde desea iniciar sesión en pequeños buffers o emitir directamente a un bus. Además, muchas implementaciones stdio integradas son más grandes o más lentas de lo necesario; esto es importante para el trabajo del gestor de arranque. Si no necesita ninguna de las llamadas al sistema o las campanas + silbidos de stdio, simplemente puede usar nanoprintf y nosys.specs
y reducir su estructura.
Este código está optimizado para el tamaño, no para la legibilidad o la estructura. Desafortunadamente, la modularidad y la "limpieza" (lo que sea que eso signifique) agregan gastos generales a esta pequeña escala, por lo que la mayor parte de la funcionalidad y la lógica se integran en npf_vpprintf
. Este no es el aspecto que debería tener el código normal de los sistemas integrados; es una sopa #ifdef
y es difícil encontrarle sentido, y me disculpo si tienes que deleitarte en la implementación. Con suerte, las diversas pruebas te servirán como guía si las modificas.
Alternativamente, ¡quizás seas un programador significativamente mejor que yo! En ese caso, ayúdenme a hacer este código más pequeño y más limpio sin que la huella sea más grande, o empújenme en la dirección correcta. :)
nanoprintf tiene 4 funciones principales:
npf_snprintf
: Úselo como snprintf.
npf_vsnprintf
: Úselo como vsnprintf (compatible con va_list
).
npf_pprintf
: Úselo como printf con una devolución de llamada de escritura por carácter (semihosting, UART, etc.).
npf_vpprintf
: Úselo como npf_pprintf
pero toma va_list
.
Las variaciones pprintf
reciben una devolución de llamada que recibe el carácter a imprimir y un puntero de contexto proporcionado por el usuario.
Pase NULL
o nullptr
a npf_[v]snprintf
para no escribir nada y solo devolver la longitud de la cadena formateada.
nanoprintf no proporciona printf
ni putchar
; estos se consideran servicios a nivel de sistema y nanoprintf es una biblioteca de utilidades. Sin embargo, es de esperar que nanoprintf sea un buen componente básico para desarrollar su propio printf
.
Todas las funciones nanoprintf devuelven el mismo valor: la cantidad de caracteres que se enviaron a la devolución de llamada (para npf_pprintf) o la cantidad de caracteres que se habrían escrito en el búfer proporcionaron espacio suficiente. El byte 0 del terminador nulo no forma parte del recuento.
El estándar C permite que las funciones printf devuelvan valores negativos en caso de que no se puedan realizar codificaciones de cadenas o caracteres, o si el flujo de salida encuentra EOF. Dado que nanoprintf no tiene en cuenta los recursos del sistema operativo, como los archivos, y no admite el modificador de longitud l
para la compatibilidad con wchar_t
, cualquier error de tiempo de ejecución es un error interno (¡infórmelo!) o un uso incorrecto. Debido a esto, nanoprintf solo devuelve valores no negativos que representan cuántos bytes contiene la cadena formateada (nuevamente, menos el byte terminador nulo).
nanoprintf tiene los siguientes indicadores de configuración estática.
NANOPRINTF_USE_FIELD_WIDTH_FORMAT_SPECIFIERS
: establecido en 0
o 1
. Habilita los especificadores de ancho de campo.
NANOPRINTF_USE_PRECISION_FORMAT_SPECIFIERS
: establecido en 0
o 1
. Habilita especificadores de precisión.
NANOPRINTF_USE_FLOAT_FORMAT_SPECIFIERS
: establecido en 0
o 1
. Habilita especificadores de punto flotante.
NANOPRINTF_USE_LARGE_FORMAT_SPECIFIERS
: establecido en 0
o 1
. Habilita modificadores de gran tamaño.
NANOPRINTF_USE_BINARY_FORMAT_SPECIFIERS
: establecido en 0
o 1
. Habilita especificadores binarios.
NANOPRINTF_USE_WRITEBACK_FORMAT_SPECIFIERS
: establecido en 0
o 1
. Habilita %n
para reescritura.
NANOPRINTF_VISIBILITY_STATIC
: Definición opcional. Marca los prototipos como static
en el sandbox nanoprintf.
Si no se especifican indicadores de configuración, nanoprintf utilizará de forma predeterminada valores incrustados "razonables" en un intento de ser útil: los flotantes están habilitados, pero los formateadores de reescritura, binarios y grandes están deshabilitados. Si algún indicador de configuración se especifica explícitamente, nanoprintf requiere que todos los indicadores se especifiquen explícitamente.
Si se utiliza una función de especificador de formato deshabilitada, no se producirá ninguna conversión y en su lugar simplemente se imprimirá la cadena del especificador de formato.
nanoprintf tiene la siguiente configuración específica de punto flotante definida.
NANOPRINTF_CONVERSION_BUFFER_SIZE
: opcional, el valor predeterminado es 23
. Establece el tamaño de un búfer de caracteres utilizado para almacenar el valor convertido. Establezca un número mayor para permitir la impresión de números de punto flotante con más caracteres. El tamaño del búfer incluye la parte entera, la parte fraccionaria y el separador decimal, pero no incluye el signo ni los caracteres de relleno. Si el número no cabe en el búfer, se imprime un err
. Tenga cuidado con los tamaños grandes ya que el búfer de conversión se asigna en la memoria de la pila.
NANOPRINTF_CONVERSION_FLOAT_TYPE
: opcional, por defecto es unsigned int
. Establece el tipo de entero utilizado para el algoritmo de conversión flotante, que determina la precisión de la conversión. Se puede configurar en cualquier tipo de entero sin signo, como por ejemplo uint64_t
o uint8_t
.
De forma predeterminada, npf_snprintf y npf_vsnprintf se comportan de acuerdo con el estándar C: el búfer proporcionado se llenará pero no se desbordará. Si la cadena hubiera desbordado el búfer, se escribirá un byte terminador nulo en el byte final del búfer. Si el búfer es null
o de tamaño cero, no se escribirán bytes.
Si define NANOPRINTF_SNPRINTF_SAFE_EMPTY_STRING_ON_OVERFLOW
y su cadena es más grande que su búfer, el primer byte del búfer se sobrescribirá con un byte terminador nulo. Esto es similar en espíritu al snprintf_s de Microsoft.
En todos los casos, nanoprintf devolverá el número de bytes que se habrían escrito en el búfer si hubiera suficiente espacio. Este valor no tiene en cuenta el byte terminador nulo, de acuerdo con el estándar C.
nanoprintf usa solo memoria de pila y no primitivas de concurrencia, por lo que internamente no tiene en cuenta su entorno de ejecución. Esto hace que sea seguro llamar desde múltiples contextos de ejecución simultáneamente, o interrumpir una llamada npf_
con otra llamada npf_
(digamos, un ISR o algo así). Si usa npf_pprintf
simultáneamente con el mismo objetivo npf_putc
, depende de usted garantizar la corrección dentro de su devolución de llamada. Si npf_snprintf
desde varios subprocesos al mismo búfer, tendrá una carrera de datos obvia.
Al igual que printf
, nanoprintf
espera una cadena de especificación de conversión del siguiente formato:
[flags][field width][.precision][length modifier][conversion specifier]
Banderas
Ninguno o más de los siguientes:
0
: rellena el campo con ceros a la izquierda.
-
: Justifique a la izquierda el resultado de la conversión en el campo.
+
: las conversiones firmadas siempre comienzan con los caracteres +
o -
.
: (espacio) Se inserta un carácter de espacio si el primer carácter convertido no es un signo.
#
: Escribe caracteres adicionales ( 0x
para hexadecimal, .
para flotantes vacíos, '0' para octales vacíos, etc.).
Ancho de campo (si está habilitado)
Un número que especifica el ancho total del campo para la conversión agrega relleno. Si el ancho del campo es *
, el ancho del campo se lee desde el siguiente vararg.
Precisión (si está habilitada)
Con el prefijo .
, un número que especifica la precisión del número o cadena. Si la precisión es *
, la precisión se lee desde el siguiente vararg.
modificador de longitud
Ninguno o más de los siguientes:
h
: Utilice short
para ancho vararg integral y de reescritura.
L
: use long double
para el ancho de vararg flotante (nota: luego se reducirá a double
)
l
: Utilice un ancho de vararg long
, double
o ancho.
hh
: utilice char
para ancho de vararg integral y de reescritura.
ll
: (especificador grande) Use long long
para ancho de vararg integral y de reescritura.
j
: (especificador grande) Utilice los tipos [u]intmax_t
para ancho de vararg integral y de reescritura.
z
: (especificador grande) Utilice los tipos size_t
para ancho vararg integral y de reescritura.
t
: (especificador grande) Utilice los tipos ptrdiff_t
para ancho de vararg integral y de reescritura.
Especificador de conversión
Exactamente uno de los siguientes:
%
: literal de signo de porcentaje
c
: personaje
s
: cadenas terminadas en nulo
i
/ d
: enteros con signo
u
: enteros sin signo
o
: Enteros octales sin signo
x
/ X
: enteros hexadecimales sin signo
p
: punteros
n
: escribe el número de bytes escritos en el puntero vararg
f
/ F
: decimal de coma flotante
e
/ E
: Científico de punto flotante (no implementado, las impresiones son decimales flotantes)
g
/ G
: coma flotante más corta (no implementada, imprime decimal flotante)
a
/ A
: hexadecimal de punto flotante (no implementado, imprime decimal flotante)
b
/ B
: Enteros binarios
La conversión de punto flotante se realiza extrayendo las partes entera y fraccionaria del número en dos variables enteras separadas. Para cada parte, el exponente se escala de base 2 a base 10 multiplicando y dividiendo iterativamente la mantisa por 2 y 5 de forma apropiada. El orden de las operaciones de escala se selecciona dinámicamente (según el valor) para retener la mayor cantidad posible de bits más significativos de la mantisa. Cuanto más se aleje el valor del separador decimal, mayor será el error que se acumulará en la escala. Con un ancho de conversión de tipo entero de N
bits en promedio, el algoritmo conserva N - log2(5)
o N - 2.322
bits de precisión. Además, las partes enteras hasta 2 ^^ N - 1
y las partes fraccionarias con hasta N - 2.322
bits después del separador decimal se convierten perfectamente sin perder ningún bit.
Debido a que el código flotante -> fijo opera en los bits de valor flotante sin procesar, no se realizan operaciones de punto flotante. Esto permite a nanoprintf formatear flotantes de manera eficiente en arquitecturas de flotación suave como Cortex-M0, funcionar de manera idéntica con o sin optimizaciones como "matemáticas rápidas" y minimizar la huella del código.
Los especificadores %e
/ %E
, %a
/ %A
y %g
/ %G
se analizan pero no se formatean. Si se usa, la salida será idéntica a si se usara %f
/ %F
. ¡Las solicitudes de extracción son bienvenidas! :)
No existe soporte para caracteres anchos: los campos %lc
y %ls
requieren que arg se convierta en una matriz de caracteres como si fuera una llamada a wcrtomb. Cuando intervienen conversiones de configuración regional y de conjuntos de caracteres, es difícil mantener el nombre "nano". En consecuencia, %lc
y %ls
se comportan como %c
y %s
, respectivamente.
Actualmente, las únicas conversiones flotantes admitidas son las formas decimales: %f
y %F
. ¡Las solicitudes de extracción son bienvenidas!
La compilación de CI está configurada para usar gcc y nm para medir el tamaño compilado de cada solicitud de extracción. Consulte el resultado del trabajo de "informes de tamaño" de comprobaciones previas al envío para ver ejecuciones recientes.
Las siguientes medidas de tamaño se toman en comparación con la construcción 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 obtener el entorno y ejecutar pruebas:
Clona o bifurca este repositorio.
Ejecute ./b
desde la raíz (o py -3 build.py
desde la raíz, para usuarios de Windows)
Esto creará todas las pruebas unitarias, de conformidad y de compilación para su entorno host. Cualquier error en la prueba devolverá un código de salida distinto de cero.
El entorno de desarrollo nanoprintf utiliza cmake y ninja. Si los tiene en su camino, ./b
los usará. De lo contrario, ./b
los descargará y los implementará en path/to/your/nanoprintf/external
.
nanoprintf usa GitHub Actions para todas las compilaciones de integración continua. Las compilaciones de GitHub Linux utilizan esta imagen de Docker de mi repositorio de Docker.
La matriz compila [Depurar, Lanzar] x [32 bits, 64 bits] x [Mac, Windows, Linux] x [gcc, clang, msvc], menos las configuraciones clang de Mac de 32 bits.
Un conjunto de pruebas es una bifurcación del conjunto de pruebas printf, que tiene licencia del MIT. Existe como un submódulo para fines de concesión de licencias: nanoprintf es de dominio público, por lo que este conjunto de pruebas en particular es opcional y está excluido de forma predeterminada. Para compilarlo, recupérelo actualizando los submódulos y agregue el indicador --paland
a su invocación ./b
. No es necesario utilizar nanoprintf en absoluto.
La idea básica de la conversión de flotante a int se inspiró en el algoritmo fijo float -> 64:64 de Wojciech Muła y se amplió aún más agregando escalado dinámico y ancho de entero configurable por Oskars Rubenis.
Porté el conjunto de pruebas de printf a nanoprintf. Originario del código base del proyecto mpaland printf, pero adoptado y mejorado por Eyal Rozenberg y otros. (Nanoprintf tiene muchas pruebas propias, ¡pero estas también son muy completas y muy buenas!)
La implementación binaria se basa en los requisitos especificados por la propuesta N2630 de Jörg Wunsch, ¡que se espera que sea aceptada en el C23!