diff --git a/ext/standard/array.c b/ext/standard/array.c index 0b184efc3998..021089319efa 100644 --- a/ext/standard/array.c +++ b/ext/standard/array.c @@ -33,6 +33,9 @@ #include "zend_smart_str.h" #include "zend_bitset.h" #include "zend_exceptions.h" +#include "zend_extensions.h" +#include "zend_fibers.h" +#include "zend_observer.h" #include "ext/random/php_random.h" #include "zend_frameless_function.h" @@ -67,10 +70,79 @@ static void php_array_init_globals(zend_array_globals *array_globals) } /* }}} */ +/* Sorts restore these globals in LIFO order, which fibers break: keep one copy per fiber. */ +static int php_array_fiber_rid = -1; + +static void php_array_fiber_state_save(php_array_fiber_state *state) +{ + state->user_compare_fci = BG(user_compare_fci); + state->user_compare_fci_cache = BG(user_compare_fci_cache); + state->multisort_func = ARRAYG(multisort_func); + state->compare_deprecation_thrown = ARRAYG(compare_deprecation_thrown); +} + +static void php_array_fiber_state_load(const php_array_fiber_state *state) +{ + if (state) { + BG(user_compare_fci) = state->user_compare_fci; + BG(user_compare_fci_cache) = state->user_compare_fci_cache; + ARRAYG(multisort_func) = state->multisort_func; + ARRAYG(compare_deprecation_thrown) = state->compare_deprecation_thrown; + } else { + memset(&BG(user_compare_fci), 0, sizeof(BG(user_compare_fci))); + BG(user_compare_fci_cache) = empty_fcall_info_cache; + ARRAYG(multisort_func) = NULL; + ARRAYG(compare_deprecation_thrown) = 0; + } +} + +static php_array_fiber_state *php_array_fiber_state_of(zend_fiber_context *context) +{ + if (context == EG(main_fiber_context)) { + return &ARRAYG(main_fiber_state); + } + return context->reserved[php_array_fiber_rid]; +} + +static void php_array_fiber_init(zend_fiber_context *initializing) +{ + /* The reserved slots are not guaranteed to be zeroed. */ + initializing->reserved[php_array_fiber_rid] = NULL; +} + +static void php_array_fiber_switch(zend_fiber_context *from, zend_fiber_context *to) +{ + if (from == EG(main_fiber_context) || from->status != ZEND_FIBER_STATUS_DEAD) { + php_array_fiber_state *state = php_array_fiber_state_of(from); + if (!state) { + state = emalloc(sizeof(php_array_fiber_state)); + from->reserved[php_array_fiber_rid] = state; + } + php_array_fiber_state_save(state); + } + + php_array_fiber_state_load(php_array_fiber_state_of(to)); +} + +static void php_array_fiber_destroy(zend_fiber_context *destroying) +{ + if (destroying->reserved[php_array_fiber_rid]) { + efree(destroying->reserved[php_array_fiber_rid]); + destroying->reserved[php_array_fiber_rid] = NULL; + } +} + PHP_MINIT_FUNCTION(array) /* {{{ */ { ZEND_INIT_MODULE_GLOBALS(array, php_array_init_globals, NULL); + php_array_fiber_rid = zend_get_resource_handle("standard_array"); + if (php_array_fiber_rid >= 0) { + zend_observer_fiber_init_register(php_array_fiber_init); + zend_observer_fiber_switch_register(php_array_fiber_switch); + zend_observer_fiber_destroy_register(php_array_fiber_destroy); + } + return SUCCESS; } /* }}} */ diff --git a/ext/standard/php_array.h b/ext/standard/php_array.h index a7e8afdec34b..f7a923500d6b 100644 --- a/ext/standard/php_array.h +++ b/ext/standard/php_array.h @@ -64,9 +64,17 @@ PHPAPI bool php_array_pick_keys(php_random_algo_with_state engine, zval *input, extern PHPAPI zend_class_entry *sort_direction_ce; +typedef struct _php_array_fiber_state { + zend_fcall_info user_compare_fci; + zend_fcall_info_cache user_compare_fci_cache; + bucket_compare_func_t *multisort_func; + bool compare_deprecation_thrown; +} php_array_fiber_state; + ZEND_BEGIN_MODULE_GLOBALS(array) bucket_compare_func_t *multisort_func; bool compare_deprecation_thrown; + php_array_fiber_state main_fiber_state; ZEND_END_MODULE_GLOBALS(array) #define ARRAYG(v) ZEND_MODULE_GLOBALS_ACCESSOR(array, v) diff --git a/ext/standard/tests/array/array_sort_comparator_fiber.phpt b/ext/standard/tests/array/array_sort_comparator_fiber.phpt new file mode 100644 index 000000000000..eeb8f6729911 --- /dev/null +++ b/ext/standard/tests/array/array_sort_comparator_fiber.phpt @@ -0,0 +1,116 @@ +--TEST-- +Sort functions: a comparator that suspends a fiber keeps its own callback +--FILE-- + $y; + }); + return $array; +}); +$fiber->start(); +while (!$fiber->isTerminated()) { + $fiber->resume(); +} +echo implode(',', $fiber->getReturn()), "\n"; + +echo "Another sort runs while the first one is suspended\n"; +$fiber = new Fiber(function () { + $array = [3, 1, 2]; + usort($array, function ($x, $y) { + echo "A\n"; + Fiber::suspend(); + return $x <=> $y; + }); + echo "A done\n"; +}); +$fiber->start(); +$array = [5, 4, 6]; +$resumed = false; +usort($array, function ($x, $y) use ($fiber, &$resumed) { + echo "B\n"; + if (!$resumed) { + $resumed = true; + $fiber->resume(); + } + return $x <=> $y; +}); +echo implode(',', $array), "\n"; + +echo "The suspended fiber is destroyed by another sort\n"; +$fiber = new Fiber(function () { + $array = [3, 1, 2]; + usort($array, function ($x, $y) { + Fiber::suspend(); + return $x <=> $y; + }); +}); +$fiber->start(); +$array = [5, 4, 6, 1, 2]; +usort($array, function ($x, $y) use (&$fiber) { + $fiber = null; + return $x <=> $y; +}); +echo implode(',', $array), "\n"; + +echo "Two array_multisort() calls suspend in __toString()\n"; +class Item +{ + public function __construct(private string $value) {} + + public function __toString(): string + { + Fiber::suspend(); + return $this->value; + } +} +$first = new Fiber(function () { + $array = [new Item('b'), new Item('a'), new Item('c')]; + array_multisort($array, SORT_STRING); + echo implode(',', array_map('strval', $array)) === 'a,b,c' ? "first sorted\n" : "first wrong\n"; +}); +$second = new Fiber(function () { + $array = [new Item('f'), new Item('e'), new Item('d')]; + array_multisort($array, SORT_STRING); + echo implode(',', array_map('strval', $array)) === 'd,e,f' ? "second sorted\n" : "second wrong\n"; +}); +$first->start(); +$second->start(); +while (!$first->isTerminated()) { + $first->resume(); +} +while (!$second->isTerminated()) { + $second->resume(); +} + +echo "A sort nested in a comparator\n"; +$array = [3, 1, 2]; +usort($array, function ($x, $y) { + $inner = [2, 1]; + usort($inner, fn($a, $b) => $a <=> $b); + return $x <=> $y; +}); +echo implode(',', $array), "\n"; + +?> +--EXPECT-- +Suspend and resume in a single sort +1,2,3 +Another sort runs while the first one is suspended +A +B +A +B +B +4,5,6 +The suspended fiber is destroyed by another sort +1,2,4,5,6 +Two array_multisort() calls suspend in __toString() +first sorted +second sorted +A sort nested in a comparator +1,2,3