- PVSM.RU - https://www.pvsm.ru -
По результатам опроса в первой статье [1], решено было сделать обзор реализации расширения. К этому моменту в угоду существующим IDE немного изменился синтаксис, который, пожалуй, был наиболее обсуждаемым моментом.
Это не еще-одна-статья-о-hello-world-расширении, т.к. желающим разобраться в основах легко найти массу материалов как на [2] самом [3] Хабре [4], так и в русскоязычном [5] RTFG [6].
Статья о предпосылках, реализации и подводнях камнях. В ней будет мало PHP, в основном C.
Если не интересно читать tl;dr, то можно сразу перейти к Реализации [7].
Декораторы функций (методы не упоминаются здесь и далее, т.к. для декорирования они ничем не отличаются) позволяют изменить работу последних, оборачивая их вызовы дополнительным слоем логики. В декларативной форме описывается список обёрток, которые в итоге должны вернуть нечто, чем будет заменена исходная функция. В python синтаксисе получаем следующее [9]:
@decomaker(argA, argB, ...)
def func(arg1, arg2, ...):
# ...
# Эквивалентно:
func = decomaker(argA, argB, ...)(func)
В PHP такой возможности нет. Сначала я решил взять этот синтаксис как есть и перенести его без изменений (кроме передачи параметров декоратора при вызове; об этом ниже). В первой статье именно такой синтаксис и описывается. Однако IDE с проверкой синтаксиса и один из двух лагерей комментаторов заставили задуматься. В итоге синтаксис сделан более переносимым. Теперь описание декоратора должно описываться в однострочном комментарии #:
Определившись с форматом описания нужно решить, как сами декораторы будут реализовываться. Функция-декоратор должна возвращать функцию, которая замещает собой исходную декорируемую. Тут как нельзя кстати приходятся анонимные функции и замыкания:
<?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'а [10], чтобы не пришлось пересобирать сам PHP (работает — не трожь).
Для выполнения задуманного есть два варианта:
Второй вариант выглядел сомнительным в вопросе совместимости со всякими opcode кешерами и оптимизаторами. Да и первоначальный вариант синтаксиса декораторов (без # комментария) в этом случае бы не работал.
Выбран был первый вариант.
В Zend есть два источника «поступления» исходного кода:
В обоих случаях у нас есть указатели на функции с конкретной реализацией. Стандартные реализации можно найти, посмотрев на инициализацию указателей в zend_startup [15]:
Обе функции принимают на вход в той или иной форме исходный код и выдают массив opcode'ов в виде структурки _zend_op_array [18]. К сожалению, несмотря на схожесть выполняемых задач, реализация у них разная. Так что влиять будем на обе.
Влияние на подобные указатели на функции в Zend и PHP расширениях поставлено на поток. К примеру, та же zend_compile_file подменяется в ZendAccelerator [19] и phar [20]. Это не считая сторонних расширений.
Для подмены, нужно лишь реализовать свой аналог, и подменить указатель, сохранив оригинал. Всё как всегда.
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 [21]. Причем в разных случаях используются разные наборы полей.
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 [22], функция, которая унифицирует все источники поступления исходного кода и выдает на выходе считанный буфер и его размер. Вот вроде бы то, что нам нужно, но повлиять на работу 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 [23], пример использования которого в своих целях можно посмотреть в реализации token_get_all [24] и tokenize [25], вызываемой внутри token_get_all.
zend_lex_state original_lex_state;
zend_save_lexical_state(&original_lex_state TSRMLS_CC);
zend_prepare_string_for_scanning(&source_z, "" TSRMLS_CC)
LANG_SCNG(yy_state) = yycST_IN_SCRIPTING;
В отличие от token_get_all мы парсим уже PHP код, так что наличие открывающего тега нам не обязательно. Соотвественно, начальное состояние у нас не yycINITIAL, а yycST_IN_SCRIPTING.
zval token_zv;
int token_type;
while (token_type = lex_scan(&token_zv TSRMLS_CC)) {
//…
}
token_type — тип лексемы:
token_zv содержит само значение лексемы. Однако, в качестве альтернативы можно использовать поля yy_text и yy_leng структуры zend_lex_state [28], хранящие адрес первого байта текущей лексемы и её длину соотвественно. Доступ к этим полям, как и многое в 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.
zend_restore_lexical_state(&original_lex_state TSRMLS_CC);
Всё, у нас есть лексический разбор исходного кода. Но хотелось бы осветить еще некоторые моменты парсинга.
При ошибках разбора PHP обработчик генерит ошибку или исключение, имя файла и номер строки в тексте которых берутся из состояния _zend_compiler_globals [29]. Имя файла, например, берётся из поля compiled_filename. Которое задается при вызове zend_prepare_string_for_scanning(). А используется внутри zend_error [30] (используемая для генерации всякие 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.
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 [31] есть реализация 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) и возвращается адрес '@'.
При обработке декораторов немного меняется исходный код декорируемой функции: тело функции заворачивается в анонимную функцию, которая передается параметром ближайшему декоратору. Т.е. для кода
// 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)) {…
Ну вот и всё. Если вы и правда до сюда дочитали, то, надеюсь, было интересно.
Приведу и в этой статье ссылку на github [32].
Автор: AterCattus
Источник [33]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/php-2/35288
Ссылки в тексте:
[1] первой статье: http://habrahabr.ru/post/180827/
[2] на: http://habrahabr.ru/post/98862/
[3] самом: http://habrahabr.ru/post/125597/
[4] Хабре: http://habrahabr.ru/post/144582/
[5] русскоязычном: http://highloadblog.ru/articles/10.html
[6] RTFG: http://adobkin.com/blog/categories/development-extensions-to-php/
[7] Реализации: #code
[8] моё расширение: https://github.com/AterCattus/unifiedphp/blob/master/README_ru.md
[9] следующее: http://www.python.org/dev/peps/pep-0318/
[10] переписывания лексера Zend'а: http://habrahabr.ru/post/179441/
[11] zend_eval_string[l][_ex]: http://lxr.php.net/xref/PHP_5_5/Zend/zend_execute.h#66
[12] zend_compile_string: http://lxr.php.net/xref/PHP_5_5/Zend/zend_compile.c#101
[13] zend_stream_type: http://lxr.php.net/xref/PHP_5_5/Zend/zend_stream.h#zend_stream_type
[14] zend_compile_file: http://lxr.php.net/xref/PHP_5_5/Zend/zend_compile.c#zend_compile_file
[15] zend_startup: http://lxr.php.net/xref/PHP_5_5/Zend/zend.c#zend_startup
[16] compile_file: http://lxr.php.net/xref/PHP_5_5/Zend/zend_language_scanner.c#compile_file
[17] compile_string: http://lxr.php.net/xref/PHP_5_5/Zend/zend_language_scanner.c#compile_string
[18] _zend_op_array: http://lxr.php.net/xref/PHP_5_5/Zend/zend_compile.h#_zend_op_array
[19] ZendAccelerator: http://lxr.php.net/xref/PHP_5_5/ext/opcache/ZendAccelerator.c#2549
[20] phar: http://lxr.php.net/xref/PHP_5_5/ext/phar/phar.c#3533
[21] zend_file_handle: http://lxr.php.net/xref/PHP_5_5/Zend/zend_stream.h#zend_file_handle
[22] zend_stream_fixup: http://lxr.php.net/xref/PHP_5_5/Zend/zend_stream.c#zend_stream_fixup
[23] lex_scan: http://lxr.php.net/xref/PHP_5_5/Zend/zend_compile.h#430
[24] token_get_all: http://lxr.php.net/xref/PHP_5_5/ext/tokenizer/tokenizer.c#180
[25] tokenize: http://lxr.php.net/xref/PHP_5_5/ext/tokenizer/tokenizer.c#tokenize
[26] тут: http://lxr.php.net/xref/PHP_5_5/Zend/zend_language_scanner_defs.h
[27] T_*: http://lxr.php.net/xref/PHP_5_5/Zend/zend_language_parser.y#57
[28] zend_lex_state: http://lxr.php.net/xref/PHP_5_5/Zend/zend_language_scanner.h#zend_lex_state
[29] _zend_compiler_globals: http://lxr.php.net/xref/PHP_5_5/Zend/zend_globals.h#_zend_compiler_globals
[30] zend_error: http://lxr.php.net/xref/PHP_5_5/Zend/zend.c#zend_error
[31] /PHP/ext/standard/php_smart_str.h: http://lxr.php.net/xref/PHP_5_5/ext/standard/php_smart_str.h
[32] github: https://github.com/AterCattus/php-decorators
[33] Источник: http://habrahabr.ru/post/180939/
Нажмите здесь для печати.