Пользовательские функции printf в PHP

Вы все знаете printf() из libc и его семейство. Эта глава подробно расскажет о многочисленных клонах, которые объявляет и использует PHP, об их цели, зачем их использовать и когда.

Note

Документация libc по printf() и похожим функциям находится здесь

Вы знаете, что эти функции полезны, но иногда не предоставляют достаточной функциональности. Также вы знаете, что добавление форматных строк в семейство printf() — задача нетривиальная, непортируемая и небезопасная.

PHP добавляет свои собственные функции, подобные printf, чтобы заменить функции libc и использовать их для внутренней разработки. Они в основном добавят новые форматы, будут работать с zend_string вместо char * и так далее… Давайте рассмотрим их вместе.

Warning

Вы должны освоить стандартные форматы printf() из libc. Прочитайте их документацию здесь.

Note

Эти функции добавлены для замены функций libc, это означает, что если вы используете, например, sprintf(), это не приведёт к вызову sprintf() из libc, а к замене PHP. За исключением традиционного printf(), всё остальное заменено.

Традиционное использование

Прежде всего, вам не следует использовать sprintf(), так как эта функция не выполняет никаких проверок и допускает множество ошибок переполнения буфера. Пожалуйста, старайтесь избегать её использования.

Warning

Пожалуйста, старайтесь максимально избегать использования sprintf().

Далее у вас есть выбор.

Вы знаете размер результирующего буфера

Если вы знаете размер своего буфера, snprintf() или slprintf() сделают эту работу за вас. Есть разница в том, что возвращают эти функции, но не в том, что они делают.

Обе они печатают согласно переданным форматам, и обе завершают ваш буфер байтом NUL ‘\0’, что бы ни случилось. Однако snprintf() возвращает количество символов, которые могли бы быть использованы, тогда как slprintf() возвращает количество символов, которые были фактически использованы, таким образом позволяя обнаружить слишком маленькие буферы и обрезание строки. Это не считая финального ‘\0’.

Вот пример, чтобы вы полностью поняли:

char foo[8]; /* 8-char large buffer */
const char str[] = "Hello world"; /* 12 chars including \0 in count */
int r;

r = snprintf(foo, sizeof(foo), "%s", str);
/* r = 11 here even if only 7 printable chars were written in foo */

/* foo value is now 'H' 'e' 'l' 'l' 'o' ' ' 'w' '\0' */

snprintf() — не очень хорошая функция для использования, так как она не позволяет обнаружить возможное обрезание строки. Как видно из примера выше, “Hello world\0” не помещается в восьмибайтный буфер, это очевидно, но snprintf() всё равно возвращает вам 11, что является strlen("Hello world\0"). У вас нет способа обнаружить, что строка была обрезана.

Вот slprintf():

char foo[8]; /* 8-char large buffer */
const char str[] = "Hello world"; /* 12 chars including \0 in count */
int r;

r = slprintf(foo, sizeof(foo), "%s", str);
/* r = 7 here , because 7 printable chars were written in foo */

/* foo value is now 'H' 'e' 'l' 'l' 'o' ' ' 'w' '\0' */

С slprintf() результирующий буфер foo содержит абсолютно ту же строку, но возвращаемое значение теперь 7. 7 меньше, чем 11 символов строки “Hello world”, таким образом вы можете обнаружить, что она была обрезана:

if (slprintf(foo, sizeof(foo), "%s", str) < strlen(str)) {
    /* A string truncation occurred */
}

Запомните:

  • Эти две функции всегда завершают строку NUL, независимо от обрезания. Результирующие строки, таким образом, являются безопасными C-строками.

  • Только slprintf() позволяет обнаружить обрезание строки.

Эти две функции определены в main/snprintf.c

Вы не знаете размер своего буфера

Теперь, если вы не знаете размер результирующего буфера, вам нужен динамически выделенный буфер, и тогда вы воспользуетесь spprintf(). Помните, что вам придётся самостоятельно освобождать буфер!

Вот пример:

#include <time.h>

char *result;
int r;

time_t timestamp = time(NULL);

r = spprintf(&result, 0, "Here is the date: %s", asctime(localtime(&timestamp)));

/* now use result that contains something like "Here is the date: Thu Jun 15 19:12:51 2017\n" */

efree(result);

spprintf() возвращает количество символов, которые были выведены в результирующий буфер, не считая финального ‘\0’, следовательно, вы знаете количество байт, которые были выделены для вас (минус один).

Обратите внимание, что выделение памяти выполняется с помощью ZendMM (выделение в рамках запроса), и поэтому должно использоваться как часть запроса и освобождаться с помощью efree(), а не free().

Note

Глава о менеджере памяти Zend (ZendMM) подробно рассказывает о том, как динамическая память выделяется в PHP.

Если вы хотите ограничить размер буфера, передайте этот предел в качестве второго аргумента, если вы передадите 0, это означает неограниченный размер:

#include <time.h>

char *result;
int r;

time_t timestamp = time(NULL);

/* Do not print more than 10 bytes || allocate more than 11 bytes */
r = spprintf(&result, 10, "Here is the date: %s", asctime(localtime(&timestamp)));

/* r == 10 here, and 11 bytes were allocated into result */

efree(result);

Note

Когда это возможно, старайтесь не использовать динамическое выделение памяти. Это влияет на производительность. Если у вас есть выбор, используйте статически выделенный на стеке буфер.

spprintf() описана в main/spprintf.c.

А что насчёт printf()?

Если вам нужно вызвать printf(), то есть напечатать форматированный вывод в поток вывода, используйте php_printf(). Эта функция внутренне использует spprintf() и, таким образом, выполняет динамическое выделение памяти, которое сама же освобождает сразу после отправки его в вывод SAPI, то есть stdout в случае CLI, или буфер вывода (например, буфер CGI) для других SAPI.

Специальные форматы printf в PHP

Помните, что PHP заменяет большинство функций printf() из libc своими собственными, собственного дизайна. Вы можете посмотреть на API разбора аргументов, который легко понять, прочитав исходный код.

Это означает, что алгоритм разбора аргументов был полностью переписан и может отличаться от того, к чему вы привыкли в libc. Например, локаль libc в большинстве случаев не учитывается.

Могут использоваться специальные форматы, такие как “%I64”, чтобы явно вывести int64, или “%I32”. Вы также можете использовать “%Z”, чтобы сделать zval печатаемым (согласно правилам приведения PHP к строке), это отличное дополнение.

Форматировщик также распознает бесконечные числа и выведет “INF”, или “NAN” для не-числа.

Если вы ошибётесь и попросите форматировщик вывести указатель NULL, где libc точно рухнет, PHP вернёт строку “(null)” в качестве результата.

Note

Если в printf вы видите появляющееся магическое “(null)”, это означает, что вы передали указатель NULL одной из функций семейства printf в PHP.

Вывод с помощью printf() в zend_string

Поскольку zend_string — очень распространённая структура в исходном коде PHP, вам может понадобиться выполнить printf() в zend_string вместо традиционного C char *. Для этого используйте strpprintf().

API выглядит так: zend_string *strpprintf(size_t max_len, const char *format, ...), это означает, что вам возвращается zend_string, а не количество напечатанных символов, как вы могли бы ожидать. Вы можете ограничить это число, однако, используя первый параметр (передайте 0, чтобы означать бесконечность); и вы должны помнить, что zend_string будет выделен с помощью менеджера памяти Zend и, таким образом, привязан к текущему запросу.

Очевидно, что API форматирования общий с тем, что мы видели выше.

Вот краткий пример:

zend_string *result;

result = strpprintf(0, "You are using PHP %s", PHP_VERSION);

/* Do something with result */

zend_string_release(result);

Замечание об API zend_

Вы можете встретить функции zend_spprintf() или zend_strpprintf(). Это точно такие же функции, как те, что мы видели выше.

Они просто существуют как часть разделения между движком Zend и ядром PHP, деталь, которая для нас не важна, так как в исходном коде всё смешано вместе.