Fungsi Anonim PHP: Mengekspos Sesi Sihir Hitam



Mungkin, seseorang harus mulai dengan fakta bahwa fungsi anonim (penutupan) dalam PHP bukan fungsi, tetapi objek dari kelas Penutupan . Sebenarnya, artikel ini bisa saja selesai, tetapi jika ada yang tertarik dengan detailnya, selamat datang ke kucing.


Agar tidak berdasar:
$func = function (){}; var_dump($func); --------- object(Closure)#1 (0) { } 

Ke depan, saya akan mengatakan bahwa ini bukan objek biasa. Mari kita cari tahu.

Misalnya, kode seperti itu
 $func = function (){ echo 'Hello world!'; }; $func(); 

mengkompilasi ke dalam sekumpulan opcode:
 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 

Blok dengan deskripsi fungsi tubuh tidak terlalu menarik bagi kami, tetapi di blok pertama ada dua opcode yang menarik: DECLARE_LAMBDA_FUNCTION dan INIT_DYNAMIC_CALL . Mari kita mulai dengan yang kedua.

INIT_DYNAMIC_CALL


Opcode ini digunakan ketika kompiler melihat panggilan fungsi pada variabel atau array. Yaitu
 $variable(); ['ClassName', 'staticMethod'](); 

Ini bukan opcode unik khusus untuk penutupan saja. Sintaks ini juga berfungsi untuk objek dengan memanggil metode __invoke () , untuk variabel string yang berisi nama fungsi ( $ a = 'funcName'; $ a (); ), dan untuk array yang berisi nama kelas dan metode statis di dalamnya.

Dalam kasus penutupan, kami tertarik untuk memanggil variabel dengan objek, yang logis.
Menuju lebih dalam ke kode VM yang memproses opcode ini, kita menuju ke fungsi zend_init_dynamic_call_object , di mana kita akan melihat yang berikut (mengiris):
 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; } ... } 

Sangat lucu bahwa panggilan metode __invoke yang biasa dalam hal VM adalah upaya untuk memanggil penutupan - get_closure .

Sebenarnya, pada titik ini perbedaan dimulai dalam menangani panggilan ke fungsi anonim dan metode __invoke objek biasa.
Dalam PHP, setiap objek memiliki satu set penangan yang berbeda yang mendefinisikan utilitas dan metode sihirnya.
Perangkat standar terlihat seperti ini
 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 */ }; 


Sekarang kami tertarik dengan penangan get_closure . Untuk objek biasa, ini menunjuk ke fungsi zend_std_get_closure , yang memeriksa bahwa fungsi __invoke didefinisikan untuk objek dan mengembalikan pointer ke objek tersebut atau kesalahan. Tetapi untuk kelas Penutupan , yang mengimplementasikan fungsi anonim, dalam susunan penangan ini hampir semua fungsi utilitas didefinisikan ulang, termasuk yang mengontrol siklus hidup. Yaitu Meskipun bagi pengguna itu terlihat seperti objek biasa, tetapi sebenarnya itu adalah mutan dengan kekuatan super :)
Daftarkan penangan untuk objek Penutupan kelas
 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; } 


Manual mengatakan:
Selain metode yang dijelaskan di sini, kelas ini juga memiliki metode __invoke . Metode ini hanya diperlukan untuk kompatibilitas dengan kelas lain di mana panggilan ajaib diterapkan, karena metode ini tidak digunakan saat memanggil fungsi.

Dan ini benar. Fungsi get_closure untuk penutupan tidak mengembalikan __invoke , tetapi fungsi Anda dari mana penutupan dibuat.

Anda dapat mempelajari sendiri sumbernya dengan lebih detail - file zend_closure.c , dan kami akan beralih ke opcode berikutnya.

DECLARE_LAMBDA_FUNCTION


Tapi ini opcode yang khusus untuk sirkuit dan tidak lagi bekerja dengan apa pun. Di bawah kap prosesor, ada tiga operasi utama:
  1. Pointer ke fungsi yang dikompilasi dicari, yang akan menjadi inti dari penutupan.
  2. Konteks untuk membuat penutupan (dengan kata lain, ini ) didefinisikan.
  3. Berdasarkan dua poin pertama, objek kelas Penutupan dibuat .


Dan di sini di tempat ini berita yang tidak menyenangkan dimulai.

Jadi apa yang salah dengan fungsi anonim?


Membuat penutupan adalah operasi yang lebih sulit daripada menciptakan objek biasa. Tidak hanya mekanisme standar untuk membuat objek yang disebut, itu juga menambahkan sejumlah logika, yang paling tidak menyenangkan adalah menyalin seluruh array opcodes fungsi Anda ke tubuh penutupan. Ini sendiri tidak begitu menakutkan, tetapi tepat sampai Anda mulai menggunakannya "salah".

Untuk memahami dengan tepat di mana masalah menunggu, kami akan menganalisis kasus ketika penutupan dibuat.
Penutupan dibuat kembali:
a) pada setiap pemrosesan opcode DECLARE_LAMBDA_FUNCTION.
Secara intuitif - persisnya kasus di mana penutupan terlihat bagus, tetapi sebenarnya objek penutupan baru akan dibuat pada setiap iterasi dari loop.
 foreach($values as $value){ doSomeStuff($value, function($args) { closureBody }); } 

b) setiap kali metode bind dan bindTo dipanggil :
Di sini penutupan akan dibuat kembali juga di setiap iterasi.
 $closure = function($args) { closureBody }; foreach($objects as $object){ $closure->bindTo($object); $object->doSomeStuff($closure); } 

c) setiap kali metode panggilan dipanggil , jika generator digunakan sebagai fungsi. Dan jika bukan generator, tetapi fungsi biasa, maka hanya bagian dengan menyalin array opcode yang dieksekusi. Hal-hal seperti itu.

Kesimpulan


Jika kinerja tidak penting bagi Anda di semua biaya, maka fungsi anonim nyaman dan menyenangkan. Dan jika penting, maka mungkin tidak sepadan.

Bagaimanapun, sekarang Anda tahu bahwa penutupan dan siklus, jika mereka tidak disiapkan dengan benar, adalah kombinasi seperti itu.

Terima kasih atas perhatian anda!

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


All Articles