diff --git a/_codeql_detected_source_root b/_codeql_detected_source_root new file mode 120000 index 0000000..945c9b4 --- /dev/null +++ b/_codeql_detected_source_root @@ -0,0 +1 @@ +. \ No newline at end of file diff --git a/coroutine.c b/coroutine.c index 56a7ede..9b51f8e 100644 --- a/coroutine.c +++ b/coroutine.c @@ -32,6 +32,7 @@ #include "zend_exceptions.h" #include "zend_generators.h" #include "zend_ini.h" +#include "zend_builtin_functions.h" #define METHOD(name) PHP_METHOD(Async_Coroutine, name) #define THIS_COROUTINE ((async_coroutine_t *) ZEND_ASYNC_OBJECT_TO_EVENT(Z_OBJ_P(ZEND_THIS))) @@ -1333,9 +1334,43 @@ METHOD(getException) METHOD(getTrace) { - // TODO: Implement debug trace collection - // This would require fiber stack trace functionality - array_init(return_value); + zend_long options = DEBUG_BACKTRACE_PROVIDE_OBJECT; + zend_long limit = 0; + + ZEND_PARSE_PARAMETERS_START(0, 2) + Z_PARAM_OPTIONAL + Z_PARAM_LONG(options) + Z_PARAM_LONG(limit) + ZEND_PARSE_PARAMETERS_END(); + + async_coroutine_t *coroutine = THIS_COROUTINE; + + // Return null if coroutine is not suspended + if (!ZEND_COROUTINE_SUSPENDED(&coroutine->coroutine)) { + RETURN_NULL(); + } + + async_fiber_context_t *fiber_context = coroutine->fiber_context; + + // Additional safety check for fiber context + if (fiber_context == NULL || !fiber_context->execute_data) { + RETURN_NULL(); + } + + // Switch to the coroutine's VM stack to generate the backtrace + zend_vm_stack orig_vm_stack = EG(vm_stack); + zend_execute_data *orig_execute_data = EG(current_execute_data); + + EG(vm_stack) = fiber_context->vm_stack; + EG(current_execute_data) = fiber_context->execute_data; + + // Generate the backtrace using Zend's built-in function + // skip_last = 0 (skip zero frames from the end of the trace) + zend_fetch_debug_backtrace(return_value, 0, (int)options, (int)limit); + + // Restore original VM stack and execute data + EG(vm_stack) = orig_vm_stack; + EG(current_execute_data) = orig_execute_data; } // Location Methods diff --git a/coroutine.stub.php b/coroutine.stub.php index 1893dfa..bfe3c82 100644 --- a/coroutine.stub.php +++ b/coroutine.stub.php @@ -42,8 +42,13 @@ public function getException(): mixed {} /** * Returns the Coroutine debug trace. + * If the coroutine is in the suspended state, returns a backtrace array. + * Otherwise, returns null. + * + * @param int $options Options for the backtrace (DEBUG_BACKTRACE_PROVIDE_OBJECT, DEBUG_BACKTRACE_IGNORE_ARGS) + * @param int $limit Maximum number of stack frames to return (0 for no limit) */ - public function getTrace(): array {} + public function getTrace(int $options = DEBUG_BACKTRACE_PROVIDE_OBJECT, int $limit = 0): ?array {} /** * Return spawn file and line. diff --git a/coroutine_arginfo.h b/coroutine_arginfo.h index e741424..92362d5 100644 --- a/coroutine_arginfo.h +++ b/coroutine_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit the .stub.php file instead. - * Stub hash: 866a851c4d87feafd6585532d622426dc453710f */ + * Stub hash: 2469ab708f75ecce11e559548c2462562fd39c2a */ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Async_Coroutine_getId, 0, 0, IS_LONG, 0) ZEND_END_ARG_INFO() @@ -15,7 +15,9 @@ ZEND_END_ARG_INFO() #define arginfo_class_Async_Coroutine_getException arginfo_class_Async_Coroutine_getResult -ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Async_Coroutine_getTrace, 0, 0, IS_ARRAY, 0) +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Async_Coroutine_getTrace, 0, 0, IS_ARRAY, 1) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, options, IS_LONG, 0, "DEBUG_BACKTRACE_PROVIDE_OBJECT") + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, limit, IS_LONG, 0, "0") ZEND_END_ARG_INFO() #define arginfo_class_Async_Coroutine_getSpawnFileAndLine arginfo_class_Async_Coroutine_getTrace diff --git a/tests/coroutine/009-coroutine_getTrace.phpt b/tests/coroutine/009-coroutine_getTrace.phpt index 787f2a4..103f579 100644 --- a/tests/coroutine/009-coroutine_getTrace.phpt +++ b/tests/coroutine/009-coroutine_getTrace.phpt @@ -1,20 +1,37 @@ --TEST-- -Coroutine: getTrace() - returns empty array (TODO implementation) +Coroutine: getTrace() - returns null for non-suspended coroutine --FILE-- getTrace(); +echo "Trace before start is null: " . ($traceBeforeStart === null ? "yes" : "no") . "\n"; + +// Wait for coroutine to complete +await($coroutine); + +// After completion, trace should be null $trace = $coroutine->getTrace(); +echo "Trace after completion is null: " . ($trace === null ? "yes" : "no") . "\n"; + +// Test with options parameter - should still return null +$trace2 = $coroutine->getTrace(DEBUG_BACKTRACE_IGNORE_ARGS); +echo "Trace with IGNORE_ARGS is null: " . ($trace2 === null ? "yes" : "no") . "\n"; -var_dump(is_array($trace)); -var_dump(count($trace)); +// Test with limit parameter - should still return null +$trace3 = $coroutine->getTrace(DEBUG_BACKTRACE_PROVIDE_OBJECT, 5); +echo "Trace with limit is null: " . ($trace3 === null ? "yes" : "no") . "\n"; ?> --EXPECT-- -bool(true) -int(0) \ No newline at end of file +Trace before start is null: yes +Trace after completion is null: yes +Trace with IGNORE_ARGS is null: yes +Trace with limit is null: yes \ No newline at end of file diff --git a/tests/coroutine/037-coroutine_getTrace_suspended.phpt b/tests/coroutine/037-coroutine_getTrace_suspended.phpt new file mode 100644 index 0000000..9e85a49 --- /dev/null +++ b/tests/coroutine/037-coroutine_getTrace_suspended.phpt @@ -0,0 +1,65 @@ +--TEST-- +Coroutine: getTrace() - returns backtrace for suspended coroutine +--FILE-- +getTrace(); + + if ($trace !== null) { + echo "Child trace is array: " . (is_array($trace) ? "yes" : "no") . "\n"; + echo "Child trace has entries: " . (count($trace) > 0 ? "yes" : "no") . "\n"; + + // Test with DEBUG_BACKTRACE_IGNORE_ARGS + $traceNoArgs = $childCoroutine->getTrace(DEBUG_BACKTRACE_IGNORE_ARGS); + echo "Trace with IGNORE_ARGS is array: " . (is_array($traceNoArgs) ? "yes" : "no") . "\n"; + + // Test with limit + $traceLimited = $childCoroutine->getTrace(DEBUG_BACKTRACE_PROVIDE_OBJECT, 2); + echo "Trace with limit is array: " . (is_array($traceLimited) ? "yes" : "no") . "\n"; + } else { + echo "Child trace is null\n"; + } + + // Yield control to allow scheduler to resume the child coroutine + suspend(); + + // After completion, trace should be null + $traceAfter = $childCoroutine->getTrace(); + echo "Child trace after completion is null: " . ($traceAfter === null ? "yes" : "no") . "\n"; +}); + +?> +--EXPECT-- +Parent: Starting +Child: Before suspend +Parent: Back from suspend +Child trace is array: yes +Child trace has entries: yes +Trace with IGNORE_ARGS is array: yes +Trace with limit is array: yes +Child: After suspend +Child trace after completion is null: yes