Funções anônimas do PHP: expondo uma sessão de magia negra



Provavelmente, deve-se começar com o fato de que uma função anônima (fechamento) no PHP não é uma função, mas um objeto da classe Closure . Na verdade, este artigo poderia ter sido concluído, mas se alguém estiver interessado nos detalhes, bem-vindo ao gato.


Para não ser infundado:
$func = function (){}; var_dump($func); --------- object(Closure)#1 (0) { } 

Olhando para o futuro, direi que esse não é realmente um objeto comum. Vamos descobrir.

Por exemplo, esse código
 $func = function (){ echo 'Hello world!'; }; $func(); 

compila em um conjunto de códigos de operação:
 line #* EIO 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 #* EIO op fetch ext return operands -------------------------------------------------------------------------- 9 0 E > ECHO 'Hello+world%21' 10 1 > RETURN null 

O bloco com a descrição do corpo da função não é particularmente interessante para nós, mas no primeiro bloco existem dois códigos de operação interessantes: DECLARE_LAMBDA_FUNCTION e INIT_DYNAMIC_CALL . Vamos começar com o segundo.

INIT_DYNAMIC_CALL


Esse código de operação é usado quando o compilador vê uma chamada de função em uma variável ou matriz. I.e.
 $variable(); ['ClassName', 'staticMethod'](); 

Este não é um código de operação exclusivo específico apenas para fechamentos. Essa sintaxe também funciona para objetos chamando o método __invoke () , para variáveis ​​de string que contêm o nome da função ( $ a = 'funcName'; $ a (); ) e para matrizes que contêm o nome da classe e o método estático nele.

No caso de fechamento, estamos interessados ​​em chamar uma variável com um objeto, o que é lógico.
Indo mais fundo no código da VM que processa esse código de operação, chegamos à função zend_init_dynamic_call_object , na qual veremos o seguinte (fatia):
 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; } ... } 

É engraçado que a chamada familiar do método __invoke em termos de VM seja uma tentativa de chamar encerramento - get_closure .

Na verdade, nesse ponto, a diferença começa no tratamento da chamada para a função anônima e o método __invoke de um objeto regular.
No PHP, cada objeto possui um conjunto de manipuladores diferentes que definem seus métodos de utilidade e mágica.
O conjunto padrão se parece com isso
 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 */ }; 


Agora estamos interessados ​​no manipulador get_closure . Para um objeto regular, ele aponta para a função zend_std_get_closure , que verifica se a função __invoke está definida para o objeto e retorna um ponteiro para ele ou um erro. Mas para a classe Closure , que implementa funções anônimas, nessa matriz de manipuladores, quase todas as funções utilitárias são redefinidas, incluindo aquelas que controlam o ciclo de vida. I.e. embora para o usuário pareça um objeto comum, mas na verdade é um mutante com superpoderes :)
Registrar manipuladores para um objeto da classe 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; } 


O manual diz:
Além dos métodos descritos aqui, esta classe também possui um método __invoke . Esse método é necessário apenas para compatibilidade com outras classes nas quais a chamada mágica é implementada, pois esse método não é usado ao chamar a função.

E isso é verdade. A função get_closure para um fechamento não retorna __invoke , mas sua função a partir da qual o fechamento foi criado.

Você pode estudar as fontes com mais detalhes - o arquivo zend_closure.c , e passaremos para o próximo opcode.

DECLARE_LAMBDA_FUNCTION


Mas este é um código de operação exclusivo para circuito e não está mais funcionando com nada. Sob o capô do processador, há três operações principais:
  1. É procurado um ponteiro para uma função compilada, que será a essência do fechamento.
  2. O contexto para a criação do fechamento (em outras palavras, isso ) é definido.
  3. Com base nos dois primeiros pontos, um objeto da classe Closure é criado .


E aqui neste lugar não começam notícias muito agradáveis.

Então, o que há de errado com funções anônimas?


Criar um fechamento é uma operação mais difícil do que criar um objeto comum. Não é apenas o mecanismo padrão para criar um objeto chamado, mas também adiciona uma certa quantidade de lógica, a mais desagradável é copiar toda a matriz de códigos de operação da sua função para o corpo do fechamento. Isso por si só não é tão assustador, mas exatamente até você começar a usá-lo "incorretamente".

Para entender exatamente onde os problemas aguardam, analisaremos os casos quando um encerramento for criado.
O fechamento é recriado:
a) em cada processamento do código de operação DECLARE_LAMBDA_FUNCTION.
Intuitivamente - exatamente o caso em que o fechamento parece bom, mas, na verdade, um novo objeto de fechamento será criado a cada iteração do loop.
 foreach($values as $value){ doSomeStuff($value, function($args) { closureBody }); } 

b) sempre que os métodos bind e bindTo são chamados :
Aqui, o fechamento será recriado também a cada iteração.
 $closure = function($args) { closureBody }; foreach($objects as $object){ $closure->bindTo($object); $object->doSomeStuff($closure); } 

c) sempre que o método de chamada for chamado , se um gerador for usado como uma função. E se não for um gerador, mas uma função comum, somente a parte com a cópia do conjunto de códigos de operação é executada. Essas coisas.

Conclusões


Se o desempenho não for importante para você a todo custo, as funções anônimas são convenientes e agradáveis. E se importante, então provavelmente não vale a pena.

De qualquer forma, agora você sabe que fechamentos e ciclos, se não forem preparados corretamente, são uma combinação.

Obrigado pela atenção!

Source: https://habr.com/ru/post/pt478596/


All Articles