Статьи

Повторная реализация оператора диапазона в PHP

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

В этой статье предполагается, что читатель может построить PHP из исходного кода. Если это не так, пожалуйста, сначала ознакомьтесь с главой « Сборка PHP» в книге PHP Internals .

Летающий слон


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

Еще раз спасибо Никите Попову за корректуру этой статьи!

Недостатки предыдущей реализации

Первоначальная реализация помещала всю логику для оператора диапазона в виртуальную ZEND_RANGE Zend, что заставляло вычисления выполняться только во время выполнения, когда был выполнен код операции ZEND_RANGE . Это не только означало, что вычисления не могли быть перенесены на время компиляции для операндов, которые были буквальными, но также означало, что некоторые функции просто не будут работать.

В этой реализации мы сместим логику оператора диапазона из Zend VM, чтобы сделать возможным выполнение вычислений либо во время компиляции (для литеральных операндов), либо во время выполнения (для динамических операндов). Это не только обеспечит небольшой выигрыш для пользователей Opcache, но, что более важно, позволит использовать функции с постоянными выражениями с оператором диапазона.

Например:

 // as constant definitions const AN_ARRAY = 1 | > 100 ; // as initial property definitions class A { private $a = 1 | > 2 ; } // as default values for optional parameters: function a ( $a = 1 | > 2 ) {  // } 

Так что без дальнейших церемоний, давайте переопределим оператор диапазона.

Обновление Лексера

Реализация лексера остается точно такой же. Маркер сначала регистрируется в Zend / zend_language_scanner.l (строка ~ 1200):

 < ST_IN_SCRIPTING > "|>" { RETURN_TOKEN ( T_RANGE ) ; } 

И затем объявлено в Zend / zend_language_parser.y (строка ~ 220):

 % token T_RANGE "|> (T_RANGE)" 

Расширение токенизатора должно быть заново сгенерировано, перейдя в каталог ext / tokenizer и выполнив файл tokenizer_data_gen.sh .

Обновление парсера

Реализация парсера частично такая же, как и раньше. Мы снова начнем с определения приоритета и ассоциативности оператора, добавив токен T_RANGE в конец следующей строки (строка ~ 70):

 % nonassoc T_IS_EQUAL T_IS_NOT_EQUAL T_IS_IDENTICAL T_IS_NOT_IDENTICAL T_SPACESHIP T_RANGE 

Затем мы снова обновляем производственное правило expr_without_variable , хотя на этот раз семантическое действие (код в фигурных скобках) будет немного другим. Обновите его следующим кодом (я поместил его чуть ниже правила T_SPACESHIP , строка ~ 930):

 | expr T_RANGE expr { $$ = zend_ast_create_binary_op ( ZEND_RANGE , $ 1 , $ 3 ) ; } 

На этот раз мы использовали функцию zend_ast_create_binary_op (вместо функции zend_ast_create ), которая создает для нас узел ZEND_AST_BINARY_OP . zend_ast_create_binary_op принимает имя кода операции, которое будет использоваться для отличия двоичных операций друг от друга на этапе компиляции.

Поскольку сейчас мы повторно используем ZEND_AST_BINARY_OP узла ZEND_AST_BINARY_OP , нет необходимости определять новый ZEND_AST_RANGE узла ZEND_AST_RANGE как это делалось ранее в файле Zend / zend_ast.h .

Обновление этапа компиляции

На этот раз нет необходимости обновлять файл Zend / zend_compile.c, поскольку он уже содержит необходимую логику для обработки двоичных операций. Таким образом, мы просто повторно используем эту логику, превращая наш оператор в узел ZEND_AST_BINARY_OP .

Ниже приведена усеченная версия функции zend_compile_binary_op :

 void zend_compile_binary_op ( znode * result , zend_ast * ast ) /* {{{ */ { zend_ast * left_ast = ast - > child [ 0 ] ; zend_ast * right_ast = ast - > child [ 1 ] ; uint32_t opcode = ast - > attr ; znode left_node , right_node ; zend_compile_expr ( & left_node , left_ast ) ; zend_compile_expr ( & right_node , right_ast ) ; if ( left_node . op_type == IS_CONST && right_node . op_type == IS_CONST ) { if ( zend_try_ct_eval_binary_op ( & result - > u . constant , opcode , & left_node . u . constant , & right_node . u . constant ) ) { result - > op_type = IS_CONST ; zval_ptr_dtor ( & left_node . u . constant ) ; zval_ptr_dtor ( & right_node . u . constant ) ; return ; } } do { // redacted code zend_emit_op_tmp ( result , opcode , & left_node , & right_node ) ; } while ( 0 ) ; } /* }}} */ 

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

Тип кода операции получен от узла AST на этот раз (в отличие от ZEND_AST_BINARY_OP кодирования, как было видно в прошлый раз), поскольку узел ZEND_AST_BINARY_OP хранит это значение (как видно из семантического действия нового производственного правила), чтобы различать двоичные операции. Когда оба операнда являются литералами, zend_try_ct_eval_binary_op функция zend_try_ct_eval_binary_op . Эта функция выглядит следующим образом:

 static inline zend_bool zend_try_ct_eval_binary_op ( zval * result , uint32_t opcode , zval * op1 , zval * op2 ) /* {{{ */ { binary_op_type fn = get_binary_op ( opcode ) ; /* don't evaluate division by zero at compile-time */ if ( ( opcode == ZEND_DIV || opcode == ZEND_MOD ) && zval_get_long ( op2 ) == 0 ) { return 0 ; } else if ( ( opcode == ZEND_SL || opcode == ZEND_SR ) && zval_get_long ( op2 ) < 0 ) { return 0 ; } fn ( result , op1 , op2 ) ; return 1 ; } /* }}} */ 

Функция получает обратный вызов из функции get_binary_op ( источник ) в Zend / zend_opcode.c в соответствии с типом кода операции. Это означает, что нам нужно будет обновить эту функцию рядом с обслуживанием ZEND_RANGE операции ZEND_RANGE . Добавьте следующую инструкцию get_binary_op функцию get_binary_op (строка ~ 750):

 case ZEND_RANGE : return ( binary_op_type ) range_function ; 

Теперь мы должны определить функцию range_function . Это будет сделано в файле Zend / zend_operators.c вместе со всеми другими операторами:

 ZEND_API int ZEND_FASTCALL range_function ( zval * result , zval * op1 , zval * op2 ) /* {{{ */ { zval tmp ; ZVAL_DEREF ( op1 ) ; ZVAL_DEREF ( op2 ) ; if ( Z_TYPE_P ( op1 ) == IS_LONG && Z_TYPE_P ( op2 ) == IS_LONG ) { zend_long min = Z_LVAL_P ( op1 ) , max = Z_LVAL_P ( op2 ) ; zend_ulong size , i ; if ( min > max ) { zend_throw_error ( NULL , "Min should be less than (or equal to) max" ) ; return FAILURE ; } // calculate size (one less than the total size for an inclusive range) size = max - min ; // the size cannot be greater than or equal to HT_MAX_SIZE // HT_MAX_SIZE - 1 takes into account the inclusive range size if ( size >= HT_MAX_SIZE - 1 ) { zend_throw_error ( NULL , "Range size is too large" ) ; return FAILURE ; } // increment the size to take into account the inclusive range ++ size ; // set the zval type to be a long Z_TYPE_INFO ( tmp ) = IS_LONG ; // initialise the array to a given size array_init_size ( result , size ) ; zend_hash_real_init ( Z_ARRVAL_P ( result ) , 1 ) ; ZEND_HASH_FILL_PACKED ( Z_ARRVAL_P ( result ) ) { for ( i = 0 ; i < size ; ++ i ) { Z_LVAL ( tmp ) = min + i ; ZEND_HASH_FILL_ADD ( & tmp ) ; } } ZEND_HASH_FILL_END ( ) ; } else if ( // if both operands are either integers or doubles ( Z_TYPE_P ( op1 ) == IS_LONG || Z_TYPE_P ( op1 ) == IS_DOUBLE ) && ( Z_TYPE_P ( op2 ) == IS_LONG || Z_TYPE_P ( op2 ) == IS_DOUBLE ) ) { long double min , max , size , i ; if ( Z_TYPE_P ( op1 ) == IS_LONG ) { min = ( long double ) Z_LVAL_P ( op1 ) ; max = ( long double ) Z_DVAL_P ( op2 ) ; } else if ( Z_TYPE_P ( op2 ) == IS_LONG ) { min = ( long double ) Z_DVAL_P ( op1 ) ; max = ( long double ) Z_LVAL_P ( op2 ) ; } else { min = ( long double ) Z_DVAL_P ( op1 ) ; max = ( long double ) Z_DVAL_P ( op2 ) ; } if ( min > max ) { zend_throw_error ( NULL , "Min should be less than (or equal to) max" ) ; return FAILURE ; } size = max - min ; if ( size >= HT_MAX_SIZE - 1 ) { zend_throw_error ( NULL , "Range size is too large" ) ; return FAILURE ; } // we cast the size to an integer to get rid of the decimal places, // since we only care about whole number sizes size = ( int ) size + 1 ; Z_TYPE_INFO ( tmp ) = IS_DOUBLE ; array_init_size ( result , size ) ; zend_hash_real_init ( Z_ARRVAL_P ( result ) , 1 ) ; ZEND_HASH_FILL_PACKED ( Z_ARRVAL_P ( result ) ) { for ( i = 0 ; i < size ; ++ i ) { Z_DVAL ( tmp ) = min + i ; ZEND_HASH_FILL_ADD ( & tmp ) ; } } ZEND_HASH_FILL_END ( ) ; } else { zend_throw_error ( NULL , "Unsupported operand types - only ints and floats are supported" ) ; return FAILURE ; } return SUCCESS ; } /* }}} */ 

Прототип функции содержит два новых макроса: ZEND_API и ZEND_FASTCALL . ZEND_API используется для управления видимостью функций, делая их доступными для расширений, которые компилируются как общие объекты. ZEND_FASTCALL используется для обеспечения более эффективного соглашения о вызовах, когда первые два аргумента будут передаваться с использованием регистров, а не стека (больше относится к 32-разрядным сборкам, чем к 64-разрядным сборкам на x86).

Тело функции очень похоже на то, что было в файле Zend / zend_vm_def.h в предыдущей статье. Специфичные для ВМ вещи больше не присутствуют, включая HANDLE_EXCEPTION макроса HANDLE_EXCEPTION (которые были заменены на return FAILURE; ), и ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION макроса ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION были полностью удалены (эта проверка и операция должны остаться в ВМ, и поэтому макрос будет вызван из кода VM позже).

Еще одно ZVAL_DEFEF отличие состоит в том, что мы применяем ZVAL_DEFEF к обоим операндам, чтобы обеспечить правильную обработку ссылок. Это было то, что ранее было сделано внутри виртуальной машины с использованием псевдо-макроса GET_OPn_ZVAL_PTR_DEREF , но теперь было перенесено в эту функцию. Это было сделано не потому, что это необходимо во время компиляции (поскольку для обработки во время компиляции оба операнда должны быть литералами, и на них нельзя ссылаться), а потому, что это позволяет другим местам внутри кодовой базы безопасно вызывать range_function без необходимости беспокоиться об обработке ссылок. Таким образом, обработка ссылок выполняется большинством функций оператора, а не в их определении кода операции виртуальной машины (кроме случаев, когда имеет значение производительность).

Наконец, мы должны добавить прототип range_function в файл Zend / zend_operators.h :

 ZEND_API int ZEND_FASTCALL range_function ( zval * result , zval * op1 , zval * op2 ) ; 

Обновление Zend VM

Теперь мы должны еще раз обновить виртуальную ZEND_RANGE Zend, чтобы обрабатывать выполнение ZEND_RANGE операции ZEND_RANGE во время выполнения. Поместите следующий код в Zend / zend_vm_def.h (внизу):

 ZEND_VM_HANDLER ( 182 , ZEND_RANGE , CONST | TMPVAR | CV , CONST | TMPVAR | CV ) { USE_OPLINE zend_free_op free_op1 , free_op2 ; zval * op1 , * op2 ; SAVE_OPLINE ( ) ; op1 = GET_OP1_ZVAL_PTR ( BP_VAR_R ) ; op2 = GET_OP2_ZVAL_PTR ( BP_VAR_R ) ; range_function ( EX_VAR ( opline - > result . var ) , op1 , op2 ) ; FREE_OP1 ( ) ; FREE_OP2 ( ) ; ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION ( ) ; } 

(Опять же, номер кода операции должен быть на единицу больше, чем текущий максимальный номер кода операции, который можно увидеть в нижней части файла Zend / zend_vm_opcodes.h .)

Определение на этот раз намного короче, так как вся работа выполняется в range_function . Мы просто вызываем эту функцию, передавая операнд результата текущего opline для хранения вычисленного значения. Проверки исключений и переход на следующий код операции, которые были удалены из range_function , все еще обрабатываются в виртуальной ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION посредством вызова ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION в конце. Также, как упоминалось ранее, мы избегаем обработки ссылок в ВМ, используя GET_OPn_ZVAL_PTR псевдо-макросы GET_OPn_ZVAL_PTR_DEREF (вместо GET_OPn_ZVAL_PTR_DEREF ).

Теперь восстановите виртуальную машину, выполнив файл Zend / zend_vm_gen.php .

Наконец, симпатичный принтер нуждается в обновлении в файле Zend / zend_ast.c еще раз. Обновите комментарий таблицы приоритетов, указав новый оператор с приоритетом 170 (строка ~ 520):

 * 170 non - associative == != == = != = | > 

Затем вставьте оператор case в функцию zend_ast_export_ex для обработки ZEND_RANGE операции ZEND_RANGE в ZEND_AST_BINARY_OP case ZEND_AST_BINARY_OP (строка ~ 1300):

 case ZEND_RANGE : BINARY_OP ( " |> " , 170 , 171 , 171 ) ; 

Вывод

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

Третья часть этой серии статей будет опираться на эту реализацию, освещая, как мы можем перегрузить этот оператор. Это позволит использовать объекты в качестве операндов (например, объекты из библиотеки GMP или объекты, реализующие __toString ). Также будет показано, как мы можем добавить правильную поддержку строк (не так, как в текущей функции диапазона PHP). Но сейчас, я надеюсь, это послужило хорошей демонстрацией некоторых дополнительных аспектов ZE при внедрении операторов в PHP.