diff --git a/Zend/tests/fibers/fiber-status.phpt b/Zend/tests/fibers/fiber-status.phpt index 45b2c60e057..92c37a2ef67 100644 --- a/Zend/tests/fibers/fiber-status.phpt +++ b/Zend/tests/fibers/fiber-status.phpt @@ -10,6 +10,18 @@ $fiber = new Fiber(function (): void { var_dump($fiber->isRunning()); var_dump($fiber->isSuspended()); var_dump($fiber->isTerminated()); + + $nested = new Fiber(function () use ($fiber): void { + echo "\nWithin Nested Fiber:\n"; + var_dump($fiber->isStarted()); + var_dump($fiber->isRunning()); + var_dump($fiber->isSuspended()); + var_dump($fiber->isTerminated()); + Fiber::suspend(); + }); + + $nested->start(); + Fiber::suspend(); }); @@ -49,6 +61,12 @@ bool(true) bool(false) bool(false) +Within Nested Fiber: +bool(true) +bool(true) +bool(false) +bool(false) + After Start: bool(true) bool(false) diff --git a/Zend/tests/fibers/resume-previous-fiber.phpt b/Zend/tests/fibers/resume-previous-fiber.phpt new file mode 100644 index 00000000000..bcdc11ab139 --- /dev/null +++ b/Zend/tests/fibers/resume-previous-fiber.phpt @@ -0,0 +1,28 @@ +--TEST-- +Resume previous fiber +--FILE-- +resume(); + }); + + $fiber2->start(); +}); + +$fiber->start(); + +?> +--EXPECTF-- +Fatal error: Uncaught FiberError: Cannot resume a fiber that is not suspended in %sresume-previous-fiber.php:%d +Stack trace: +#0 %sresume-previous-fiber.php(%d): Fiber->resume() +#1 [internal function]: {closure}() +#2 %sresume-previous-fiber.php(%d): Fiber->start() +#3 [internal function]: {closure}() +#4 %sresume-previous-fiber.php(%d): Fiber->start() +#5 {main} + thrown in %sresume-previous-fiber.php on line %d diff --git a/Zend/zend_fibers.c b/Zend/zend_fibers.c index e3276a28bf3..556d589dc37 100644 --- a/Zend/zend_fibers.c +++ b/Zend/zend_fibers.c @@ -192,8 +192,12 @@ static ZEND_NORETURN void zend_fiber_trampoline(transfer_t transfer) __sanitizer_finish_switch_fiber(NULL, &from->stack.prior_pointer, &from->stack.prior_size); #endif + zend_fiber_context *to = context->function(context); + + context->status = ZEND_FIBER_STATUS_DEAD; + /* Final context switch, the fiber must not be resumed afterwards! */ - zend_fiber_switch_context(context->function(context)); + zend_fiber_switch_context(to); /* Abort here because we are in an inconsistent program state. */ abort(); @@ -222,14 +226,27 @@ ZEND_API void zend_fiber_destroy_context(zend_fiber_context *context) zend_fiber_stack_free(&context->stack); } -ZEND_API void zend_fiber_switch_context(zend_fiber_context *to) +ZEND_API zend_fiber_context *zend_fiber_switch_context(zend_fiber_context *to) { zend_fiber_context *from = EG(current_fiber); + zend_fiber_vm_state state; ZEND_ASSERT(to && to->handle && "Invalid fiber context"); ZEND_ASSERT(from && "From fiber context must be present"); ZEND_ASSERT(to != from && "Cannot switch into the running fiber context"); + zend_observer_fiber_switch_notify(from, to); + + zend_fiber_capture_vm_state(&state); + + to->status = ZEND_FIBER_STATUS_RUNNING; + + if (from->status == ZEND_FIBER_STATUS_RUNNING) { + from->status = ZEND_FIBER_STATUS_SUSPENDED; + } + + EG(current_fiber) = to; + #ifdef __SANITIZE_ADDRESS__ void *fake_stack = NULL; __sanitizer_start_switch_fiber( @@ -238,50 +255,30 @@ ZEND_API void zend_fiber_switch_context(zend_fiber_context *to) to->stack.prior_size); #endif - EG(current_fiber) = to; - transfer_t transfer = jump_fcontext(to->handle, from); - ((zend_fiber_context *) transfer.data)->handle = transfer.context; - - EG(current_fiber) = from; #ifdef __SANITIZE_ADDRESS__ __sanitizer_finish_switch_fiber(fake_stack, &to->stack.prior_pointer, &to->stack.prior_size); #endif -} -static void zend_fiber_suspend_from(zend_fiber *fiber) -{ - zend_fiber_vm_state state; + EG(current_fiber) = from; - ZEND_ASSERT(fiber->caller && "Fiber has no caller"); + zend_fiber_context *previous = transfer.data; + previous->handle = transfer.context; - zend_fiber_capture_vm_state(&state); - zend_fiber_switch_context(fiber->caller); - zend_fiber_restore_vm_state(&state); -} - -static void zend_fiber_switch_to(zend_fiber *fiber) -{ - zend_fiber_context *context = zend_fiber_get_context(fiber); - zend_fiber_vm_state state; - - zend_observer_fiber_switch_notify(EG(current_fiber), context); - - fiber->caller = EG(current_fiber); - - zend_fiber_capture_vm_state(&state); - zend_fiber_switch_context(context); zend_fiber_restore_vm_state(&state); - fiber->caller = NULL; + /* Destroy context first to ensure it does not leak if some extension does custom bailout handling. */ + if (previous->status == ZEND_FIBER_STATUS_DEAD) { + zend_fiber_destroy_context(previous); + } - zend_observer_fiber_switch_notify(context, EG(current_fiber)); - - if (UNEXPECTED(fiber->flags & ZEND_FIBER_FLAG_BAILOUT)) { - // zend_bailout() was called in the fiber, so call it again in the previous fiber or {main}. + /* Propagate bailout to current fiber / main. */ + if (UNEXPECTED(previous->flags & ZEND_FIBER_FLAG_BAILOUT)) { zend_bailout(); } + + return previous; } @@ -328,8 +325,6 @@ static ZEND_STACK_ALIGNED zend_fiber_context *zend_fiber_execute(zend_fiber_cont fiber->fci.retval = &fiber->value; - fiber->status = ZEND_FIBER_STATUS_RUNNING; - zend_call_function(&fiber->fci, &fiber->fci_cache); zval_ptr_dtor(&fiber->fci.function_name); @@ -347,13 +342,14 @@ static ZEND_STACK_ALIGNED zend_fiber_context *zend_fiber_execute(zend_fiber_cont fiber->flags |= ZEND_FIBER_FLAG_BAILOUT; } zend_end_try(); - fiber->status = ZEND_FIBER_STATUS_DEAD; - zend_vm_stack_destroy(); fiber->execute_data = NULL; fiber->stack_bottom = NULL; - return fiber->caller; + zend_fiber_context *caller = fiber->caller; + fiber->caller = NULL; + + return caller; } static zend_object *zend_fiber_object_create(zend_class_entry *ce) @@ -380,10 +376,10 @@ static void zend_fiber_object_destroy(zend_object *object) zend_object *exception = EG(exception); EG(exception) = NULL; - fiber->status = ZEND_FIBER_STATUS_RUNNING; + fiber->caller = EG(current_fiber); fiber->flags |= ZEND_FIBER_FLAG_DESTROYED; - zend_fiber_switch_to(fiber); + zend_fiber_switch_context(zend_fiber_get_context(fiber)); if (EG(exception)) { if (!exception && EG(current_execute_data) && EG(current_execute_data)->func @@ -412,23 +408,9 @@ static void zend_fiber_object_free(zend_object *object) zval_ptr_dtor(&fiber->value); - zend_fiber_destroy_context(zend_fiber_get_context(fiber)); - zend_object_std_dtor(&fiber->std); } -ZEND_API zend_fiber *zend_fiber_create(const zend_fcall_info *fci, const zend_fcall_info_cache *fci_cache) -{ - zend_fiber *fiber = (zend_fiber *) zend_fiber_object_create(zend_ce_fiber); - - fiber->fci = *fci; - fiber->fci_cache = *fci_cache; - - Z_TRY_ADDREF(fiber->fci.function_name); - - return fiber; -} - ZEND_METHOD(Fiber, __construct) { zend_fiber *fiber = (zend_fiber *) Z_OBJ_P(getThis()); @@ -441,31 +423,6 @@ ZEND_METHOD(Fiber, __construct) Z_TRY_ADDREF(fiber->fci.function_name); } -ZEND_API void zend_fiber_start(zend_fiber *fiber, zval *params, uint32_t param_count, zend_array *named_params, zval *return_value) -{ - if (fiber->status != ZEND_FIBER_STATUS_INIT) { - zend_throw_error(zend_ce_fiber_error, "Cannot start a fiber that has already been started"); - RETURN_THROWS(); - } - - fiber->fci.params = params; - fiber->fci.param_count = param_count; - fiber->fci.named_params = named_params; - - if (!zend_fiber_init_context(zend_fiber_get_context(fiber), zend_ce_fiber, zend_fiber_execute, EG(fiber_stack_size))) { - RETURN_THROWS(); - } - - zend_fiber_switch_to(fiber); - - if (fiber->status == ZEND_FIBER_STATUS_DEAD) { - RETURN_NULL(); - } - - RETVAL_COPY_VALUE(&fiber->value); - ZVAL_UNDEF(&fiber->value); -} - ZEND_METHOD(Fiber, start) { zend_fiber *fiber = (zend_fiber *) Z_OBJ_P(getThis()); @@ -477,51 +434,27 @@ ZEND_METHOD(Fiber, start) Z_PARAM_VARIADIC_WITH_NAMED(params, param_count, named_params); ZEND_PARSE_PARAMETERS_END(); - zend_fiber_start(fiber, params, param_count, named_params, return_value); -} - -ZEND_API void zend_fiber_suspend(zval *value, zval *return_value) -{ - if (UNEXPECTED(EG(current_fiber)->kind != zend_ce_fiber)) { - zend_throw_error(zend_ce_fiber_error, "Cannot suspend outside of a fiber"); + if (fiber->status != ZEND_FIBER_STATUS_INIT) { + zend_throw_error(zend_ce_fiber_error, "Cannot start a fiber that has already been started"); RETURN_THROWS(); } - zend_fiber *fiber = zend_fiber_from_context(EG(current_fiber)); + fiber->fci.params = params; + fiber->fci.param_count = param_count; + fiber->fci.named_params = named_params; - if (UNEXPECTED(fiber->flags & ZEND_FIBER_FLAG_DESTROYED)) { - zend_throw_error(zend_ce_fiber_error, "Cannot suspend in a force-closed fiber"); + zend_fiber_context *context = zend_fiber_get_context(fiber); + + if (!zend_fiber_init_context(context, zend_ce_fiber, zend_fiber_execute, EG(fiber_stack_size))) { RETURN_THROWS(); } - ZEND_ASSERT(fiber->status == ZEND_FIBER_STATUS_RUNNING); + fiber->caller = EG(current_fiber); - if (value) { - ZVAL_COPY(&fiber->value, value); - } else { - ZVAL_NULL(&fiber->value); - } + zend_fiber_switch_context(context); - fiber->execute_data = EG(current_execute_data); - fiber->status = ZEND_FIBER_STATUS_SUSPENDED; - fiber->stack_bottom->prev_execute_data = NULL; - - zend_fiber_suspend_from(fiber); - - if (fiber->flags & ZEND_FIBER_FLAG_DESTROYED) { - // This occurs when the fiber is GC'ed while suspended. - zend_throw_graceful_exit(); - RETURN_THROWS(); - } - - fiber->status = ZEND_FIBER_STATUS_RUNNING; - - if (fiber->exception) { - zval *exception = fiber->exception; - fiber->exception = NULL; - - zend_throw_exception_object(exception); - RETURN_THROWS(); + if (fiber->status == ZEND_FIBER_STATUS_DEAD) { + RETURN_NULL(); } RETVAL_COPY_VALUE(&fiber->value); @@ -537,29 +470,46 @@ ZEND_METHOD(Fiber, suspend) Z_PARAM_ZVAL(value); ZEND_PARSE_PARAMETERS_END(); - zend_fiber_suspend(value, return_value); -} - -ZEND_API void zend_fiber_resume(zend_fiber *fiber, zval *value, zval *return_value) -{ - if (UNEXPECTED(fiber->status != ZEND_FIBER_STATUS_SUSPENDED)) { - zend_throw_error(zend_ce_fiber_error, "Cannot resume a fiber that is not suspended"); + if (UNEXPECTED(EG(current_fiber)->kind != zend_ce_fiber)) { + zend_throw_error(zend_ce_fiber_error, "Cannot suspend outside of a fiber"); RETURN_THROWS(); } + zend_fiber *fiber = zend_fiber_from_context(EG(current_fiber)); + zend_fiber_context *caller = fiber->caller; + + if (UNEXPECTED(fiber->flags & ZEND_FIBER_FLAG_DESTROYED)) { + zend_throw_error(zend_ce_fiber_error, "Cannot suspend in a force-closed fiber"); + RETURN_THROWS(); + } + + ZEND_ASSERT(fiber->status == ZEND_FIBER_STATUS_RUNNING); + ZEND_ASSERT(caller != NULL); + if (value) { ZVAL_COPY(&fiber->value, value); } else { ZVAL_NULL(&fiber->value); } - fiber->status = ZEND_FIBER_STATUS_RUNNING; - fiber->stack_bottom->prev_execute_data = EG(current_execute_data); + fiber->caller = NULL; + fiber->execute_data = EG(current_execute_data); + fiber->stack_bottom->prev_execute_data = NULL; - zend_fiber_switch_to(fiber); + zend_fiber_switch_context(caller); - if (fiber->status == ZEND_FIBER_STATUS_DEAD) { - RETURN_NULL(); + if (fiber->flags & ZEND_FIBER_FLAG_DESTROYED) { + // This occurs when the fiber is GC'ed while suspended. + zend_throw_graceful_exit(); + RETURN_THROWS(); + } + + if (fiber->exception) { + zval *exception = fiber->exception; + fiber->exception = NULL; + + zend_throw_exception_object(exception); + RETURN_THROWS(); } RETVAL_COPY_VALUE(&fiber->value); @@ -578,23 +528,21 @@ ZEND_METHOD(Fiber, resume) fiber = (zend_fiber *) Z_OBJ_P(getThis()); - zend_fiber_resume(fiber, value, return_value); -} - -ZEND_API void zend_fiber_throw(zend_fiber *fiber, zval *exception, zval *return_value) -{ - if (UNEXPECTED(fiber->status != ZEND_FIBER_STATUS_SUSPENDED)) { + if (UNEXPECTED(fiber->status != ZEND_FIBER_STATUS_SUSPENDED || fiber->caller != NULL)) { zend_throw_error(zend_ce_fiber_error, "Cannot resume a fiber that is not suspended"); RETURN_THROWS(); } - Z_ADDREF_P(exception); - fiber->exception = exception; + if (value) { + ZVAL_COPY(&fiber->value, value); + } else { + ZVAL_NULL(&fiber->value); + } - fiber->status = ZEND_FIBER_STATUS_RUNNING; + fiber->caller = EG(current_fiber); fiber->stack_bottom->prev_execute_data = EG(current_execute_data); - zend_fiber_switch_to(fiber); + zend_fiber_switch_context(zend_fiber_get_context(fiber)); if (fiber->status == ZEND_FIBER_STATUS_DEAD) { RETURN_NULL(); @@ -615,7 +563,25 @@ ZEND_METHOD(Fiber, throw) fiber = (zend_fiber *) Z_OBJ_P(getThis()); - zend_fiber_throw(fiber, exception, return_value); + if (UNEXPECTED(fiber->status != ZEND_FIBER_STATUS_SUSPENDED || fiber->caller != NULL)) { + zend_throw_error(zend_ce_fiber_error, "Cannot resume a fiber that is not suspended"); + RETURN_THROWS(); + } + + Z_ADDREF_P(exception); + fiber->exception = exception; + + fiber->caller = EG(current_fiber); + fiber->stack_bottom->prev_execute_data = EG(current_execute_data); + + zend_fiber_switch_context(zend_fiber_get_context(fiber)); + + if (fiber->status == ZEND_FIBER_STATUS_DEAD) { + RETURN_NULL(); + } + + RETVAL_COPY_VALUE(&fiber->value); + ZVAL_UNDEF(&fiber->value); } ZEND_METHOD(Fiber, isStarted) @@ -637,7 +603,7 @@ ZEND_METHOD(Fiber, isSuspended) fiber = (zend_fiber *) Z_OBJ_P(getThis()); - RETURN_BOOL(fiber->status == ZEND_FIBER_STATUS_SUSPENDED); + RETURN_BOOL(fiber->status == ZEND_FIBER_STATUS_SUSPENDED && fiber->caller == NULL); } ZEND_METHOD(Fiber, isRunning) @@ -648,7 +614,7 @@ ZEND_METHOD(Fiber, isRunning) fiber = (zend_fiber *) Z_OBJ_P(getThis()); - RETURN_BOOL(fiber->status == ZEND_FIBER_STATUS_RUNNING); + RETURN_BOOL(fiber->status == ZEND_FIBER_STATUS_RUNNING || fiber->caller != NULL); } ZEND_METHOD(Fiber, isTerminated) diff --git a/Zend/zend_fibers.h b/Zend/zend_fibers.h index df909b55c4e..940f03c6d38 100644 --- a/Zend/zend_fibers.h +++ b/Zend/zend_fibers.h @@ -89,6 +89,8 @@ struct _zend_fiber_context { /* Zend VM state that needs to be captured / restored during fiber context switch. */ typedef struct _zend_fiber_vm_state { zend_vm_stack vm_stack; + zval *vm_stack_top; + zval *vm_stack_end; size_t vm_stack_page_size; zend_execute_data *current_execute_data; int error_reporting; @@ -100,12 +102,12 @@ struct _zend_fiber { /* PHP object handle. */ zend_object std; - /* Fiber that resumed us. */ - zend_fiber_context *caller; - /* Fiber context fields (embedded to avoid memory allocation). */ ZEND_FIBER_CONTEXT_FIELDS; + /* Fiber that resumed us. */ + zend_fiber_context *caller; + /* Callback and info / cache to be used when fiber is started. */ zend_fcall_info fci; zend_fcall_info_cache fci_cache; @@ -123,17 +125,10 @@ struct _zend_fiber { zval value; }; -/* These functions create and manipulate a Fiber object, allowing any internal function to start, resume, or suspend a fiber. */ -ZEND_API zend_fiber *zend_fiber_create(const zend_fcall_info *fci, const zend_fcall_info_cache *fci_cache); -ZEND_API void zend_fiber_start(zend_fiber *fiber, zval *params, uint32_t param_count, zend_array *named_params, zval *return_value); -ZEND_API void zend_fiber_suspend(zval *value, zval *return_value); -ZEND_API void zend_fiber_resume(zend_fiber *fiber, zval *value, zval *return_value); -ZEND_API void zend_fiber_throw(zend_fiber *fiber, zval *exception, zval *return_value); - -/* These functions may be used to create custom fibers (coroutines) using the bundled fiber switching context. */ +/* These functions may be used to create custom fiber objects using the bundled fiber switching context. */ ZEND_API bool 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); -ZEND_API void zend_fiber_switch_context(zend_fiber_context *to); +ZEND_API zend_fiber_context *zend_fiber_switch_context(zend_fiber_context *to); END_EXTERN_C() @@ -152,8 +147,8 @@ static zend_always_inline zend_fiber_context *zend_fiber_get_context(zend_fiber static zend_always_inline void zend_fiber_capture_vm_state(zend_fiber_vm_state *state) { state->vm_stack = EG(vm_stack); - state->vm_stack->top = EG(vm_stack_top); - state->vm_stack->end = EG(vm_stack_end); + state->vm_stack_top = EG(vm_stack_top); + state->vm_stack_end = EG(vm_stack_end); state->vm_stack_page_size = EG(vm_stack_page_size); state->current_execute_data = EG(current_execute_data); state->error_reporting = EG(error_reporting); @@ -164,8 +159,8 @@ static zend_always_inline void zend_fiber_capture_vm_state(zend_fiber_vm_state * static zend_always_inline void zend_fiber_restore_vm_state(zend_fiber_vm_state *state) { EG(vm_stack) = state->vm_stack; - EG(vm_stack_top) = state->vm_stack->top; - EG(vm_stack_end) = state->vm_stack->end; + EG(vm_stack_top) = state->vm_stack_top; + EG(vm_stack_end) = state->vm_stack_end; EG(vm_stack_page_size) = state->vm_stack_page_size; EG(current_execute_data) = state->current_execute_data; EG(error_reporting) = state->error_reporting; diff --git a/ext/zend_test/test.c b/ext/zend_test/test.c index af4fb0950df..c4c42221c07 100644 --- a/ext/zend_test/test.c +++ b/ext/zend_test/test.c @@ -436,7 +436,12 @@ static void fiber_enter_observer(zend_fiber_context *from, zend_fiber_context *t if (ZT_G(observer_fiber_switch)) { if (to->status == ZEND_FIBER_STATUS_INIT) { php_printf("\n", to); - } else if (to->status == ZEND_FIBER_STATUS_RUNNING && from->status == ZEND_FIBER_STATUS_RUNNING) { + } else if (to->kind == zend_ce_fiber) { + zend_fiber *fiber = zend_fiber_from_context(to); + if (fiber->caller != from) { + return; + } + if (to->flags & ZEND_FIBER_FLAG_DESTROYED) { php_printf("\n", to); } else if (to->status != ZEND_FIBER_STATUS_DEAD) { @@ -449,9 +454,7 @@ static void fiber_enter_observer(zend_fiber_context *from, zend_fiber_context *t static void fiber_suspend_observer(zend_fiber_context *from, zend_fiber_context *to) { if (ZT_G(observer_fiber_switch)) { - if (from->status == ZEND_FIBER_STATUS_SUSPENDED) { - php_printf("\n", from); - } else if (from->status == ZEND_FIBER_STATUS_DEAD) { + if (from->status == ZEND_FIBER_STATUS_DEAD) { if (from->flags & ZEND_FIBER_FLAG_THREW) { php_printf("\n", from); } else if (from->flags & ZEND_FIBER_FLAG_DESTROYED) { @@ -459,6 +462,11 @@ static void fiber_suspend_observer(zend_fiber_context *from, zend_fiber_context } else { php_printf("\n", from); } + } else if (from->kind == zend_ce_fiber) { + zend_fiber *fiber = zend_fiber_from_context(from); + if (fiber->caller == NULL) { + php_printf("\n", from); + } } } }