Декораторы в PHP. Реализация расширения

от автора

По результатам опроса в первой статье, решено было сделать обзор реализации расширения. К этому моменту в угоду существующим IDE немного изменился синтаксис, который, пожалуй, был наиболее обсуждаемым моментом.
Это не еще-одна-статья-о-hello-world-расширении, т.к. желающим разобраться в основах легко найти массу материалов как на самом Хабре, так и в русскоязычном RTFG.
Статья о предпосылках, реализации и подводнях камнях. В ней будет мало PHP, в основном C.

Предпосылки

Если не интересно читать tl;dr, то можно сразу перейти к Реализации.

Начну издалека

Мне нравится Python, особенно некоторые моменты его синтаксиса. А так как последнее время пишу я в основном на PHP, то хочется, чтобы рабочий инструмент был удобнее и функциональнее. PHP в последних версиях неплохо так развивается, но тех же декораторов всё нет. Так что приходится брать всё в свои руки. Сначала была идея унификации в плане имён и передачи параметров core функций (другое моё расширение, которое пока в стадии формирования идеи. Если нужно, могу и про его создание написать), теперь вот декораторы и ещё кое-какие наработки.

Декораторы функций (методы не упоминаются здесь и далее, т.к. для декорирования они ничем не отличаются) позволяют изменить работу последних, оборачивая их вызовы дополнительным слоем логики. В декларативной форме описывается список обёрток, которые в итоге должны вернуть нечто, чем будет заменена исходная функция. В python синтаксисе получаем следующее:

@decomaker(argA, argB, ...) def func(arg1, arg2, ...):     # ...  # Эквивалентно: func = decomaker(argA, argB, ...)(func) 

В PHP такой возможности нет. Сначала я решил взять этот синтаксис как есть и перенести его без изменений (кроме передачи параметров декоратора при вызове; об этом ниже). В первой статье именно такой синтаксис и описывается. Однако IDE с проверкой синтаксиса и один из двух лагерей комментаторов заставили задуматься. В итоге синтаксис сделан более переносимым. Теперь описание декоратора должно описываться в однострочном комментарии #:

  • # частично является deprecated, так что его применение будет заметнее обычных //, /* и /**;
  • Однострочный комментарий не сворачивается и его тяжелее упустить из виду;
  • Вдруг в php 5.x декораторы-таки появятся и необходимость в этом расширении со странным синтаксисом отпадет.

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

немного PHP кода

<?php function decor($func) {     echo "Decorator!\n";     return function () use($func) {         return call_user_func_array($func, func_get_args());     }; }  function a($v) {     var_dump($v); }  $b = decor('a');  $b(42);  /* Вывод:  Decorator! int(42) */ 

В PHP опосредованный вызов функции с передачей ей параметров, конечно, многословен, этого не отнять.

В итоге получается синтаксис с изображения в начале статьи:

<?php function decor($func) {     return function(){} }  # @decor function original() {     // ... } 

И вот это хочется получить без переписывания лексера Zend’а, чтобы не пришлось пересобирать сам PHP (работает — не трожь).

Реализация

Для выполнения задуманного есть два варианта:

  • Менять исходный код до того, как он попадет к лексеру PHP;
  • Менять уже готовый opcode, но тогда синтаксис должен быть совместим с существующим.

Второй вариант выглядел сомнительным в вопросе совместимости со всякими opcode кешерами и оптимизаторами. Да и первоначальный вариант синтаксиса декораторов (без # комментария) в этом случае бы не работал.
Выбран был первый вариант.
В Zend есть два источника «поступления» исходного кода:

  • Просто строка с кодом (аналог eval): zend_eval_string[l][_ex], которые все в итоге сводятся к вызову zend_compile_string;
  • Файл, полученный из разных источников (stdin, stream, etc. Все варианты указаны в перечислении zend_stream_type). В этом случае нас интересует zend_compile_file.

В обоих случаях у нас есть указатели на функции с конкретной реализацией. Стандартные реализации можно найти, посмотрев на инициализацию указателей в zend_startup:

  • zend_compile_file реализуется в compile_file;
  • zend_compile_string — в столь же очевидной compile_string.

Обе функции принимают на вход в той или иной форме исходный код и выдают массив opcode’ов в виде структурки _zend_op_array. К сожалению, несмотря на схожесть выполняемых задач, реализация у них разная. Так что влиять будем на обе.

Влияние на подобные указатели на функции в Zend и PHP расширениях поставлено на поток. К примеру, та же zend_compile_file подменяется в ZendAccelerator и phar. Это не считая сторонних расширений.

Для подмены, нужно лишь реализовать свой аналог, и подменить указатель, сохранив оригинал. Всё как всегда.

Получится примерно следующее

PHP_MINIT_FUNCTION(decorators); PHP_MSHUTDOWN_FUNCTION(decorators);  zend_module_entry decorators_module_entry = {     // ...     decorators_functions,     PHP_MINIT(decorators),     PHP_MSHUTDOWN(decorators),     // ... };  zend_op_array *(*decorators_orig_zend_compile_string)(zval *source_string, char *filename TSRMLS_DC); zend_op_array *(*decorators_orig_zend_compile_file)(zend_file_handle *file_handle, int type TSRMLS_DC);  zend_op_array* decorators_zend_compile_string(zval *source_string, char *filename TSRMLS_DC); zend_op_array* decorators_zend_compile_file(zend_file_handle *file_handle, int type TSRMLS_DC);  /* {{{ PHP_MINIT_FUNCTION  */ PHP_MINIT_FUNCTION(decorators) {     decorators_orig_zend_compile_string = zend_compile_string;     zend_compile_string                 = decorators_zend_compile_string;      decorators_orig_zend_compile_file = zend_compile_file;     zend_compile_file                 = decorators_zend_compile_file;      return SUCCESS; } /* }}} */  /* {{{ PHP_MSHUTDOWN_FUNCTION  */ PHP_MSHUTDOWN_FUNCTION(decorators) {     zend_compile_string = decorators_orig_zend_compile_string;      zend_compile_file = decorators_orig_zend_compile_file;      return SUCCESS; } /* }}} */  zend_op_array* decorators_zend_compile_string(zval *source_string, char *filename TSRMLS_DC) /* {{{ */ {     return decorators_orig_zend_compile_string(source_string, filename TSRMLS_CC); } /* }}} */  zend_op_array* decorators_zend_compile_file(zend_file_handle *file_handle, int type TSRMLS_DC) /* {{{ */ {     return decorators_orig_zend_compile_file(file_handle, type TSRMLS_CC); } /* }}} */  

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

И если с compile_string все более-менее ясно (на вход приходит исходная строка), то вот с compile_file не все так радужно — исходника-то у нас нет, только описание источника в zend_file_handle. Причем в разных случаях используются разные наборы полей.

Непосредственное чтение исходника закопано довольно далеко

ZEND_API zend_op_array *compile_file(zend_file_handle *file_handle, int type TSRMLS_DC) {     // ...     open_file_for_scanning(file_handle TSRMLS_CC)     // ... }  ZEND_API int open_file_for_scanning(zend_file_handle *file_handle TSRMLS_DC) {     // ...     zend_stream_fixup(file_handle, &buf, &size TSRMLS_CC)     // ... } 

И самое интересное тут для нас — zend_stream_fixup, функция, которая унифицирует все источники поступления исходного кода и выдает на выходе считанный буфер и его размер. Вот вроде бы то, что нам нужно, но повлиять на работу zend_stream_fixup и open_file_for_scanning мы не можем, у нас есть контроль только над compile_file. Кто-то пошел копипастить к себе эти функции и все их зависимости, но мы сделаем проще. Если посмотреть на исходник zend_stream_fixup, то видно, что все типы сводятся в итоге к единому ZEND_HANDLE_MAPPED, у которого в file_handle->handle.stream.mmap.buf и file_handle->handle.stream.mmap.len содержится исходный код и его длина соответственно. Причем, если в file_handle уже указан этот тип данных, то практически ничего уже менять не надо и всё выдается как есть.
Выходит, если мы передадим в compile_file() zend_file_handle *file_handle уже в формате ZEND_HANDLE_MAPPED с корректным значением всех полей, то compile_file это примет как так и было. А сделать это мы может вызвав zend_stream_fixup (которая является функцией Zend API, а не подменяемым указателем) еще разок до вызова compile_file. Тогда повторный вызов внутри open_file_for_scanning просто ничего не изменит.

Пробуем

zend_op_array* decorators_zend_compile_file(zend_file_handle *file_handle, int type TSRMLS_DC) /* {{{ */ {     char *buf;     size_t size;      if (zend_stream_fixup(file_handle, &buf, &size TSRMLS_CC) == FAILURE) {         return NULL;     }     // теперь в file_handle у нас гарантированно ZEND_HANDLE_MAPPED      return decorators_orig_zend_compile_file(file_handle, type TSRMLS_CC); } /* }}} */ 

Ура, работает. Более того, у нас в file_handle->handle.stream.mmap.buf/len содержится исходник, откуда бы PHP его не взял: stdin, fd, include http stream… Осталось положить туда наш измененный вариант кода и вызвать оригинальную zend_compile_file.

Как работает decorators_preprocessor() писать не буду: очевидное получение строки, ее передача препроцессору и отдача результирующей строки. Ниже и так будут куски кода из этой функции.

Осталось рассмотреть сам препроцессор.

Передача разрозненных исходных данных в единую функцию

void preprocessor(zval *source_zv, zval *return_value TSRMLS_DC) {     // Из исходной строки source_zv формирую строку в return_value }  /* {{{ DECORS_CALL_PREPROCESS */ #define DECORS_CALL_PREPROCESS(result_zv, buf, len)   \     do {                                              \         zval *source_zv;                              \         ALLOC_INIT_ZVAL(result_zv);                   \         ALLOC_INIT_ZVAL(source_zv);                   \         ZVAL_STRINGL(source_zv, (buf), (len), 1);     \         preprocessor(source_zv, result_zv TSRMLS_CC); \         zval_dtor(source_zv);                         \         FREE_ZVAL(source_zv);                         \     } while (0);                                      \ /* }}} */  /* {{{ proto string decorators_preprocessor(string $code) */ PHP_FUNCTION(decorators_preprocessor) {     char *source;     int source_len;     zval *result;      if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "s", &source, &source_len) == FAILURE) {         return;     }     DECORS_CALL_PREPROCESS(result, source, source_len);     // ... } /* }}} */  zend_op_array* decorators_zend_compile_string(zval *source_string, char *filename TSRMLS_DC) /* {{{ */ {     zval *result;     DECORS_CALL_PREPROCESS(result, Z_STRVAL_P(source_string), Z_STRLEN_P(source_string));     // ... } /* }}} */  zend_op_array* decorators_zend_compile_file(zend_file_handle *file_handle, int type TSRMLS_DC) /* {{{ */ {     // ...     zval *result;     DECORS_CALL_PREPROCESS(result, file_handle->handle.stream.mmap.buf, file_handle->handle.stream.mmap.len);     // ... } /* }}} */ 

Найти и переделать!

Задача препроцессора — найти описания декораторов и модифицировать код функций, на которые декораторы влияют. А для этого лучше всего работать с токенами исходного текста. Чтобы не изобретать велосипед, был использован родной Zend’овский лексический сканер lex_scan, пример использования которого в своих целях можно посмотреть в реализации token_get_all и tokenize, вызываемой внутри token_get_all.

  1. Сохраняем текущее окружение сканера, в котором работает наш код:

    zend_lex_state original_lex_state;
    zend_save_lexical_state(&original_lex_state TSRMLS_CC);

  2. Подготавливаем исходную строку к парсингу:

    zend_prepare_string_for_scanning(&source_z, "" TSRMLS_CC)

  3. Задаем начальное состояние лексера (все варианты тут):

    LANG_SCNG(yy_state) = yycST_IN_SCRIPTING;

    В отличие от token_get_all мы парсим уже PHP код, так что наличие открывающего тега нам не обязательно. Соотвественно, начальное состояние у нас не yycINITIAL, а yycST_IN_SCRIPTING.

  4. В цикле получаем все лексемы исходной строки:

    zval token_zv;
    int token_type;
    while (token_type = lex_scan(&token_zv TSRMLS_CC)) {
    //…
    }

    token_type — тип лексемы:

    • <256 — код символа односимвольной лексемы;
    • >= 256 — значение константы T_*. Строковое описание по token_type можно получить через PHP_FUNCTION(token_name)/get_token_type_name.

    token_zv содержит само значение лексемы. Однако, в качестве альтернативы можно использовать поля yy_text и yy_leng структуры zend_lex_state, хранящие адрес первого байта текущей лексемы и её длину соотвественно. Доступ к этим полям, как и многое в Zend, реализуется через соответствующие макросы:

    #define zendtext LANG_SCNG(yy_text)
    #define zendleng LANG_SCNG(yy_leng)

    Теперь пользуемся char* zendtext и unsigned int zendleng.

    Чтобы не было memory leak’ов нужно учитывать, что значение token_zv иногда берется как есть из исходного буфера, а иногда под него выделяется память. Которую нужно освобождать. Интересующиеся могут посмотреть код lex_scan(), а сейчас просто возьмем необходимый кусок логики из token_get_all.

  5. Восстанавливаем окружение сканера, в котором работает наш код:

    zend_restore_lexical_state(&original_lex_state TSRMLS_CC);

Всё, у нас есть лексический разбор исходного кода. Но хотелось бы осветить еще некоторые моменты парсинга.

При ошибках разбора PHP обработчик генерит ошибку или исключение, имя файла и номер строки в тексте которых берутся из состояния _zend_compiler_globals. Имя файла, например, берётся из поля compiled_filename. Которое задается при вызове zend_prepare_string_for_scanning(). А используется внутри zend_error (используемая для генерации всякие E_* ошибок; она же используется и в этом расширении для генерации E_PARSE). Но compiled_filename в zend_error() используется только если Zend находится в состоянии компилирования (zend_bool in_compilation; всё в том же _zend_compiler_globals). Который сам по себе не активируется, если мы парсим исходник.
Так что перед парсингом мы переключаемся на «компилирование»:

zend_bool original_in_compilation = CG(in_compilation);
CG(in_compilation) = 1;

А по окончанию возвращаем всё обратно:

CG(in_compilation) = original_in_compilation;

Теперь, если мы передадим в zend_prepare_string_for_scanning корректный filename, то возможные ошибки будут гораздо информативней. Получить текущее имя файла можно через zend_get_compiled_filename(), который, правда, может вернуть NULL, от которого php (если NULL передать в zend_prepare_string_for_scanning) упадет в segfault.

Осталось задать корректное имя файла в decorators_preprocessor и decorators_zend_compile_file

PHP_FUNCTION(decorators_preprocessor) {     // ...     char *prev_filename = zend_get_compiled_filename(TSRMLS_CC) ? zend_get_compiled_filename(TSRMLS_CC) : "";     zend_set_compiled_filename("-" TSRMLS_CC);      DECORS_CALL_PREPROCESS(result, source, source_len);      zend_set_compiled_filename(prev_filename TSRMLS_CC);     // ... }  zend_op_array* decorators_zend_compile_file(zend_file_handle *file_handle, int type TSRMLS_DC) /* {{{ */ {     // ...     char *prev_filename = zend_get_compiled_filename(TSRMLS_CC) ? zend_get_compiled_filename(TSRMLS_CC) : "";     const char* filename = (file_handle->opened_path) ? file_handle->opened_path : file_handle->filename;     zend_set_compiled_filename(filename TSRMLS_CC);      zval *result;     DECORS_CALL_PREPROCESS(result, file_handle->handle.stream.mmap.buf, file_handle->handle.stream.mmap.len);      zend_set_compiled_filename(prev_filename TSRMLS_CC);     // ... } 

В decorators_zend_compile_string имя файла у нас и так известно.

Модификация исходного кода

Получив всё, что нужно для препроцессинга, осталось его собственно и произвести. Задача в переводе текста, составленного из кусков (лексем) в итоговый текст могла бы быть не так проста в C из-за активной работы со склейкой строк. Однако, в /PHP/ext/standard/php_smart_str.h есть реализация smart строк, которые нам очень пригодятся.

Коротко

    smart_str str = {0};     smart_str str2 = {0};     smart_str_appendc(&str, '!');     smart_str_appendl(&str, "hello", 5);     smart_str_append(&str, &str2);     smart_str_append_long(&str, 42);     // и т.д.     // результирующая строка длиной size_t str.len может быть получена через char* str.c     // освобождение памяти:     smart_str_free(&result); 

В цикле разбора лексем клеим результирующую строку из лексем (zendtext, zendleng), где нужно меняя/добавляя от себя. Непосредственно алгоритм замены декораторов, имхо, не столь интересен. Из потенциально интересного — проверка, что лексема типа T_COMMENT похожа на описание декоратора: идет проверка регулярки ‘^#[ \t]*@’ (простым циклом, без regexp) и возвращается адрес ‘@’.

Немного PHP напоследок

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

// comment @a(A) @b @c(C, D) /**  * yet another comment  */ function x(X) {     Y } 

в результате препроцессинга получится следующий код:

// comment    /**  * yet another comment  */ function x(X) { return call_user_func_array(a(b(c(function(X) {     Y }, C, D)), A), func_get_args());} 

Под A, C, D, X подразумевается произвольный код, который копируется as is.
Из этого вытекают следующие последствия:

  • Если декорируемая функция объявлена с параметрами, имеющими значение по умолчанию, то в анонимной функции всё будет также:

    function foo($a, $b=42, $c=array(100, 500))
    {…
    =>
    function foo($a, $b=42, $c=array(100, 500))
    { return call_user_func_array(…(function($a, $b=42, $c=array(100, 500)) {…

  • При ошибках в описании имен декораторов, строкой кода при описании ошибки будет строка с открывающей тело функции ‘{‘. Аналогично с параметрами декораторов — они будут на строке с закрывающей тело функции ‘}’;
  • Параметрам декораторов можно задавать имена переменных декорируемой функции. Получается read/write контроль над параметрами;
  • Т.к. для передачи параметров используется func_get_args(), то передача параметра по ссылке в декорируемую функцию в данный момент не работает.

Ну вот и всё. Если вы и правда до сюда дочитали, то, надеюсь, было интересно.

Приведу и в этой статье ссылку на github.

ссылка на оригинал статьи http://habrahabr.ru/post/180939/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *