From 1cdde73a7d7c6974aaf059ea04133a162f864623 Mon Sep 17 00:00:00 2001 From: Arnaud Le Blanc Date: Wed, 21 Feb 2024 16:32:58 +0100 Subject: [PATCH 1/2] Expose fibers API --- Zend/zend_fibers.c | 71 ++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 63 insertions(+), 8 deletions(-) diff --git a/Zend/zend_fibers.c b/Zend/zend_fibers.c index 1fde0453b9c65..273647692d824 100644 --- a/Zend/zend_fibers.c +++ b/Zend/zend_fibers.c @@ -20,6 +20,7 @@ #include "zend.h" #include "zend_API.h" #include "zend_ini.h" +#include "zend_variables.h" #include "zend_vm.h" #include "zend_exceptions.h" #include "zend_builtin_functions.h" @@ -637,7 +638,11 @@ static zend_always_inline void zend_fiber_delegate_transfer_result( RETURN_THROWS(); } - RETURN_COPY_VALUE(&transfer->value); + if (return_value != NULL) { + RETURN_COPY_VALUE(&transfer->value); + } else { + zval_ptr_dtor(&transfer->value); + } } static zend_always_inline zend_fiber_transfer zend_fiber_switch_to( @@ -665,7 +670,7 @@ static zend_always_inline zend_fiber_transfer zend_fiber_switch_to( return transfer; } -static zend_always_inline zend_fiber_transfer zend_fiber_resume(zend_fiber *fiber, zval *value, bool exception) +static zend_always_inline zend_fiber_transfer zend_fiber_resume_internal(zend_fiber *fiber, zval *value, bool exception) { zend_fiber *previous = EG(active_fiber); @@ -683,8 +688,10 @@ static zend_always_inline zend_fiber_transfer zend_fiber_resume(zend_fiber *fibe return transfer; } -static zend_always_inline zend_fiber_transfer zend_fiber_suspend(zend_fiber *fiber, zval *value) +static zend_always_inline zend_fiber_transfer zend_fiber_suspend_internal(zend_fiber *fiber, zval *value) { + ZEND_ASSERT(!(fiber->flags & ZEND_FIBER_FLAG_DESTROYED)); + ZEND_ASSERT(fiber->context.status == ZEND_FIBER_STATUS_RUNNING || fiber->context.status == ZEND_FIBER_STATUS_SUSPENDED); ZEND_ASSERT(fiber->caller != NULL); zend_fiber_context *caller = fiber->caller; @@ -695,6 +702,54 @@ static zend_always_inline zend_fiber_transfer zend_fiber_suspend(zend_fiber *fib return zend_fiber_switch_to(caller, value, false); } +ZEND_API zend_result zend_fiber_start(zend_fiber *fiber, zval *return_value) +{ + ZEND_ASSERT(fiber->context.status == ZEND_FIBER_STATUS_INIT); + + if (zend_fiber_init_context(&fiber->context, zend_ce_fiber, zend_fiber_execute, EG(fiber_stack_size)) == FAILURE) { + return FAILURE; + } + + fiber->previous = &fiber->context; + + zend_fiber_transfer transfer = zend_fiber_resume_internal(fiber, NULL, false); + + zend_fiber_delegate_transfer_result(&transfer, EG(current_execute_data), return_value); + + return SUCCESS; +} + +ZEND_API void zend_fiber_resume(zend_fiber *fiber, zval *value, zval *return_value) +{ + ZEND_ASSERT(fiber->context.status == ZEND_FIBER_STATUS_SUSPENDED && fiber->caller == NULL); + + fiber->stack_bottom->prev_execute_data = EG(current_execute_data); + + zend_fiber_transfer transfer = zend_fiber_resume_internal(fiber, value, /* exception */ false); + + zend_fiber_delegate_transfer_result(&transfer, EG(current_execute_data), return_value); +} + +ZEND_API void zend_fiber_resume_exception(zend_fiber *fiber, zval *exception, zval *return_value) +{ + ZEND_ASSERT(fiber->context.status == ZEND_FIBER_STATUS_SUSPENDED && fiber->caller == NULL); + + fiber->stack_bottom->prev_execute_data = EG(current_execute_data); + + zend_fiber_transfer transfer = zend_fiber_resume_internal(fiber, exception, /* exception */ true); + + zend_fiber_delegate_transfer_result(&transfer, EG(current_execute_data), return_value); +} + +ZEND_API void zend_fiber_suspend(zend_fiber *fiber, zval *value, zval *return_value) +{ + fiber->stack_bottom->prev_execute_data = NULL; + + zend_fiber_transfer transfer = zend_fiber_suspend_internal(fiber, value); + + zend_fiber_delegate_transfer_result(&transfer, EG(current_execute_data), return_value); +} + static zend_object *zend_fiber_object_create(zend_class_entry *ce) { zend_fiber *fiber = emalloc(sizeof(zend_fiber)); @@ -720,7 +775,7 @@ static void zend_fiber_object_destroy(zend_object *object) fiber->flags |= ZEND_FIBER_FLAG_DESTROYED; - zend_fiber_transfer transfer = zend_fiber_resume(fiber, &graceful_exit, true); + zend_fiber_transfer transfer = zend_fiber_resume_internal(fiber, &graceful_exit, true); zval_ptr_dtor(&graceful_exit); @@ -836,7 +891,7 @@ ZEND_METHOD(Fiber, start) fiber->previous = &fiber->context; - zend_fiber_transfer transfer = zend_fiber_resume(fiber, NULL, false); + zend_fiber_transfer transfer = zend_fiber_resume_internal(fiber, NULL, false); zend_fiber_delegate_transfer_result(&transfer, INTERNAL_FUNCTION_PARAM_PASSTHRU); } @@ -871,7 +926,7 @@ ZEND_METHOD(Fiber, suspend) fiber->stack_bottom->prev_execute_data = NULL; - zend_fiber_transfer transfer = zend_fiber_suspend(fiber, value); + zend_fiber_transfer transfer = zend_fiber_suspend_internal(fiber, value); zend_fiber_delegate_transfer_result(&transfer, INTERNAL_FUNCTION_PARAM_PASSTHRU); } @@ -900,7 +955,7 @@ ZEND_METHOD(Fiber, resume) fiber->stack_bottom->prev_execute_data = EG(current_execute_data); - zend_fiber_transfer transfer = zend_fiber_resume(fiber, value, false); + zend_fiber_transfer transfer = zend_fiber_resume_internal(fiber, value, false); zend_fiber_delegate_transfer_result(&transfer, INTERNAL_FUNCTION_PARAM_PASSTHRU); } @@ -928,7 +983,7 @@ ZEND_METHOD(Fiber, throw) fiber->stack_bottom->prev_execute_data = EG(current_execute_data); - zend_fiber_transfer transfer = zend_fiber_resume(fiber, exception, true); + zend_fiber_transfer transfer = zend_fiber_resume_internal(fiber, exception, true); zend_fiber_delegate_transfer_result(&transfer, INTERNAL_FUNCTION_PARAM_PASSTHRU); } From 2d38ca3f258ef7161681378ad214c99371a58722 Mon Sep 17 00:00:00 2001 From: Arnaud Le Blanc Date: Wed, 21 Feb 2024 16:33:33 +0100 Subject: [PATCH 2/2] Allow fiber switching during GC --- Zend/tests/bug69446.phpt | 8 +- Zend/tests/fibers/destructors_001.phpt | 53 +++++ Zend/tests/fibers/destructors_002.phpt | 35 +++ Zend/tests/fibers/destructors_003.phpt | 42 ++++ Zend/tests/fibers/destructors_004.phpt | 79 +++++++ Zend/tests/fibers/destructors_005.phpt | 60 +++++ Zend/tests/fibers/destructors_006.phpt | 52 +++++ Zend/tests/fibers/destructors_007.phpt | 54 +++++ Zend/tests/fibers/destructors_008.phpt | 39 ++++ Zend/tests/fibers/destructors_009.phpt | 39 ++++ Zend/tests/fibers/destructors_010.phpt | 40 ++++ Zend/tests/fibers/no-switch-dtor-resume.phpt | 30 --- Zend/tests/fibers/no-switch-dtor-start.phpt | 20 -- Zend/tests/fibers/no-switch-dtor-suspend.phpt | 24 -- Zend/tests/fibers/no-switch-dtor-throw.phpt | 30 --- .../fibers/no-switch-force-close-finally.phpt | 31 --- Zend/tests/fibers/no-switch-gc.phpt | 36 --- Zend/zend.c | 1 + Zend/zend_fibers.h | 4 + Zend/zend_gc.c | 214 +++++++++++++++--- Zend/zend_gc.h | 1 + Zend/zend_objects_API.c | 6 - 22 files changed, 688 insertions(+), 210 deletions(-) create mode 100644 Zend/tests/fibers/destructors_001.phpt create mode 100644 Zend/tests/fibers/destructors_002.phpt create mode 100644 Zend/tests/fibers/destructors_003.phpt create mode 100644 Zend/tests/fibers/destructors_004.phpt create mode 100644 Zend/tests/fibers/destructors_005.phpt create mode 100644 Zend/tests/fibers/destructors_006.phpt create mode 100644 Zend/tests/fibers/destructors_007.phpt create mode 100644 Zend/tests/fibers/destructors_008.phpt create mode 100644 Zend/tests/fibers/destructors_009.phpt create mode 100644 Zend/tests/fibers/destructors_010.phpt delete mode 100644 Zend/tests/fibers/no-switch-dtor-resume.phpt delete mode 100644 Zend/tests/fibers/no-switch-dtor-start.phpt delete mode 100644 Zend/tests/fibers/no-switch-dtor-suspend.phpt delete mode 100644 Zend/tests/fibers/no-switch-dtor-throw.phpt delete mode 100644 Zend/tests/fibers/no-switch-force-close-finally.phpt delete mode 100644 Zend/tests/fibers/no-switch-gc.phpt diff --git a/Zend/tests/bug69446.phpt b/Zend/tests/bug69446.phpt index 6bc3c7006898d..f1b3b9984cb88 100644 --- a/Zend/tests/bug69446.phpt +++ b/Zend/tests/bug69446.phpt @@ -23,12 +23,12 @@ unset($foo); gc_collect_cycles(); var_dump($bar); ?> ---EXPECT-- -object(bad)#2 (2) { +--EXPECTF-- +object(bad)#%d (2) { ["x"]=> - object(stdClass)#3 (0) { + object(stdClass)#%d (0) { } ["y"]=> - object(stdClass)#4 (0) { + object(stdClass)#%d (0) { } } diff --git a/Zend/tests/fibers/destructors_001.phpt b/Zend/tests/fibers/destructors_001.phpt new file mode 100644 index 0000000000000..7a5ef8c328d89 --- /dev/null +++ b/Zend/tests/fibers/destructors_001.phpt @@ -0,0 +1,53 @@ +--TEST-- +Fibers in destructors 001: Suspend in destructor +--FILE-- +self = $this; + } + public function __destruct() { + $id = self::$counter++; + printf("%d: Start destruct\n", $id); + if ($id === 0) { + global $f2; + $f2 = Fiber::getCurrent(); + Fiber::suspend(new stdClass); + } + printf("%d: End destruct\n", $id); + } +} + +$f = new Fiber(function () { + global $f2; + new Cycle(); + new Cycle(); + new Cycle(); + new Cycle(); + new Cycle(); + gc_collect_cycles(); + $f2->resume(); +}); + +$f->start(); + +?> +--EXPECT-- +0: Start destruct +1: Start destruct +1: End destruct +2: Start destruct +2: End destruct +3: Start destruct +3: End destruct +4: Start destruct +4: End destruct +0: End destruct +Shutdown diff --git a/Zend/tests/fibers/destructors_002.phpt b/Zend/tests/fibers/destructors_002.phpt new file mode 100644 index 0000000000000..aca5cf5bfddf1 --- /dev/null +++ b/Zend/tests/fibers/destructors_002.phpt @@ -0,0 +1,35 @@ +--TEST-- +Fibers in destructors 002: Start in destructor +--FILE-- +self = $this; + } + public function __destruct() { + $id = self::$counter++; + printf("%d: Start destruct\n", $id); + $f = new Fiber(function () { }); + $f->start(); + printf("%d: End destruct\n", $id); + } +} + +new Cycle(); +new Cycle(); +gc_collect_cycles(); + +?> +--EXPECT-- +0: Start destruct +0: End destruct +1: Start destruct +1: End destruct +Shutdown diff --git a/Zend/tests/fibers/destructors_003.phpt b/Zend/tests/fibers/destructors_003.phpt new file mode 100644 index 0000000000000..84832ae62ef10 --- /dev/null +++ b/Zend/tests/fibers/destructors_003.phpt @@ -0,0 +1,42 @@ +--TEST-- +Fibers in destructors 003: Resume in destructor +--FILE-- +self = $this; + } + public function __destruct() { + $id = self::$counter++; + printf("%d: Start destruct\n", $id); + global $f; + $f->resume(); + printf("%d: End destruct\n", $id); + } +} + +$f = new Fiber(function () { + while (true) { + Fiber::suspend(); + } +}); +$f->start(); + +new Cycle(); +new Cycle(); +gc_collect_cycles(); + +?> +--EXPECT-- +0: Start destruct +0: End destruct +1: Start destruct +1: End destruct +Shutdown diff --git a/Zend/tests/fibers/destructors_004.phpt b/Zend/tests/fibers/destructors_004.phpt new file mode 100644 index 0000000000000..682d6a1243fc4 --- /dev/null +++ b/Zend/tests/fibers/destructors_004.phpt @@ -0,0 +1,79 @@ +--TEST-- +Fibers in destructors 004: Suspend and throw in destructor +--FILE-- +self = $this; + } + public function __destruct() { + $id = self::$counter++; + printf("%d: Start destruct\n", $id); + if ($id === 0) { + global $f2; + $f2 = Fiber::getCurrent(); + Fiber::suspend(new stdClass); + } + printf("%d: End destruct\n", $id); + throw new \Exception(sprintf("%d exception", $id)); + } +} + +$f = new Fiber(function () { + global $f2; + new Cycle(); + new Cycle(); + new Cycle(); + try { + gc_collect_cycles(); + } catch (\Exception $e) { + echo $e, "\n"; + } + $f2->resume(); +}); + +$f->start(); + +?> +--EXPECTF-- +0: Start destruct +1: Start destruct +1: End destruct +2: Start destruct +2: End destruct +Exception: 1 exception in %s:%d +Stack trace: +#0 [internal function]: Cycle->__destruct() +#1 [internal function]: gc_destructor_fiber() +#2 %s(%d): gc_collect_cycles() +#3 [internal function]: {closure:%s:%d}() +#4 %s(%d): Fiber->start() +#5 {main} + +Next Exception: 2 exception in %s:%d +Stack trace: +#0 [internal function]: Cycle->__destruct() +#1 [internal function]: gc_destructor_fiber() +#2 %s(%d): gc_collect_cycles() +#3 [internal function]: {closure:%s:%d}() +#4 %s(%d): Fiber->start() +#5 {main} +0: End destruct + +Fatal error: Uncaught Exception: 0 exception in %s:%d +Stack trace: +#0 [internal function]: Cycle->__destruct() +#1 [internal function]: gc_destructor_fiber() +#2 %s(%d): Fiber->resume() +#3 [internal function]: {closure:%s:%d}() +#4 %s(%d): Fiber->start() +#5 {main} + thrown in %s on line %d +Shutdown diff --git a/Zend/tests/fibers/destructors_005.phpt b/Zend/tests/fibers/destructors_005.phpt new file mode 100644 index 0000000000000..c08e82fc31a4c --- /dev/null +++ b/Zend/tests/fibers/destructors_005.phpt @@ -0,0 +1,60 @@ +--TEST-- +Fibers in destructors 005: Suspended and not resumed destructor +--FILE-- +self = $this; + } + public function __destruct() { + printf("%d: Start destruct\n", $this->id); + try { + if ($this->id === 0) { + /* Fiber will be collected by GC because it's not referenced */ + Fiber::suspend(new stdClass); + } else if ($this->id === 1) { + /* Fiber will be dtor during shutdown */ + global $f2; + $f2 = Fiber::getCurrent(); + Fiber::suspend(new stdClass); + } + } finally { + printf("%d: End destruct\n", $this->id); + } + } +} + +$refs = []; +$f = new Fiber(function () use (&$refs) { + $refs[] = WeakReference::create(new Cycle(0)); + $refs[] = WeakReference::create(new Cycle(1)); + $refs[] = WeakReference::create(new Cycle(2)); + gc_collect_cycles(); +}); + +$f->start(); + +gc_collect_cycles(); + +foreach ($refs as $id => $ref) { + printf("%d: %s\n", $id, $ref->get() ? 'Live' : 'Collected'); +} + +?> +--EXPECT-- +2: Start destruct +2: End destruct +0: Start destruct +0: End destruct +1: Start destruct +0: Collected +1: Live +2: Collected +Shutdown +1: End destruct diff --git a/Zend/tests/fibers/destructors_006.phpt b/Zend/tests/fibers/destructors_006.phpt new file mode 100644 index 0000000000000..e2c30ed1a54ee --- /dev/null +++ b/Zend/tests/fibers/destructors_006.phpt @@ -0,0 +1,52 @@ +--TEST-- +Fibers in destructors 006: multiple GC runs +--FILE-- +self = $this; + } + public function __destruct() { + $id = self::$counter++; + printf("%d: Start destruct\n", $id); + if ($id === 0) { + global $f2; + $f2 = Fiber::getCurrent(); + Fiber::suspend(new stdClass); + } + printf("%d: End destruct\n", $id); + } +} + +$f = new Fiber(function () { + new Cycle(); + new Cycle(); + gc_collect_cycles(); +}); + +$f->start(); + +new Cycle(); +new Cycle(); +gc_collect_cycles(); + +$f2->resume(); + +?> +--EXPECT-- +0: Start destruct +1: Start destruct +1: End destruct +2: Start destruct +2: End destruct +3: Start destruct +3: End destruct +0: End destruct +Shutdown diff --git a/Zend/tests/fibers/destructors_007.phpt b/Zend/tests/fibers/destructors_007.phpt new file mode 100644 index 0000000000000..35e17728e7afd --- /dev/null +++ b/Zend/tests/fibers/destructors_007.phpt @@ -0,0 +1,54 @@ +--TEST-- +Fibers in destructors 007: scope destructor +--FILE-- +start(); + break; + case 2: + global $f2; + $f2->resume(); + break; + } + printf("%d: End destruct\n", $id); + } +} + +$f = new Fiber(function () { + new Cycle(); +}); + +$f->start(); + +new Cycle(); +new Cycle(); + +?> +--EXPECT-- +0: Start destruct +1: Start destruct +1: Fiber +1: End destruct +2: Start destruct +0: End destruct +2: End destruct +Shutdown diff --git a/Zend/tests/fibers/destructors_008.phpt b/Zend/tests/fibers/destructors_008.phpt new file mode 100644 index 0000000000000..35636a16d795d --- /dev/null +++ b/Zend/tests/fibers/destructors_008.phpt @@ -0,0 +1,39 @@ +--TEST-- +Fibers in destructors 008: Fibers in shutdown sequence +--FILE-- +start(); + $f->resume(); + // Can not suspend main fiber + Fiber::suspend(); + } +} + +C::$instance = new C(); + +?> +--EXPECTF-- +Shutdown +Started +Resumed + +Fatal error: Uncaught FiberError: Cannot suspend outside of a fiber in %s:%d +Stack trace: +#0 %s(%d): Fiber::suspend() +#1 [internal function]: C->__destruct() +#2 {main} + thrown in %s on line %d diff --git a/Zend/tests/fibers/destructors_009.phpt b/Zend/tests/fibers/destructors_009.phpt new file mode 100644 index 0000000000000..5e46f934040b4 --- /dev/null +++ b/Zend/tests/fibers/destructors_009.phpt @@ -0,0 +1,39 @@ +--TEST-- +Fibers in destructors 009: Destructor resurrects object, suspends +--FILE-- +self = $this; + } + public function __destruct() { + global $ref, $f2; + $ref = $this; + $f2 = Fiber::getCurrent(); + Fiber::suspend(); + } +} + +$f = new Fiber(function () { + global $weakRef; + $weakRef = WeakReference::create(new Cycle()); + gc_collect_cycles(); +}); + +$f->start(); +var_dump((bool) $weakRef->get()); +gc_collect_cycles(); +$f2->resume(); +gc_collect_cycles(); +var_dump((bool) $weakRef->get()); +?> +--EXPECT-- +bool(true) +bool(true) +Shutdown diff --git a/Zend/tests/fibers/destructors_010.phpt b/Zend/tests/fibers/destructors_010.phpt new file mode 100644 index 0000000000000..9839bf7e09c2e --- /dev/null +++ b/Zend/tests/fibers/destructors_010.phpt @@ -0,0 +1,40 @@ +--TEST-- +Fibers in destructors 010: Destructor resurrects object, suspends, unrefs +--FILE-- +self = $this; + } + public function __destruct() { + global $ref, $f2; + $ref = $this; + $f2 = Fiber::getCurrent(); + Fiber::suspend(); + $ref = null; + } +} + +$f = new Fiber(function () { + global $weakRef; + $weakRef = WeakReference::create(new Cycle()); + gc_collect_cycles(); +}); + +$f->start(); +var_dump((bool) $weakRef->get()); +gc_collect_cycles(); +$f2->resume(); +gc_collect_cycles(); +var_dump((bool) $weakRef->get()); +?> +--EXPECT-- +bool(true) +bool(false) +Shutdown diff --git a/Zend/tests/fibers/no-switch-dtor-resume.phpt b/Zend/tests/fibers/no-switch-dtor-resume.phpt deleted file mode 100644 index 48ae34300b3ba..0000000000000 --- a/Zend/tests/fibers/no-switch-dtor-resume.phpt +++ /dev/null @@ -1,30 +0,0 @@ ---TEST-- -Cannot resume fiber within destructor ---FILE-- -start(); - -return new class ($fiber) { - private $fiber; - - public function __construct(Fiber $fiber) { - $this->fiber = $fiber; - } - - public function __destruct() { - $this->fiber->resume(1); - } -}; - -?> ---EXPECTF-- -Fatal error: Uncaught FiberError: Cannot switch fibers in current execution context in %sno-switch-dtor-resume.php:%d -Stack trace: -#0 %sno-switch-dtor-resume.php(%d): Fiber->resume(1) -#1 %sno-switch-dtor-resume.php(%d): class@anonymous->__destruct() -#2 {main} - thrown in %sno-switch-dtor-resume.php on line %d diff --git a/Zend/tests/fibers/no-switch-dtor-start.phpt b/Zend/tests/fibers/no-switch-dtor-start.phpt deleted file mode 100644 index 0d81981a1ece7..0000000000000 --- a/Zend/tests/fibers/no-switch-dtor-start.phpt +++ /dev/null @@ -1,20 +0,0 @@ ---TEST-- -Cannot start fiber within destructor ---FILE-- - null); - $fiber->start(); - } -}; - -?> ---EXPECTF-- -Fatal error: Uncaught FiberError: Cannot switch fibers in current execution context in %sno-switch-dtor-start.php:%d -Stack trace: -#0 %sno-switch-dtor-start.php(%d): Fiber->start() -#1 %sno-switch-dtor-start.php(%d): class@anonymous->__destruct() -#2 {main} - thrown in %sno-switch-dtor-start.php on line %d diff --git a/Zend/tests/fibers/no-switch-dtor-suspend.phpt b/Zend/tests/fibers/no-switch-dtor-suspend.phpt deleted file mode 100644 index 4d634f9a18ec6..0000000000000 --- a/Zend/tests/fibers/no-switch-dtor-suspend.phpt +++ /dev/null @@ -1,24 +0,0 @@ ---TEST-- -Cannot suspend fiber within destructor ---FILE-- -start(); - -?> ---EXPECTF-- -Fatal error: Uncaught FiberError: Cannot switch fibers in current execution context in %sno-switch-dtor-suspend.php:%d -Stack trace: -#0 %sno-switch-dtor-suspend.php(%d): Fiber::suspend() -#1 [internal function]: class@anonymous->__destruct() -#2 %sno-switch-dtor-suspend.php(%d): Fiber->start() -#3 {main} - thrown in %sno-switch-dtor-suspend.php on line %d diff --git a/Zend/tests/fibers/no-switch-dtor-throw.phpt b/Zend/tests/fibers/no-switch-dtor-throw.phpt deleted file mode 100644 index 127d1a74d2b3a..0000000000000 --- a/Zend/tests/fibers/no-switch-dtor-throw.phpt +++ /dev/null @@ -1,30 +0,0 @@ ---TEST-- -Cannot resume fiber within destructor ---FILE-- -start(); - -return new class ($fiber) { - private $fiber; - - public function __construct(Fiber $fiber) { - $this->fiber = $fiber; - } - - public function __destruct() { - $this->fiber->throw(new Error()); - } -}; - -?> ---EXPECTF-- -Fatal error: Uncaught FiberError: Cannot switch fibers in current execution context in %sno-switch-dtor-throw.php:%d -Stack trace: -#0 %sno-switch-dtor-throw.php(%d): Fiber->throw(Object(Error)) -#1 %sno-switch-dtor-throw.php(%d): class@anonymous->__destruct() -#2 {main} - thrown in %sno-switch-dtor-throw.php on line %d diff --git a/Zend/tests/fibers/no-switch-force-close-finally.phpt b/Zend/tests/fibers/no-switch-force-close-finally.phpt deleted file mode 100644 index 62dce4c779aa0..0000000000000 --- a/Zend/tests/fibers/no-switch-force-close-finally.phpt +++ /dev/null @@ -1,31 +0,0 @@ ---TEST-- -Cannot start a new fiber in a finally block in a force-closed fiber ---FILE-- -start(); - } -}); - -$fiber->start(); - -?> ---EXPECTF-- -finally - -Fatal error: Uncaught FiberError: Cannot switch fibers in current execution context in %sno-switch-force-close-finally.php:%d -Stack trace: -#0 %sno-switch-force-close-finally.php(%d): Fiber->start() -#1 [internal function]: {closure:%s:%d}() -#2 {main} - thrown in %sno-switch-force-close-finally.php on line %d diff --git a/Zend/tests/fibers/no-switch-gc.phpt b/Zend/tests/fibers/no-switch-gc.phpt deleted file mode 100644 index 6773edc0ff27f..0000000000000 --- a/Zend/tests/fibers/no-switch-gc.phpt +++ /dev/null @@ -1,36 +0,0 @@ ---TEST-- -Context switches are prevented during GC collect cycles ---FILE-- -next = $b; - $b->next = $a; - }); - - gc_collect_cycles(); -}); - -$fiber->start(); - -?> ---EXPECTF-- -Fatal error: Uncaught FiberError: Cannot switch fibers in current execution context in %sno-switch-gc.php:%d -Stack trace: -#0 %sno-switch-gc.php(%d): Fiber::suspend() -#1 [internal function]: class@anonymous->__destruct() -#2 %sno-switch-gc.php(%d): gc_collect_cycles() -#3 [internal function]: {closure:%s:%d}() -#4 %sno-switch-gc.php(%d): Fiber->start() -#5 {main} - thrown in %sno-switch-gc.php on line %d diff --git a/Zend/zend.c b/Zend/zend.c index efff18b7b675f..49b63e4bccccd 100644 --- a/Zend/zend.c +++ b/Zend/zend.c @@ -1128,6 +1128,7 @@ zend_result zend_post_startup(void) /* {{{ */ #ifdef ZEND_CHECK_STACK_LIMIT zend_call_stack_init(); #endif + gc_init(); return SUCCESS; } diff --git a/Zend/zend_fibers.h b/Zend/zend_fibers.h index 5c81f44a642e4..9442019bfa259 100644 --- a/Zend/zend_fibers.h +++ b/Zend/zend_fibers.h @@ -132,6 +132,10 @@ struct _zend_fiber { zval result; }; +ZEND_API zend_result zend_fiber_start(zend_fiber *fiber, zval *return_value); +ZEND_API void zend_fiber_resume(zend_fiber *fiber, zval *value, zval *return_value); +ZEND_API void zend_fiber_suspend(zend_fiber *fiber, zval *value, zval *return_value); + /* These functions may be used to create custom fiber objects using the bundled fiber switching context. */ ZEND_API zend_result zend_fiber_init_context(zend_fiber_context *context, void *kind, zend_fiber_coroutine coroutine, size_t stack_size); ZEND_API void zend_fiber_destroy_context(zend_fiber_context *context); diff --git a/Zend/zend_gc.c b/Zend/zend_gc.c index 3016ff8a1af04..30314a1e48ce6 100644 --- a/Zend/zend_gc.c +++ b/Zend/zend_gc.c @@ -68,9 +68,14 @@ */ #include "zend.h" #include "zend_API.h" +#include "zend_compile.h" +#include "zend_errors.h" #include "zend_fibers.h" #include "zend_hrtime.h" +#include "zend_portability.h" +#include "zend_types.h" #include "zend_weakrefs.h" +#include "zend_string.h" #ifndef GC_BENCH # define GC_BENCH 0 @@ -265,6 +270,11 @@ typedef struct _zend_gc_globals { zend_hrtime_t dtor_time; zend_hrtime_t free_time; + uint32_t dtor_idx; /* root buffer index */ + uint32_t dtor_end; + zend_fiber *dtor_fiber; + bool dtor_fiber_running; + #if GC_BENCH uint32_t root_buf_length; uint32_t root_buf_peak; @@ -489,6 +499,11 @@ static void gc_globals_ctor_ex(zend_gc_globals *gc_globals) gc_globals->free_time = 0; gc_globals->activated_at = 0; + gc_globals->dtor_idx = GC_FIRST_ROOT; + gc_globals->dtor_end = 0; + gc_globals->dtor_fiber = NULL; + gc_globals->dtor_fiber_running = false; + #if GC_BENCH gc_globals->root_buf_length = 0; gc_globals->root_buf_peak = 0; @@ -532,6 +547,11 @@ void gc_reset(void) GC_G(dtor_time) = 0; GC_G(free_time) = 0; + GC_G(dtor_idx) = GC_FIRST_ROOT; + GC_G(dtor_end) = 0; + GC_G(dtor_fiber) = NULL; + GC_G(dtor_fiber_running) = false; + #if GC_BENCH GC_G(root_buf_length) = 0; GC_G(root_buf_peak) = 0; @@ -1776,6 +1796,120 @@ static void zend_get_gc_buffer_release(void); static void zend_gc_check_root_tmpvars(void); static void zend_gc_remove_root_tmpvars(void); +static zend_internal_function gc_destructor_fiber; + +static ZEND_COLD ZEND_NORETURN void gc_create_destructor_fiber_error(void) +{ + zend_error_noreturn(E_ERROR, "Unable to create destructor fiber"); +} + +static ZEND_COLD ZEND_NORETURN void gc_start_destructor_fiber_error(void) +{ + zend_error_noreturn(E_ERROR, "Unable to start destructor fiber"); +} + +static zend_always_inline zend_result gc_call_destructors(uint32_t idx, uint32_t end, zend_fiber *fiber) +{ + gc_root_buffer *current; + zend_refcounted *p; + + /* The root buffer might be reallocated during destructors calls, + * make sure to reload pointers as necessary. */ + while (idx != end) { + current = GC_IDX2PTR(idx); + if (GC_IS_DTOR_GARBAGE(current->ref)) { + p = GC_GET_PTR(current->ref); + /* Mark this is as a normal root for the next GC run */ + current->ref = p; + /* Double check that the destructor hasn't been called yet. It + * could have already been invoked indirectly by some other + * destructor. */ + if (!(OBJ_FLAGS(p) & IS_OBJ_DESTRUCTOR_CALLED)) { + if (fiber != NULL) { + GC_G(dtor_idx) = idx; + } + zend_object *obj = (zend_object*)p; + GC_TRACE_REF(obj, "calling destructor"); + GC_ADD_FLAGS(obj, IS_OBJ_DESTRUCTOR_CALLED); + GC_ADDREF(obj); + obj->handlers->dtor_obj(obj); + GC_TRACE_REF(obj, "returned from destructor"); + GC_DELREF(obj); + if (UNEXPECTED(fiber != NULL && GC_G(dtor_fiber) != fiber)) { + /* We resumed after suspension */ + gc_check_possible_root((zend_refcounted*)&obj->gc); + return FAILURE; + } + } + } + idx++; + } + + return SUCCESS; +} + +static zend_fiber *gc_create_destructor_fiber(void) +{ + zval zobj; + zend_fiber *fiber; + + GC_TRACE("starting destructor fiber"); + + if (UNEXPECTED(object_init_ex(&zobj, zend_ce_fiber) == FAILURE)) { + gc_create_destructor_fiber_error(); + } + + fiber = (zend_fiber *)Z_OBJ(zobj); + fiber->fci.size = sizeof(fiber->fci); + fiber->fci_cache.function_handler = (zend_function*) &gc_destructor_fiber; + + GC_G(dtor_fiber) = fiber; + + if (UNEXPECTED(zend_fiber_start(fiber, NULL) == FAILURE)) { + gc_start_destructor_fiber_error(); + } + + return fiber; +} + +static zend_never_inline void gc_call_destructors_in_fiber(uint32_t end) +{ + ZEND_ASSERT(!GC_G(dtor_fiber_running)); + + zend_fiber *fiber = GC_G(dtor_fiber); + + GC_G(dtor_idx) = GC_FIRST_ROOT; + GC_G(dtor_end) = GC_G(first_unused); + + if (UNEXPECTED(!fiber)) { + fiber = gc_create_destructor_fiber(); + } else { + zend_fiber_resume(fiber, NULL, NULL); + } + + for (;;) { + /* At this point, fiber has executed until suspension */ + GC_TRACE("resumed from destructor fiber"); + + if (UNEXPECTED(GC_G(dtor_fiber_running))) { + /* Fiber was suspended by a destructor. Start a new one for the + * remaining destructors. */ + GC_TRACE("destructor fiber suspended by destructor"); + GC_G(dtor_fiber) = NULL; + GC_G(dtor_idx)++; + /* We do not own the fiber anymore. It may be collected if the + * application does not reference it. */ + zend_object_release(&fiber->std); + fiber = gc_create_destructor_fiber(); + continue; + } else { + /* Fiber suspended itself after calling all destructors */ + GC_TRACE("destructor fiber suspended itself"); + break; + } + } +} + ZEND_API int zend_gc_collect_cycles(void) { int total_count = 0; @@ -1824,8 +1958,6 @@ ZEND_API int zend_gc_collect_cycles(void) goto finish; } - zend_fiber_switch_block(); - end = GC_G(first_unused); if (gc_flags & GC_HAS_DESTRUCTORS) { @@ -1876,38 +2008,18 @@ ZEND_API int zend_gc_collect_cycles(void) idx++; } - /* Actually call destructors. - * - * The root buffer might be reallocated during destructors calls, - * make sure to reload pointers as necessary. */ + /* Actually call destructors. */ zend_hrtime_t dtor_start_time = zend_hrtime(); - idx = GC_FIRST_ROOT; - while (idx != end) { - current = GC_IDX2PTR(idx); - if (GC_IS_DTOR_GARBAGE(current->ref)) { - p = GC_GET_PTR(current->ref); - /* Mark this is as a normal root for the next GC run, - * it's no longer garbage for this run. */ - current->ref = p; - /* Double check that the destructor hasn't been called yet. It could have - * already been invoked indirectly by some other destructor. */ - if (!(OBJ_FLAGS(p) & IS_OBJ_DESTRUCTOR_CALLED)) { - zend_object *obj = (zend_object*)p; - GC_TRACE_REF(obj, "calling destructor"); - GC_ADD_FLAGS(obj, IS_OBJ_DESTRUCTOR_CALLED); - GC_ADDREF(obj); - obj->handlers->dtor_obj(obj); - GC_DELREF(obj); - } - } - idx++; + if (EXPECTED(!EG(active_fiber))) { + gc_call_destructors(GC_FIRST_ROOT, end, NULL); + } else { + gc_call_destructors_in_fiber(end); } GC_G(dtor_time) += zend_hrtime() - dtor_start_time; if (GC_G(gc_protected)) { /* something went wrong */ zend_get_gc_buffer_release(); - zend_fiber_switch_unblock(); GC_G(collector_time) += zend_hrtime() - start_time; return 0; } @@ -1970,8 +2082,6 @@ ZEND_API int zend_gc_collect_cycles(void) GC_G(free_time) += zend_hrtime() - free_start_time; - zend_fiber_switch_unblock(); - GC_TRACE("Collection finished"); GC_G(collected) += count; total_count += count; @@ -2125,3 +2235,49 @@ size_t zend_gc_globals_size(void) return sizeof(zend_gc_globals); } #endif + +static ZEND_FUNCTION(gc_destructor_fiber) +{ + uint32_t idx, end; + + zend_fiber *fiber = GC_G(dtor_fiber); + ZEND_ASSERT(fiber != NULL); + ZEND_ASSERT(fiber == EG(active_fiber)); + + for (;;) { + GC_G(dtor_fiber_running) = true; + + idx = GC_G(dtor_idx); + end = GC_G(dtor_end); + if (UNEXPECTED(gc_call_destructors(idx, end, fiber) == FAILURE)) { + /* We resumed after being suspended by a destructor */ + return; + } + + /* We have called all destructors. Suspend fiber until the next GC run + */ + GC_G(dtor_fiber_running) = false; + zend_fiber_suspend(fiber, NULL, NULL); + + if (UNEXPECTED(fiber->flags & ZEND_FIBER_FLAG_DESTROYED)) { + /* Fiber is being destroyed by shutdown sequence */ + GC_DELREF(&fiber->std); + gc_check_possible_root((zend_refcounted*)&fiber->std.gc); + return; + } + } +} + +static zend_internal_function gc_destructor_fiber = { + .type = ZEND_INTERNAL_FUNCTION, + .fn_flags = ZEND_ACC_PUBLIC, + .handler = ZEND_FN(gc_destructor_fiber), +}; + +void gc_init(void) +{ + gc_destructor_fiber.function_name = zend_string_init_interned( + "gc_destructor_fiber", + strlen("gc_destructor_fiber"), + true); +} diff --git a/Zend/zend_gc.h b/Zend/zend_gc.h index 84519aa68ca96..a52de1bfcfa14 100644 --- a/Zend/zend_gc.h +++ b/Zend/zend_gc.h @@ -65,6 +65,7 @@ ZEND_API int zend_gc_collect_cycles(void); ZEND_API void zend_gc_get_status(zend_gc_status *status); +void gc_init(void); void gc_globals_ctor(void); void gc_globals_dtor(void); void gc_reset(void); diff --git a/Zend/zend_objects_API.c b/Zend/zend_objects_API.c index 80f5b747db710..8a6b714c8b3fd 100644 --- a/Zend/zend_objects_API.c +++ b/Zend/zend_objects_API.c @@ -44,8 +44,6 @@ ZEND_API void ZEND_FASTCALL zend_objects_store_call_destructors(zend_objects_sto { EG(flags) |= EG_FLAGS_OBJECT_STORE_NO_REUSE; if (objects->top > 1) { - zend_fiber_switch_block(); - uint32_t i; for (i = 1; i < objects->top; i++) { zend_object *obj = objects->object_buckets[i]; @@ -62,8 +60,6 @@ ZEND_API void ZEND_FASTCALL zend_objects_store_call_destructors(zend_objects_sto } } } - - zend_fiber_switch_unblock(); } } @@ -179,11 +175,9 @@ ZEND_API void ZEND_FASTCALL zend_objects_store_del(zend_object *object) /* {{{ * if (object->handlers->dtor_obj != zend_objects_destroy_object || object->ce->destructor) { - zend_fiber_switch_block(); GC_SET_REFCOUNT(object, 1); object->handlers->dtor_obj(object); GC_DELREF(object); - zend_fiber_switch_unblock(); } }