API zend_string¶
Строки в C обычно представляются в виде null-terminated указателей char *. Поскольку PHP поддерживает строки,
содержащие нулевые байты, PHP должен явно хранить длину строки. Кроме того, PHP должен обеспечивать соответствие
строк общей концепции структур с подсчётом ссылок. Именно для этого предназначен тип zend_string.
Структура¶
Структура zend_string выглядит следующим образом:
struct _zend_string {
zend_refcounted_h gc;
zend_ulong h;
size_t len;
char val[1];
};
Как и многие другие структуры в PHP, она включает в себя заголовок zend_refcounted_h, который хранит
подсчёт ссылок, а также некоторые флаги.
Фактическое содержимое символов строки хранится с помощью так называемого “struct hack”: содержимое строки
дописывается в конец структуры. Хотя оно объявлено как char[1], фактический размер определяется динамически.
Это означает, что заголовок zend_string и содержимое строки объединены в единую область выделенной памяти, что
более эффективно, чем использование двух отдельных выделений. Вы увидите, что PHP использует struct hack в довольно
многих местах, где заголовок фиксированного размера сочетается с динамическим объёмом данных.
Длина строки хранится явно в поле len. Это необходимо для поддержки строк, содержащих нулевые байты, а также
полезно для производительности, поскольку длину строки не нужно постоянно пересчитывать. Следует отметить, что хотя
len хранит длину без учёта завершающего нулевого байта, фактическое содержимое строки в val всегда должно
содержать завершающий нулевой байт. Причина в том, что существует довольно много C API, принимающих
null-terminated строку, и мы хотим использовать эти API без создания отдельной null-terminated копии строки.
Например, строка PHP "foo\0bar" будет храниться с len = 7, но val = "foo\0bar\0".
Наконец, строка хранит кэш значения хеша h, который используется при использовании строк в качестве ключей
хеш-таблицы. Изначально он имеет значение 0, означающее, что хеш ещё не вычислен, а
настоящий хеш вычисляется при первом использовании.
Макросы доступа к строке¶
Как и с zval’ами, вы не манипулируете полями zend_string напрямую, а используете ряд макросов
доступа:
zend_string *str = zend_string_init("foo", strlen("foo"), 0);
php_printf("This is my string: %s\n", ZSTR_VAL(str));
php_printf("It is %zd char long\n", ZSTR_LEN(str)); // %zd is the printf format for size_t
zend_string_release(str);
Два самых важных из них — ZSTR_VAL(), который возвращает содержимое строки как char *, и ZSTR_LEN(),
который возвращает длину строки как size_t.
Именование этих макросов немного неудачное: существуют как ZSTR_VAL/ZSTR_LEN, так и
Z_STRVAL/Z_STRLEN, и отличаются они только позицией символа подчёркивания. Запомните, что макросы ZSTR_*
работают с zend_string, а макросы Z_ работают с zval:
zval val;
ZVAL_STRING(&val, "foo");
// Z_STRLEN, Z_STRVAL work on zval.
php_printf("string(%zd) \"%s\"\n", Z_STRLEN(val), Z_STRVAL(val));
// ZSTR_LEN, ZSTR_VAL work on zend_string.
zend_string *str = Z_STR(val);
php_printf("string(%zd) \"%s\"\n", ZSTR_LEN(str), ZSTR_VAL(str));
zval_ptr_dtor(&val);
Доступ к кэшу значения хеша строки можно получить с помощью ZSTR_H(). Однако это обращение к необработанному
кэшу, который будет равен нулю, если хеш ещё не вычислен. Вместо этого следует использовать ZSTR_HASH() или
zend_string_hash_val(), чтобы либо получить предварительно вычисленный хеш, либо вычислить его. В очень редком
случае, когда строка модифицируется после первоначального создания, можно сбросить закэшированное значение с помощью
zend_string_forget_hash_val().
Управление памятью¶
Хотя мы уже знаем, как инициализировать строковые zval’ы, единственным представленным
до сих пор прямым API создания строк является zend_string_init(), который используется для создания
zend_string из существующей строки и длины.
Наиболее фундаментальной функцией создания строк, на которой основаны все остальные, является
zend_string_alloc():
size_t len = 40;
zend_string *str = zend_string_alloc(len, /* persistent */ 0);
for (size_t i = 0; i < len; i++) {
ZSTR_VAL(str)[i] = 'a';
}
// Don't forget to null-terminate!
ZSTR_VAL(str)[len] = '\0';
Эта функция выделяет строку заданной длины (как всегда, длина не включает завершающий нулевой байт), оставляя её инициализацию на ваше усмотрение. Как и все функции выделения строк, она принимает параметр, определяющий, использовать ли аллокатор, привязанный к запросу, или постоянный.
Функция zend_string_safe_alloc(n, m, l, persistent) выделяет строку длины n * m + l. Эта функция часто
полезна при изменении кодировки. Например, вот как можно было бы перевести строку в hex-представление:
zend_string *convert_to_hex(zend_string *orig_str) {
zend_string *hex_str = zend_string_safe_alloc(2, ZSTR_LEN(orig_str), 0, /* persistent */ 0);
char *p = ZSTR_VAL(str);
for (size_t i = 0; i < ZSTR_LEN(orig_str), i++) {
const char *to_hex = "0123456789abcdef";
unsigned char c = ZSTR_VAL(orig_str)[i];
*p++ = to_hex[c >> 4];
*p++ = to_hex[c & 0xf];
}
*p = '\0';
return hex_str;
}
Почему нельзя просто использовать zend_string_alloc(2 * ZSTR_LEN(orig_str), 0)? Причина в том, что функция
zend_string_safe_alloc() гарантирует, что вычисление n * m + l не приведёт к переполнению. Например, если
вы работаете на 32-битной системе, и строка имеет размер ровно 2 ГБ, то умножение длины на два приведёт к
переполнению и даст нулевую длину. В результате код выйдет за границы выделенной области и повредит несвязанную
память. API zend_string_safe_alloc() обнаруживает такую ситуацию и в этом случае выбрасывает фатальную ошибку.
Также можно изменить размер строки с помощью zend_string_realloc() и его вариаций:
zend_string *zend_string_realloc(zend_string *s, size_t len, bool persistent);
// Requires new length larger old length.
zend_string *zend_string_extend(zend_string *s, size_t len, bool persistent);
// Requires new length smaller new length.
zend_string *zend_string_truncate(zend_string *s, size_t len, bool persistent)
// n * m + l safe variant of zend_string_realloc.
zend_string *zend_string_safe_realloc(zend_string *s, size_t n, size_t m, size_t l, bool persistent);
Поскольку строки являются структурами с подсчётом ссылок, функции realloc также учитывают счётчик ссылок. Хотя реализованы эти функции иначе, по семантике они эквивалентны выполнению чего-то подобного:
zend_string *new_str = zend_string_init(ZSTR_VAL(s), ZSTR_LEN(s), persistent);
zend_string_release(s);
return new_str;
То есть эти функции освобождают переданную им строку, но их безопасно использовать с общими (или неизменяемыми) строками. Если строка общая, счётчик ссылок уменьшается, но сама строка не уничтожается.
Это подводит нас к следующей теме — управлению счётчиком ссылок. Вместо использования низкоуровневых макросов
GC_*, API zend_string содержит два помощника для увеличения счётчика ссылок:
zend_string_addref(str);
return str;
// More compact:
return zend_string_copy(str);
В отличие от GC_ADDREF(), функция zend_string_addref() корректно обрабатывает неизменяемые строки. Однако
самой часто используемой функцией является zend_string_copy(). Эта функция не только увеличивает счётчик
ссылок, но и возвращает исходную строку, что делает код более читаемым на практике.
Также существует функция zend_string_dup(), выполняющая настоящее копирование строки (а не просто увеличение
счётчика ссылок), но её поведение часто считается запутывающим, поскольку она копирует только неизменяемые
(не-immutable) строки. Если вы хотите принудительно создать копию строки, лучше создать новую с помощью
zend_string_init().
Если дублирование нужно для модификации уже существующей строки, вместо этого можно использовать
zend_string_separate():
zend_string *modify_char(zend_string *orig_str) {
zend_string *str = zend_string_separate(orig_str, /* persistent */ 0);
ZEND_ASSERT(ZSTR_LEN(str) > 0);
ZSTR_VAL(str)[0] = 'A';
return str;
}
Как и в общей концепции разделения zval’ов, эта функция вернёт исходную строку (со сброшенным кэшем хеша), если её счётчик ссылок равен единице и она, таким образом, принадлежит только одному владельцу, а в противном случае создаст копию.
Наконец, строки нужно освобождать, когда они больше не используются. Вы уже знакомы с API zend_string_release(),
который уменьшает счётчик ссылок и освобождает строку, если он опускается до нуля. Как правило, достаточно
использовать только эту функцию.
Однако вы можете встретить и ряд оптимизированных вариаций. Самая распространённая из них —
zend_string_release_ex(), которая позволяет явно указать, является переданная строка постоянной или
непостоянной:
zend_string_release_ex(str, /* persistent */ 0);
Обычно это определяется на основе флагов строки. Явное указание позволяет избежать проверки во время выполнения и генерирует меньше кода. Наконец, есть ещё две функции, работающие только со строками, у которых счётчик ссылок равен единице:
// Requires refcount 1 or immutable.
zend_string_free(str);
// Requires refcount 1 and not immutable.
zend_string_efree(str);
Использования этих функций следует избегать, так как легко внести критические ошибки, если какой-либо API изменится и начнёт возвращать не новые строки, а переиспользовать уже существующие.
Другие операции¶
API zend_string поддерживает несколько дополнительных операций. Самая распространённая — сравнение строк:
zend_string *foo = zend_string_init("foo", sizeof("foo")-1, 0);
zend_string *FOO = zend_string_init("FOO", sizeof("FOO")-1, 0);
// Case-sensitive comparison between zend_strings.
bool result = zend_string_equals(foo, FOO); // false
// Case-insensitive comparison between zend_strings.
bool result = zend_string_equals_ci(foo, FOO); // true
// Case-sensitive comparison with a string literal.
bool result = zend_string_equals_literal(foo, "FOO"); // false
// Case-insensitive comparison with a string literal.
bool result = zend_string_equals_literal_ci(foo, "FOO"); // true
zend_string_release(foo);
zend_string_release(FOO);
Также есть помощники для конкатенации двух или трёх строк. Если вам нужно соединить больше строк, вместо этого
следует использовать API smart_str, рассматриваемый в следующей главе.
zend_string *foo = zend_string_init("foo", sizeof("foo")-1, 0);
zend_string *bar = zend_string_init("bar", sizeof("bar")-1, 0);
// Creates "foobar"
zend_string *foobar = zend_string_concat2(
ZSTR_VAL(foo), ZSTR_LEN(foo),
ZSTR_VAL(bar), ZSTR_LEN(bar));
// Creates "foo::bar"
zend_string *foo_bar = zend_string_concat3(
ZSTR_VAL(foo), ZSTR_LEN(foo),
"::", sizeof("::")-1,
ZSTR_VAL(bar), ZSTR_LEN(bar));
zend_string_release(foo);
zend_string_release(bar);
zend_string_release(foobar);
zend_string_release(foo_bar);
Как видите, эти API принимают пары char * и длины, а не структуры zend_string. Это позволяет передавать
части конкатенации в виде строковых литералов, не выделяя под них zend_string.
Наконец, API zend_string_tolower() можно использовать для перевода строки в нижний регистр:
zend_string *FOO = zend_string_init("FOO", sizeof("FOO")-1, 0);
zend_string *foo = zend_string_tolower(FOO);
zend_string_release(foo);
zend_string_release(FOO);
Перевод в нижний регистр использует правила ASCII и не зависит от локали. Обычно эту функцию используют для того, чтобы сделать ключи хеш-таблицы регистронезависимыми.
Интернированные строки¶
Здесь всего несколько слов о интернированных строках. Такая концепция может понадобиться вам при разработке расширений. Интернированные строки также взаимодействуют с расширением opcache.
Интернированные строки — это дедуплицированные строки. При использовании с opcache они также переиспользуются от запроса к запросу.
Допустим, вы хотите создать строку “foo”. Обычно для этого просто создают новую строку “foo”:
zend_string *foo;
foo = zend_string_init("foo", strlen("foo"), 0);
/* ... */
Но возникает вопрос: не была ли эта строка уже создана до того, как она вам понадобилась? Когда вам нужна строка, ваш код выполняется в какой-то момент жизни PHP, то есть какой-то код, выполнившийся до вашего, мог уже нуждаться в точно такой же строке (“foo” в нашем примере).
Интернированные строки — это механизм, при котором движок просит проверить хранилище интернированных строк и переиспользовать уже выделенный указатель, если такая строка найдена. Если нет — создаётся новая строка и “интернируется”, то есть становится доступной для других частей исходного кода PHP (других расширений, самого движка и т.д.).
Вот пример:
zend_string *foo;
foo = zend_string_init("foo", strlen("foo"), 0);
foo = zend_new_interned_string(foo);
php_printf("This string is interned : %s", ZSTR_VAL(foo));
zend_string_release(foo);
В приведённом выше коде мы создаём новый zend_string совершенно обычным способом. Затем мы передаём этот
созданный zend_string в zend_new_interned_string(). Эта функция ищет точно такую же строку (“foo” в данном
случае) в буфере интернированных строк движка. Если она находит её (то есть кто-то уже создал такую строку), она
освобождает вашу строку (вероятно, освобождая её память) и заменяет её строкой из буфера интернированных строк. Если
не находит — добавляет её в буфер интернированных строк, делая её доступной для дальнейшего использования или других
частей PHP.
Нужно внимательно относиться к выделению памяти. Интернированные строки всегда имеют счётчик ссылок, равный единице, поскольку им не нужен подсчёт ссылок: они будут общими для буфера интернированных строк и поэтому не могут быть уничтожены за его пределами.
Пример:
zend_string *foo, *foo2;
foo = zend_string_init("foo", strlen("foo"), 0);
foo2 = zend_string_copy(foo); /* increments refcount of foo */
/* foo points to the interned string buffer, and refcount
* in original zend_string falls back to 1 */
foo = zend_new_interned_string(foo);
/* This doesn't do anything, as foo is interned */
zend_string_release(foo);
/* The original buffer referenced by foo2 is released */
zend_string_release(foo2);
/* At the end of the process, PHP will purge its interned
string buffer, and thus free() our "foo" string itself */
Всё дело здесь в сборке мусора.
Когда строка интернируется, её флаги GC изменяются с добавлением флага IS_STR_INTERNED, независимо от того,
какой класс выделения памяти (постоянный или привязанный к запросу) они используют. Этот флаг проверяется, когда вы
хотите скопировать или освободить строку. Если строка интернирована, движок не увеличивает её счётчик ссылок при
копировании строки. Но он также не уменьшает его и не освобождает строку при её освобождении — он просто ничего не
делает. В конце жизненного цикла процесса движок уничтожит свой буфер интернированных строк и тем самым освободит
ваши интернированные строки.
На самом деле этот процесс немного сложнее. Если вы используете интернированную строку вне обработки запроса, эта строка точно будет интернирована. Однако если вы используете интернированную строку в момент, когда PHP обрабатывает запрос, эта строка будет интернирована только для текущего запроса и будет очищена после его завершения. Всё это справедливо, если вы не используете расширение opcache, а не использовать его вы не должны: используйте его.
При использовании расширения opcache, если вы используете интернированную строку вне обработки запроса, эта строка точно будет интернирована и, кроме того, будет общей для каждого процесса или потока PHP, который будет порождён вашим слоем параллелизма. Также, если вы используете интернированную строку в момент обработки PHP запроса, эта строка также будет интернирована самим opcache и станет общей для каждого процесса или потока PHP, порождённого вашим слоем параллелизма.
Механизмы интернированных строк меняются при включении расширения opcache. Opcache не только позволяет интернировать
строки, поступающие из запроса, но и делает их общими для каждого PHP-процесса одного пула. Это достигается с
помощью общей памяти (shared memory). При сохранении интернированной строки opcache также добавляет флаг
IS_STR_PERMANENT в её информацию GC. Этот флаг означает, что выделение памяти для структуры (здесь —
zend_string) постоянно и может представлять собой сегмент общей памяти, доступный только для чтения.
Интернированные строки экономят память, поскольку одна и та же строка никогда не хранится в памяти более одного раза. Но они могут расходовать некоторое процессорное время, так как часто требуется поиск в хранилище интернированных строк, хотя этот процесс и достаточно хорошо оптимизирован. Как разработчику расширений, вам стоит придерживаться следующих общих правил:
Если используется opcache (а он должен использоваться), и если вам нужны строки только для чтения — используйте интернированную строку.
Если вам нужна строка, про которую вы точно знаете, что PHP её интернирует (хорошо известная строка PHP, например “php” или “str_replace”) — используйте интернированную строку.
Если строка доступна не только для чтения и может/должна изменяться после создания — не используйте интернированную строку.
Если строка вряд ли будет переиспользована в будущем — не используйте интернированную строку.
Warning
Никогда не пытайтесь модифицировать (писать в) интернированную строку — скорее всего, вы получите крэш.
Интернированные строки подробно описаны в Zend/zend_string.c