Анонимные функции в PHP: сеанс чёрной магии с разоблачением

в 7:36, , рубрики: funcorp, php, Блог компании FunCorp, кишки, Программирование

Анонимные функции в PHP: сеанс чёрной магии с разоблачением - 1

Начать, наверное, следует с того, что анонимная функция(замыкание) в PHP — это не функция, а объект класса Closure. Собственно, на этом статью можно было бы и закончить, но если кому-то интересны подробности — добро пожаловать под кат.


Дабы не быть голословным:

$func = function (){};
var_dump($func);

---------
object(Closure)#1 (0) {
}

Забегая вперёд, скажу, что на самом деле это не совсем обычный объект. Давайте разберёмся.

Например, такой код

$func = function (){
    echo 'Hello world!';
};
$func();

компилируется в такой набор опкодов:

line     #* E I O op                       fetch   ext  return  operands
--------------------------------------------------------------------------
   8     0  E >   DECLARE_LAMBDA_FUNCTION                       '%00%7Bclosure%7D%2Fin%2FcrvX50x7fabda9ed09e'
  10     1        ASSIGN                                        !0, ~1
  11     2        INIT_DYNAMIC_CALL                             !0
         3        DO_FCALL                         0          
  11     2      > RETURN                                        1

Function %00%7Bclosure%7D%2Fin%2FcrvX50x7fabda9ed09e:
function name:  {closure}
line     #* E I O op                       fetch   ext  return  operands
--------------------------------------------------------------------------
   9     0  E >   ECHO                                          'Hello+world%21'
  10     1      > RETURN                                        null

Блок с описанием тела функции нам не особо интересен, а вот в первом блоке присутствуют два интересных нам опкода: DECLARE_LAMBDA_FUNCTION и INIT_DYNAMIC_CALL. Начнём со второго.

INIT_DYNAMIC_CALL

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

$variable();
['ClassName', 'staticMethod']();

Это не какой-то уникальный опкод, специфичный только для замыканий. Такой синтаксис также работает для объектов, вызывая метод __invoke(), для строковых переменных, содержащих имя функции ($a = 'funcName'; $a();), и для массивов, содержащих имена класса и статического метода в нём.

В случае замыкания нас интересует вызов на переменной с объектом, что логично.
Углубляясь в код VM, обрабатывающий этот опкод, мы дойдём до функции zend_init_dynamic_call_object, в которой увидим следующее (нарезка):

zend_execute_data *zend_init_dynamic_call_object(zend_object *function, uint32_t num_args)
{
	zend_function *fbc;
	zend_class_entry *called_scope;
	zend_object *object;
	...
	if (EXPECTED(function->handlers->get_closure) &&
	    EXPECTED(function->handlers->get_closure(function, &called_scope, &fbc, &object) == SUCCESS)) {
		...
	} else {
		zend_throw_error(NULL, "Function name must be a string");
		return NULL;
	}
	...
}

Забавно, что привычный всем вызов метода __invoke в терминах VM является попыткой вызова замыкания — get_closure.

Собственно, на этом месте начинается разница в обработке вызова анонимной функции и метода __invoke обычного объекта.
В PHP у каждого объекта существует набор различных обработчиков, определяющий его служебные и магические методы.

Стандартный набор выглядит так

ZEND_API const zend_object_handlers std_object_handlers = {
  0,                       /* offset */
   zend_object_std_dtor,         /* free_obj */
  zend_objects_destroy_object,    /* dtor_obj */
  zend_objects_clone_obj,           /* clone_obj */
  zend_std_read_property,           /* read_property */
  zend_std_write_property,       /* write_property */
  zend_std_read_dimension,       /* read_dimension */
  zend_std_write_dimension,      /* write_dimension */
  zend_std_get_property_ptr_ptr,  /* get_property_ptr_ptr */
  NULL,                     /* get */
  NULL,                     /* set */
  zend_std_has_property,        /* has_property */
  zend_std_unset_property,       /* unset_property */
  zend_std_has_dimension,           /* has_dimension */
  zend_std_unset_dimension,      /* unset_dimension */
  zend_std_get_properties,       /* get_properties */
  zend_std_get_method,          /* get_method */
  zend_std_get_constructor,      /* get_constructor */
  zend_std_get_class_name,       /* get_class_name */
  zend_std_compare_objects,      /* compare_objects */
  zend_std_cast_object_tostring,  /* cast_object */
  NULL,                     /* count_elements */
  zend_std_get_debug_info,       /* get_debug_info */
  /* ------- */
  zend_std_get_closure,         /* get_closure */
  /* ------- */
  zend_std_get_gc,             /* get_gc */
  NULL,                     /* do_operation */
  NULL,                     /* compare */
  NULL,                     /* get_properties_for */
};

Сейчас нас интересует обработчик get_closure. Для обычного объекта он указывает на функцию zend_std_get_closure, которая проверяет, что для объекта определена функция __invoke, и возвращает либо указатель на неё, либо ошибку. А вот для класса Closure, реализующего анонимные функции, в этом массиве обработчиков переопределены практически все служебные функции, включая те, которые управляют жизненным циклом. Т.е. хоть для пользователя он и выглядит как обычный объект, но на самом деле это мутант с суперспособностями :)

Регистрация обработчиков для объекта класса Closure
void zend_register_closure_ce(void) /* {{{ */
{
	zend_class_entry ce;

	INIT_CLASS_ENTRY(ce, "Closure", closure_functions);
	zend_ce_closure = zend_register_internal_class(&ce);
	zend_ce_closure->ce_flags |= ZEND_ACC_FINAL;
	zend_ce_closure->create_object = zend_closure_new;
	zend_ce_closure->serialize = zend_class_serialize_deny;
	zend_ce_closure->unserialize = zend_class_unserialize_deny;

	memcpy(&closure_handlers, &std_object_handlers, sizeof(zend_object_handlers));
	closure_handlers.free_obj = zend_closure_free_storage;
	closure_handlers.get_constructor = zend_closure_get_constructor;
	closure_handlers.get_method = zend_closure_get_method;
	closure_handlers.write_property = zend_closure_write_property;
	closure_handlers.read_property = zend_closure_read_property;
	closure_handlers.get_property_ptr_ptr = zend_closure_get_property_ptr_ptr;
	closure_handlers.has_property = zend_closure_has_property;
	closure_handlers.unset_property = zend_closure_unset_property;
	closure_handlers.compare_objects = zend_closure_compare_objects;
	closure_handlers.clone_obj = zend_closure_clone;
	closure_handlers.get_debug_info = zend_closure_get_debug_info;
        /* ------- */
	closure_handlers.get_closure = zend_closure_get_closure;
        /* ------- */
	closure_handlers.get_gc = zend_closure_get_gc;
}

В руководстве говорится:

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

И это таки правда. Функция get_closure для замыкания возвращает не __invoke, а вашу функцию, из которой создавалось замыкание.

Более подробно можете изучить исходники сами — файл zend_closure.c, а мы перейдём к следующему опкоду.

DECLARE_LAMBDA_FUNCTION

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

  1. Ищется указатель на скомпилированную функцию, которая и будет сутью замыкания.
  2. Определяется контекст создания замыкания (другими словами, this).
  3. На основе двух первых пунктов создаётся объект класса Closure.

И вот на этом месте начинаются не очень приятные новости.

Так что же не так с анонимными функциями?

Создание замыкания — операция более тяжёлая, чем создание обыкновенного объекта. Мало того что вызывается стандартный механизм создания объекта, к нему ещё добавляется и некоторое количество логики, самым неприятным из которой является копирование всего массива опкодов вашей функции в тело объекта замыкания. Само по себе это не то чтобы страшно, но ровно до того момента, пока вы не начинаете его использовать «неправильно».

Чтобы понять, где именно поджидают проблемы, разберём случаи, когда происходит создание замыкания.
Замыкание создаётся заново:
а) при каждой обработке опкода DECLARE_LAMBDA_FUNCTION.
Интуитивно — ровно тот кейс, где замыкание смотрится хорошо, но на самом деле новый объект замыкания будет создаваться на каждой итерации цикла.

foreach($values as $value){
	doSomeStuff($value, function($args) { closureBody });
}

б) при каждом вызове методов bind и bindTo:
Тут замыкание будет создаваться заново также на каждой итерации.

$closure = function($args) { closureBody };
foreach($objects as $object){
	$closure->bindTo($object);
	$object->doSomeStuff($closure);
}

с) при каждом вызове метода call, если в качестве функции используется генератор. А если не генератор, а обычная функция, то выполняется только часть с копированием массива опкодов. Такие дела.

Выводы

Если вам не важна производительность любой ценой, то анонимные функции удобны и приятны. А если важна, то, наверное, не стоит.

В любом случае теперь вы знаете, что замыкания и циклы, если их готовить неправильно, — такая себе комбинация.

Спасибо за внимание!

Автор: rjhdby

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js