nanoprintf — это свободная реализация snprintf и vsnprintf для встраиваемых систем, которые при полной активации стремятся обеспечить соответствие стандарту C11. Основными исключениями являются форматы с плавающей запятой, экспоненциальная запись ( %e
, %g
, %a
) и преобразования, требующие существования wcrtomb
. Двоичный целочисленный вывод C23 поддерживается дополнительно согласно N2630. Расширения безопасности для snprintf и vsnprintf можно дополнительно настроить для возврата обрезанных или полностью пустых строк при событиях переполнения буфера.
Кроме того, nanoprintf можно использовать для анализа строк формата в стиле printf для извлечения различных параметров и спецификаторов преобразования без какого-либо фактического форматирования текста.
nanoprintf не выделяет память и использует менее 100 байт стека. Он компилируется примерно в 740–2640 байт объектного кода на архитектуре Cortex-M0, в зависимости от конфигурации.
Весь код написан на минимальном диалекте C99 для максимальной совместимости с компилятором, чисто компилируется с высшими уровнями предупреждений на clang + gcc + msvc, не вызывает проблем со стороны UBsan или Asan и тщательно протестирован на 32-битных и 64-битных архитектурах. . nanoprintf включает стандартные заголовки C, но использует их только для типов C99 и списков аргументов; никакие вызовы не выполняются в stdlib/libc, за исключением любых внутренних арифметических вызовов больших целых чисел, которые может генерировать ваш компилятор. Как обычно, при компиляции для msvc требуются некоторые заголовки, специфичные для Windows.
nanoprintf — это один заголовочный файл в стиле библиотек stb. Остальная часть репозитория представляет собой тесты и леса и не требуется для использования.
nanoprintf статически настраивается, поэтому пользователи могут найти баланс между размером, требованиями компилятора и набором функций. Преобразование чисел с плавающей запятой, модификаторы «большой» длины и обратная запись размера — все это настраиваются и компилируются только по явному запросу. Подробности см. в разделе «Конфигурация».
Добавьте следующий код в один из исходных файлов, чтобы скомпилировать реализацию nanoprintf:
// define your nanoprintf configuration macros here (see "Configuration" below) #define NANOPRINTF_IMPLEMENTATION #include "path/to/nanoprintf.h"
Затем в любой файл, где вы хотите использовать nanoprintf, просто включите заголовок и вызовите функции 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"); }
Дополнительные сведения см. в примерах «Использовать nanoprintf напрямую» и «Обернуть nanoprintf».
Мне нужен был однофайловый общедоступный вставной файл printf размером менее 1 КБ в минимальной конфигурации (загрузчики и т. д.) и менее 3 КБ с включенными наворотами с плавающей запятой.
При работе с прошивкой мне обычно требуется форматирование строк stdio без требований к уровню системных вызовов или файловых дескрипторов; они почти никогда не нужны в крошечных системах, где вы хотите войти в небольшие буферы или отправить данные непосредственно на шину. Кроме того, многие реализации встроенного stdio больше или медленнее, чем нужно — это важно для работы загрузчика. Если вам не нужны системные вызовы или навороты stdio, вы можете просто использовать nanoprintf и nosys.specs
и уменьшить свою сборку.
Этот код оптимизирован по размеру, а не по читаемости или структуре. К сожалению, модульность и «чистота» (что бы это ни значило) добавляют накладные расходы в таком небольшом масштабе, поэтому большая часть функциональности и логики объединена в npf_vpprintf
. Это не то, как должен выглядеть код обычных встроенных систем; это суп #ifdef
, и его сложно понять, и я прошу прощения, если вам придется копаться в реализации. Надеюсь, различные тесты послужат вам ориентиром, если вы в этом разберетесь.
С другой стороны, возможно, вы значительно лучший программист, чем я! В этом случае, пожалуйста, помогите мне сделать этот код меньше и чище, не увеличивая его объем, или подтолкните меня в правильном направлении. :)
nanoprintf имеет 4 основные функции:
npf_snprintf
: использовать как snprintf.
npf_vsnprintf
: используйте как vsnprintf (поддержка va_list
).
npf_pprintf
: используйте как printf с обратным вызовом записи для каждого символа (полухостинг, UART и т. д.).
npf_vpprintf
: используется как npf_pprintf
, но принимает va_list
.
Варианты pprintf
принимают обратный вызов, который получает символ для печати и предоставленный пользователем указатель контекста.
Передайте NULL
или nullptr
в npf_[v]snprintf
, чтобы ничего не записывать и возвращать только длину отформатированной строки.
nanoprintf не предоставляет сами printf
или putchar
; они рассматриваются как службы системного уровня, а nanoprintf — это служебная библиотека. Надеемся, что nanoprintf станет хорошим строительным блоком для создания собственного printf
.
Все функции nanoprintf возвращают одно и то же значение: количество символов, которые были либо отправлены в обратный вызов (для npf_pprintf), либо количество символов, которые были бы записаны в буфер при наличии достаточного места. Нулевой байт-терминатор 0 не является частью счетчика.
Стандарт C позволяет функциям printf возвращать отрицательные значения в случае, если кодирование строк или символов невозможно выполнить или если выходной поток обнаруживает EOF. Поскольку nanoprintf не обращает внимания на ресурсы ОС, такие как файлы, и не поддерживает модификатор длины l
для поддержки wchar_t
, любые ошибки во время выполнения являются либо внутренними ошибками (пожалуйста, сообщите!), либо неправильным использованием. По этой причине nanoprintf возвращает только неотрицательные значения, представляющие количество байтов, содержащихся в форматированной строке (опять же, за вычетом байта нулевого терминатора).
nanoprintf имеет следующие статические флаги конфигурации.
NANOPRINTF_USE_FIELD_WIDTH_FORMAT_SPECIFIERS
: установите значение 0
или 1
. Включает спецификаторы ширины поля.
NANOPRINTF_USE_PRECISION_FORMAT_SPECIFIERS
: установлено значение 0
или 1
. Включает спецификаторы точности.
NANOPRINTF_USE_FLOAT_FORMAT_SPECIFIERS
: установлено в 0
или 1
. Включает спецификаторы с плавающей запятой.
NANOPRINTF_USE_LARGE_FORMAT_SPECIFIERS
: установите значение 0
или 1
. Включает модификаторы большого размера.
NANOPRINTF_USE_BINARY_FORMAT_SPECIFIERS
: установлено в 0
или 1
. Включает двоичные спецификаторы.
NANOPRINTF_USE_WRITEBACK_FORMAT_SPECIFIERS
: установите значение 0
или 1
. Включает %n
для обратной записи.
NANOPRINTF_VISIBILITY_STATIC
: Необязательное определение. Помечает прототипы как static
для песочницы nanoprintf.
Если флаги конфигурации не указаны, nanoprintf по умолчанию будет использовать «разумные» встроенные значения, пытаясь быть полезным: числа с плавающей запятой включены, но обратная запись, двоичные форматы и средства форматирования большого размера отключены. Если какие-либо флаги конфигурации указаны явно, nanoprintf требует, чтобы все флаги были указаны явно.
Если используется отключенная функция спецификатора формата, преобразования не произойдет, а вместо этого будет просто напечатана строка спецификатора формата.
nanoprintf имеет следующие определения конфигурации с плавающей запятой.
NANOPRINTF_CONVERSION_BUFFER_SIZE
: необязательно, по умолчанию 23
. Устанавливает размер буфера символов, используемого для хранения преобразованного значения. Установите большее число, чтобы разрешить печать чисел с плавающей запятой с большим количеством символов. Размер буфера включает целую часть, дробную часть и десятичный разделитель, но не включает знак и символы заполнения. Если число не помещается в буфер, выводится сообщение err
. Будьте осторожны с большими размерами, поскольку буфер преобразования размещается в памяти стека.
NANOPRINTF_CONVERSION_FLOAT_TYPE
: необязательно, по умолчанию используется unsigned int
. Устанавливает целочисленный тип, используемый для алгоритма преобразования с плавающей запятой, который определяет точность преобразования. Может быть установлен любой беззнаковый целочисленный тип, например uint64_t
или uint8_t
.
По умолчанию npf_snprintf и npf_vsnprintf ведут себя в соответствии со стандартом C: предоставленный буфер будет заполнен, но не переполнен. Если строка переполнит буфер, в последний байт буфера будет записан нулевой байт-терминатор. Если буфер имеет null
размер или нулевой размер, байты не будут записаны.
Если вы определяете NANOPRINTF_SNPRINTF_SAFE_EMPTY_STRING_ON_OVERFLOW
и ваша строка больше, чем ваш буфер, первый байт буфера будет перезаписан байтом с нулевым терминатором. По духу это похоже на snprintf_s от Microsoft.
Во всех случаях nanoprintf вернет количество байтов, которые были бы записаны в буфер, если бы там было достаточно места. Это значение не учитывает нулевой байт-терминатор в соответствии со стандартом C.
nanoprintf использует только стековую память и не использует примитивы параллелизма, поэтому внутри он не обращает внимания на среду выполнения. Это делает безопасным одновременный вызов из нескольких контекстов выполнения или прерывание вызова npf_
другим вызовом npf_
(скажем, ISR или чем-то еще). Если вы используете npf_pprintf
одновременно с той же целью npf_putc
, вы должны обеспечить корректность внутри вашего обратного вызова. Если вы npf_snprintf
из нескольких потоков в один и тот же буфер, у вас будет очевидная гонка данных.
Как и printf
, nanoprintf
ожидает строку спецификации преобразования следующего вида:
[flags][field width][.precision][length modifier][conversion specifier]
Флаги
Ничего или более из следующего:
0
: заполнить поле начальными нулевыми символами.
-
: выровнять результат преобразования по левому краю.
+
: Преобразования со знаком всегда начинаются с символов +
или -
.
: (пробел) Символ пробела вставляется, если первый преобразованный символ не является знаком.
#
: записывает дополнительные символы ( 0x
для шестнадцатеричных чисел, .
для пустых чисел с плавающей запятой, '0' для пустых восьмеричных чисел и т. д.).
Ширина поля (если включено)
Число, определяющее общую ширину поля для преобразования, добавляет дополнение. Если ширина поля равна *
, ширина поля считывается из следующего vararg.
Точность (если включена)
С префиксом .
, число, определяющее точность числа или строки. Если точность равна *
, точность считывается из следующего vararg.
Модификатор длины
Ничего или более из следующего:
h
: используйте short
для целого значения и ширины переменного аргумента с обратной записью.
L
: используйте long double
для ширины vararg с плавающей запятой (примечание: затем она будет уменьшена до double
)
l
: используйте long
, double
или широкую переменную ширину.
hh
: используйте char
для целочисленной ширины и ширины vararg с обратной записью.
ll
: (большой спецификатор) Используйте long long
для целочисленной ширины vararg и обратной записи.
j
: (большой спецификатор) Используйте типы [u]intmax_t
для целочисленной ширины переменных аргументов и ширины обратной записи.
z
: (большой спецификатор) Используйте типы size_t
для целочисленной ширины vararg и обратной записи.
t
: (большой спецификатор) Используйте типы ptrdiff_t
для целочисленной ширины переменных аргументов и обратной записи.
Спецификатор преобразования
Точно одно из следующих:
%
: литерал со знаком процента.
c
: Персонаж
s
: строки с нулевым завершением
i
/ d
: целые числа со знаком
u
: Целые числа без знака
o
: Восьмеричные целые числа без знака.
x
/ X
: шестнадцатеричные целые числа без знака.
p
: Указатели
n
: записать количество байтов, записанных в указатель vararg.
f
/ F
: десятичное число с плавающей запятой
e
/ E
: Научное значение с плавающей запятой (не реализовано, печатает десятичное число с плавающей запятой)
g
/ G
: самое короткое число с плавающей запятой (не реализовано, печатает десятичное число с плавающей запятой)
a
/ A
: шестнадцатеричное число с плавающей запятой (не реализовано, печатает десятичное число с плавающей запятой)
b
/ B
: двоичные целые числа
Преобразование чисел с плавающей запятой выполняется путем извлечения целой и дробной частей числа в две отдельные целочисленные переменные. Затем для каждой части показатель степени масштабируется от основания 2 до основания 10 путем итеративного умножения и деления мантиссы на 2 и 5 соответственно. Порядок операций масштабирования выбирается динамически (в зависимости от значения), чтобы сохранить как можно больше старших бит мантиссы. Чем дальше значение от десятичного разделителя, тем больше ошибок будет накапливаться при масштабировании. При ширине целочисленного типа преобразования в среднем N
бит алгоритм сохраняет точность N - log2(5)
или N - 2.322
бита. Кроме того, целые части до 2 ^^ N - 1
и дробные части до N - 2.322
бит после десятичного разделителя преобразуются идеально, без потери битов.
Поскольку код float -> фиксированный работает с необработанными битами значений с плавающей запятой, никакие операции с плавающей запятой не выполняются. Это позволяет nanoprintf эффективно форматировать числа с плавающей запятой на архитектурах с мягким плавающим числом, таких как Cortex-M0, работать одинаково с такими оптимизациями, как «быстрая математика», или без них, а также минимизировать объем кода.
Спецификаторы %e
/ %E
, %a
/ %A
и %g
/ %G
анализируются, но не форматируются. Если используется, вывод будет идентичен тому, если бы использовался %f
/ %F
. Запросы на вытягивание приветствуются! :)
Поддержки расширенных символов не существует: поля %lc
и %ls
требуют, чтобы аргумент был преобразован в массив символов, как если бы это был вызов wcrtomb. Когда задействованы преобразования локали и набора символов, трудно сохранить имя «нано». Соответственно, %lc
и %ls
ведут себя как %c
и %s
соответственно.
В настоящее время единственными поддерживаемыми преобразованиями с плавающей запятой являются десятичные формы: %f
и %F
. Запросы на вытягивание приветствуются!
Сборка CI настроена на использование gcc и nm для измерения скомпилированного размера каждого запроса на включение. См. выходные данные задания предварительной проверки «отчеты о размерах» для последних запусков.
Следующие измерения размера сделаны для сборки 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
Чтобы получить среду и запустить тесты:
Клонируйте или разветвите этот репозиторий.
Запустите ./b
из корня (или py -3 build.py
из корня для пользователей Windows).
Это позволит построить все модульные тесты, тесты на соответствие и тесты компиляции для вашей хост-среды. Любые неудачные тесты вернут ненулевой код выхода.
Среда разработки nanoprintf использует cmake и ninja. Если они есть на вашем пути, ./b
будет их использовать. Если нет, ./b
загрузит и развернет их по path/to/your/nanoprintf/external
.
nanoprintf использует GitHub Actions для всех сборок непрерывной интеграции. Сборки GitHub Linux используют этот образ Docker из моего репозитория Docker.
Матрица строит [Debug, Release] x [32-bit, 64-bit] x [Mac, Windows, Linux] x [gcc, clang, msvc] за вычетом 32-битных конфигураций Clang Mac.
Один набор тестов является ответвлением набора тестов printf, имеющего лицензию MIT. Он существует как подмодуль для целей лицензирования: nanoprintf является общественным достоянием, поэтому этот конкретный набор тестов является необязательным и исключен по умолчанию. Чтобы собрать его, извлеките его, обновив подмодули и добавив флаг --paland
к вызову ./b
. Использовать nanoprintf вообще не обязательно.
Основная идея преобразования чисел с плавающей точкой в целое число была вдохновлена фиксированным алгоритмом Войцеха Мулы float -> 64:64 и расширена за счет добавления динамического масштабирования и настраиваемой целочисленной ширины Оскара Рубениса.
Я портировал набор тестов printf в nanoprintf. Первоначально он был взят из кодовой базы проекта mpaland printf, но был принят и улучшен Эялем Розенбергом и другими. (У Nanoprintf есть много собственных тестов, но они тоже очень тщательные и очень хорошие!)
Бинарная реализация основана на требованиях, указанных в предложении Йорга Вунша N2630, которое, будем надеяться, будет принято в C23!