Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions _codeql_detected_source_root
41 changes: 38 additions & 3 deletions coroutine.c
Original file line number Diff line number Diff line change
Expand Up @@ -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)))
Expand Down Expand Up @@ -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
Expand Down
7 changes: 6 additions & 1 deletion coroutine.stub.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
6 changes: 4 additions & 2 deletions coroutine_arginfo.h
Original file line number Diff line number Diff line change
@@ -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()
Expand All @@ -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
Expand Down
27 changes: 22 additions & 5 deletions tests/coroutine/009-coroutine_getTrace.phpt
Original file line number Diff line number Diff line change
@@ -1,20 +1,37 @@
--TEST--
Coroutine: getTrace() - returns empty array (TODO implementation)
Coroutine: getTrace() - returns null for non-suspended coroutine
--FILE--
<?php

use function Async\spawn;
use function Async\await;

$coroutine = spawn(function() {
return "test";
});

// Before the coroutine starts, trace should be null
$traceBeforeStart = $coroutine->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)
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
65 changes: 65 additions & 0 deletions tests/coroutine/037-coroutine_getTrace_suspended.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
--TEST--
Coroutine: getTrace() - returns backtrace for suspended coroutine
--FILE--
<?php

use function Async\spawn;
use function Async\suspend;
use function Async\await;

$parentCoroutine = null;
$childCoroutine = null;

// Spawn a parent coroutine
$parentCoroutine = spawn(function() use (&$childCoroutine) {
echo "Parent: Starting\n";

// Spawn a child coroutine that will suspend
$childCoroutine = spawn(function() {
echo "Child: Before suspend\n";
suspend();
echo "Child: After suspend\n";
});

// Suspend to let child start and suspend
suspend();

echo "Parent: Back from suspend\n";

// Now check child's trace while it's suspended
$trace = $childCoroutine->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
Loading