Fonctions anonymes PHP: exposer la session Black Magic



Probablement, on devrait commencer par le fait qu'une fonction anonyme (fermeture) en PHP n'est pas une fonction, mais un objet de la classe Closure . En fait, cet article aurait pu être terminé, mais si quelqu'un est intéressé par les détails, bienvenue à cat.


Pour ne pas être infondé:
$func = function (){}; var_dump($func); --------- object(Closure)#1 (0) { } 

Pour l'avenir, je dirai que ce n'est pas vraiment un objet ordinaire. Voyons cela.

Par exemple, un tel code
 $func = function (){ echo 'Hello world!'; }; $func(); 

compile dans un tel ensemble d'opcodes:
 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 

Le bloc avec la description du corps de la fonction n'est pas particulièrement intéressant pour nous, mais dans le premier bloc il y a deux opcodes intéressants: DECLARE_LAMBDA_FUNCTION et INIT_DYNAMIC_CALL . Commençons par le second.

INIT_DYNAMIC_CALL


Cet opcode est utilisé lorsque le compilateur voit un appel de fonction sur une variable ou un tableau. C'est-à-dire
 $variable(); ['ClassName', 'staticMethod'](); 

Il ne s'agit pas d'un opcode unique spécifique aux fermetures uniquement. Cette syntaxe fonctionne également pour les objets en appelant la méthode __invoke () , pour les variables de chaîne contenant le nom de la fonction ( $ a = 'funcName'; $ a (); ), et pour les tableaux contenant le nom de la classe et la méthode statique.

Dans le cas de la fermeture, nous souhaitons appeler une variable avec un objet, ce qui est logique.
En approfondissant le code VM qui traite cet opcode, nous arrivons à la fonction zend_init_dynamic_call_object , dans laquelle nous verrons ce qui suit (découpage):
 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; } ... } 

C'est drôle que l' appel familier de la méthode __invoke en termes de VM soit une tentative d'appeler la fermeture - get_closure .

En fait, à ce stade, la différence commence dans la gestion de l'appel à la fonction anonyme et à la méthode __invoke d'un objet normal.
En PHP, chaque objet a un ensemble de gestionnaires différents qui définit son utilité et ses méthodes magiques.
L'ensemble standard ressemble à ceci
 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 */ }; 


Maintenant, nous sommes intéressés par le gestionnaire get_closure . Pour un objet normal, il pointe vers la fonction zend_std_get_closure , qui vérifie que la fonction __invoke est définie pour l'objet et renvoie soit un pointeur vers lui, soit une erreur. Mais pour la classe Closure , qui implémente des fonctions anonymes, dans ce tableau de gestionnaires, presque toutes les fonctions utilitaires sont redéfinies, y compris celles qui contrôlent le cycle de vie. C'est-à-dire bien que pour l'utilisateur cela ressemble à un objet ordinaire, mais en fait c'est un mutant avec des super pouvoirs :)
Enregistrer les gestionnaires pour un objet de classe Clôture
 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; } 


Le manuel dit:
En plus des méthodes décrites ici, cette classe possède également une méthode __invoke . Cette méthode n'est nécessaire que pour la compatibilité avec d'autres classes dans lesquelles l'appel magique est implémenté, car cette méthode n'est pas utilisée lors de l'appel de la fonction.

Et c'est vrai. La fonction get_closure pour une fermeture ne renvoie pas __invoke , mais votre fonction à partir de laquelle la fermeture a été créée.

Vous pouvez étudier les sources plus en détail vous-même - le fichier zend_closure.c , et nous passerons au prochain opcode.

DECLARE_LAMBDA_FUNCTION


Mais c'est un opcode qui est exclusivement pour le circuit et ne fonctionne plus avec rien. Sous le capot du processeur, il y a trois opérations principales:
  1. Un pointeur vers une fonction compilée est recherché, ce qui sera l'essence de la fermeture.
  2. Le contexte de création de la fermeture (en d'autres termes, ceci ) est défini.
  3. Sur la base des deux premiers points, un objet de classe Clôture est créé .


Et ici sur cet endroit commence une nouvelle pas très agréable.

Alors, quel est le problème avec les fonctions anonymes?


La création d'une fermeture est une opération plus difficile que la création d'un objet ordinaire. Non seulement le mécanisme standard de création d'un objet est appelé, il ajoute également une certaine quantité de logique, dont la plus désagréable consiste à copier l'ensemble du tableau des opcodes de votre fonction dans le corps de la fermeture. En soi, ce n'est pas si effrayant, mais exactement jusqu'à ce que vous commenciez à l'utiliser «incorrectement».

Pour comprendre exactement où les problèmes attendent, nous analyserons les cas où une fermeture est créée.
La fermeture est recréée:
a) à chaque traitement de l' opcode DECLARE_LAMBDA_FUNCTION.
Intuitivement - exactement le cas où la fermeture semble bonne, mais en fait, un nouvel objet de fermeture sera créé à chaque itération de la boucle.
 foreach($values as $value){ doSomeStuff($value, function($args) { closureBody }); } 

b) chaque fois que les méthodes bind et bindTo sont appelées :
Ici, la fermeture sera recréée également à chaque itération.
 $closure = function($args) { closureBody }; foreach($objects as $object){ $closure->bindTo($object); $object->doSomeStuff($closure); } 

c) à chaque appel de la méthode d' appel , si un générateur est utilisé en fonction. Et si ce n'est pas un générateur, mais une fonction ordinaire, alors seule la partie avec la copie du tableau d'opcodes est exécutée. De telles choses.

Conclusions


Si les performances ne sont pas importantes pour vous à tout prix, les fonctions anonymes sont pratiques et agréables. Et si c'est important, cela n'en vaut probablement pas la peine.

En tout cas, vous savez maintenant que les fermetures et les cycles, s'ils ne sont pas préparés correctement, sont une telle combinaison.

Merci de votre attention!

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


All Articles