serge_gorshkov


Сергей Горшков - о бизнесе в сфере ИТ

о семантической интеграции, программировании, управлении...


Previous Entry Share Next Entry
Гик-пост для тех, кто скучает по <?php asm mov ax,bx ?>
serge_gorshkov
Близится замечательная конференция DUMP (Екатеринбург, 25 мая), на которой я буду выступать с двумя докладами. Один доклад технический, второй - из управленческой практики. Публикую здесь их содержание, чтобы была возможность обсудить. Начнем с технического.

Расширяем PHP

Доклад для тех, кто скучает по <?php asm mov ax,bx; ?>

Программисты нечасто задумываются о создании собственных расширений для PHP – на это нужны веские причины, которые не так уж часто возникают в жизни. Я расскажу о том, как мы пришли к необходимости написания собственного расширения, о проблемах, которые возникли и были решены на этом пути, и о том, насколько целесообразным оказалось в конце концов создание расширения.



Начнем с задачи. Наша компания в течение многих лет использует собственный движок на PHP+MySQL+JavaScript, на котором создаются как сайты, так и корпоративные приложения (CRM). Движок приобрел окончательную форму в 2006 году. В нем используется собственный синтаксис шаблонов, достаточно лаконичный и понятный, к которому все мы очень привыкли. Например, чтобы сформировать список, в шаблоне необходима следующая конструкция:

<ul>
  {:list}
    <li> {!item} </li>
  {:/list}
</ul> 

Данные для шаблона подаются в массиве (или в PHP-объекте), примерно таким образом:

Array( “list” => Array (
                                  Array ( “item” => первый пункт ),
                                  Array ( “item” => ”второй пункт” )));

Мы не будем сейчас обсуждать достоинства и недостатки такого шаблонизатора; скажу только, что в течение многих лет он нам верно служил. Для слияния шаблонов и данных в движке использовались регулярные выражения с eval’ом, примерно такого вида:

$result = preg_replace(”/
              \{([\?~:])([a-zA-Z0-9_]*)(([<>!=]{1,2})
              ([a-zA-Z0-9_]*)){0,1}\}(.*)\{\\1\/\\2\}
              /iseU”,
    ”ProcessBlock(‘$0′, ‘$1′, ‘$2′, ‘$4′, ‘$5′, ‘$6′, \$vars_arr) ”,
    $result);


Особую прелесть движку придавало то, что шаблонизатор в нем – управляющий: то есть, включением определенного фрагмента в шаблон можно инициировать вызов метода из PHP, который подготовит данные для слияния с этим фрагментом.

За пять лет на этом движке было создано несколько сотен проектов. В определенный момент мы обнаружили, что в ряде случаев шаблонизатор потребляет серьезные ресурсы веб-сервера (процессор, память).

Первой мыслью на тему того, как оптимизировать движок, не меняя его структуры, и сохраняя поддержку всего ранее созданного кода, было создание компилирующего шаблонизатора (по этому пути идет smarty). То есть, шаблон можно трансформировать в некий файл, где в HTML включаются управляющие конструкции PHP, соответствующие управляющим конструкциям шаблона. Затем остается подать на вход этому скрипту массив с данными, и PHP выполнит свою исконную задачу… Реализовать такой вариант не получилось по ряду причин, основная из которых указана выше – шаблонизатор является управляющим, шаблоны могут каскадироваться.

Тогда нам и пришла мысль написать на C++ собственное расширение на PHP, для того, чтобы избавиться от парсинга шаблонов при помощи регулярных выражений. Дело это оказалось не таким уж сложным. Опишу три основных проблемы, с которыми пришлось столкнуться.

1. Первая сложность состояла в том, как в недрах PHP представлены ассоциативные массивы. Именно из их недр нам предстояло получать данные. Главные понятия, необходимые для понимания этого процесса – HashTable и ZVAL. Они неплохо описаны в документации. Данные, хранящиеся в ассоциативных массивах в PHP, становятся в C++ объектами HashTable. Для работы с ними предназначен специальный набор функций, надо сказать - не очень удобных. А в нашем движке данные хранятся в многоэтажных массивах, потенциально – с неограниченной вложенностью, поэтому использовать эти функции для работы с ними становится совсем неудобно… Решение состоит в том, чтобы на один проход развернуть всю иерархию массивов в два обычных массива, один из которых содержит ключи, другой – значения. Ключи для многоэтажных массивов при этом собираются в одну строку. То есть, если в PHP написано:

$arr = Array ( “first” => 1,
                       “second” => Array (
                                           Array ( “number” => 2 )));

В расширении это превратится в следующую пару массивов (в условной нотации):

Keys    = [ "first ", "second0number" ];
Values = [ "1",      "2" ];

Пробежать по такому массиву и найти нужный ключ с любого «этажа» иерархии – одно удовольствие.

Вторая сложность состояла в том, как вызвать функцию, содержащуюся в PHP-скрипте, из расширения. Если с вызовом функций расширения из PHP все понятно, то обратный процесс не столь очевиден. Процитирую код, позволяющий это сделать.

zval *function, *itemname, *retval;
zval **params[1];
MAKE_STD_ZVAL ( function );
MAKE_STD_ZVAL ( itemname );
ZVAL_STRING ( function, MyPHPFunction, 1);
ZVAL_STRING ( itemname, значение, 1);
params[0] = &itemname;
if ( call_user_function_ex ( CG (function_table), NULL, function, &retval, 1, params, 0,
NULL TSRMLS_CC ) == FAILURE )
zend_error (E_ERROR, Function call failed);
else {
if ( Z_TYPE_P (retval) == IS_STRING ) {
… тут мы можем обработать возвращенное значение, используя макросы
Z_STRLEN_P ( retval ) и Z_STRVAL_P ( retval )

Этот код компилируется и работает. До тех пор, пока мы не вынесем его из функции, напрямую вызываемой из PHP, то есть объявленной таким образом:

ZEND_FUNCTION(MyCPPFunction) { … }

в любую другую (пусть называется innerCPPFunction), которую, предположим, вызовет эта самая MyCPPFunction. После этого компилятор начнет ругаться, и тут уже не поможет никакая документация. Для решения проблемы пришлось покопаться в исходниках PHP.

Оказывается, function_table - это параметр, передаваемый в ZEND_FUNCTION.

За макросом TSRMLS_CC скрывается переменная tsrm_ls, тоже являющаяся одним из стандартных параметров для функций, объявленных при помощи ZEND_FUNCTION.
Остается передать эти параметры в нашу innerCPPFunction:
char *result = innerCPPFunction ( CG ( function_table ), tsrm_ls );
char *innerCPPFunction ( HashTable *function_table, void ***tsrm_ls ) { ... }

Здесь innerCPPFunction - это функция, куда мы захотели вынести вызов функции PHP.

Третьи по счету грабельки, на которые пришлось наступить, состоят в том, что глобальные переменные, объявленные в расширении, охраняют свои значения между вызовами различных функций из PHP. Так что приходится специально о них заботиться. Особенно учитывая, что схема парсинга выглядит так: сначала из PHP вызывается функция расширения, которая сливает данные с шаблоном верхнего уровня, а затем уже она вызывает функции из PHP, которые инициализируют содержащиеся в странице программные модули. Для каждого из них потом снова вызывается функция из расширения, которая сливает данные модуля с его шаблоном. То есть, выполнение переходит из PHP в расширение, затем обратно в PHP, затем снова в расширение. При втором вызове как раз важно не забыть о том, что глобальные переменные первым вызовом уже проинициализированы.

Ура, расширение работает! Посмотрим, что это нам дает в плане производительности.

По результатам многочисленных измерений, скорость парсинга возросла ровно в два раза. Хорошо? Неплохо, но могло бы быть и лучше. Конечно, потенциал для оптимизации быстродействия в расширении еще есть.

Как бы то ни было, приступая к созданию нового движка, который должен заменить устаревшее решение, мы вернулись к вопросу выбора шаблонизатора. Для этого мы решили сравнить производительность уже четырех методов шаблонизации на некотором довольно сложном шаблоне, содержащем три вложенных цикла. Прогоняя этот шаблон с одним и тем же массивом данных, мы получили следующие результаты по производительности (показана относительная скорость, парсинг при помощи регулярных выражений принят за 100%):

Парсинг при помощи регулярных выражений

100%

Парсинг при помощи расширения PHP

53%

XML\XSLT

36%

«чистый» PHP (компилирующий шаблонизатор)

3%

Конечно, «чистый» PHP побеждает с чудовищным отрывом. Но проблема создания компилирующего шаблонизатора для движка с управляющим синтаксисом шаблона все еще ждет своего героя… Поэтому в итоге было принято решение использовать в новой версии нашего продукта шаблонизацию XSLT: дополнительный плюс состоит в том, что этот язык является стандартом, с которым некоторые разработчики уже знакомы, а не собственным «эксклюзивным» решением.

А расширение PHP, реализующее парсер «старого» синтаксиса, помогло нам улучшить производительность ранее написанных программных систем, уже работающих у клиентов. Кроме того, в процессе работы над парсером мы получили бесценный практический опыт «низкоуровневого» программирования для PHP. И теперь, если нам захочется написать часть скрипта на ассемблере – мы сможем это сделать!



  • 1

Компилируемый шаблонизатор

Привет.
Я не совсем понимаю проблему с компиляцией шаблона.
Что мешает использовать include() для вложенных шаблонов?

Re: Компилируемый шаблонизатор

Теоретически - ничего не мешает. На практике - никто из тех, кто за это брался, не смог довести разработку до конца.
Суть такая. Процесс парсинга управляется самим шаблоном. Сначала парсится шаблон страницы, в него вставлены специальными тегами ссылки на программные модули. Когда парсер встречает такой тег, он вызывает программный модуль (который представляет собой класс PHP), чтобы тот подготовил данные. Получается, в это место скомпилированного шаблона надо вставить не просто include, а еще создание экземпляра класса. Подготовленные им данные сливаются с собственным шаблоном модуля. В шаблоне модуля, в свою очередь, могут встречаться вызовы так называемых интерфейсов - неких часто употребляемых шаблонов (например, форма прикрепления файлов). Для подготовки данных, которые будут слиты с шаблоном интерфейса, нужно вызвать одноименный метод из класса, соответствующего программному модулю. То есть, опять же, в соответствующую точку скомпилированного шаблона надо вставить вызов этого метода, чтобы подготовить данные, а потом уже инклудить шаблон интерфейса.
Короче говоря, получается трехэтажная схема парсинга с рекурсией.
Написание компилирующего шаблонизатора становится довольно муторным.

Re: Компилируемый шаблонизатор

Насколько я помню, есть два вида тегов: вызов метода и вставка интерфейса.

При вызове метода, не надо в шаблон ничего вставлять, кроме того, что там уже есть. Основная задача - вызвать метод и слить полученные данные. Проблема - сделать замыкание на метод, так как переменные должны использоваться именно те, которые вернул метод.

При вызове интерфейса нужно использовать include, так как там нужно получить именно шаблон. Основная проблема, которая мне видится, - передать туда необходимый контекст с переменными (то есть опять же замыкание).
С другой же стороны можно выдернуть шаблон и вставить прямо сюда без include-ов.

Если использовать php 5.3, то с замыканиями проблем не должно быть. На php 5.2 наверно придется имитировать это. Наскидку приходит идея присваивать идентификаторы уровням и использовать контекст текущего уровня, как-нибудь так $context[$currentId]['variable_name'].

Но это так рассуждения. Вполне возможно там есть подводные грабли, о которые все спотыкаются.
Но задачка интересная и полезная, можно ее давать на тестовое задание, хотябы попытки посмотреть :)

Re: Компилируемый шаблонизатор

Вот, да кстати, спасибо что напомнил - контексты с переменными были одной из проблем.
Щас почитал, вспомнил, и аж захотелось написать этот шаблонизатор :))

  • 1
?

Log in

No account? Create an account