Skip to content

Commit 054aabb

Browse files
authored
Merge pull request #75 from true-async/copilot/add-coroutine-gettrace-method
Implement Coroutine::getTrace() method for suspended coroutine backtrace inspection
2 parents bcc1f4b + bd74a3c commit 054aabb

File tree

6 files changed

+136
-11
lines changed

6 files changed

+136
-11
lines changed

_codeql_detected_source_root

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
.

coroutine.c

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
#include "zend_exceptions.h"
3333
#include "zend_generators.h"
3434
#include "zend_ini.h"
35+
#include "zend_builtin_functions.h"
3536

3637
#define METHOD(name) PHP_METHOD(Async_Coroutine, name)
3738
#define THIS_COROUTINE ((async_coroutine_t *) ZEND_ASYNC_OBJECT_TO_EVENT(Z_OBJ_P(ZEND_THIS)))
@@ -1333,9 +1334,43 @@ METHOD(getException)
13331334

13341335
METHOD(getTrace)
13351336
{
1336-
// TODO: Implement debug trace collection
1337-
// This would require fiber stack trace functionality
1338-
array_init(return_value);
1337+
zend_long options = DEBUG_BACKTRACE_PROVIDE_OBJECT;
1338+
zend_long limit = 0;
1339+
1340+
ZEND_PARSE_PARAMETERS_START(0, 2)
1341+
Z_PARAM_OPTIONAL
1342+
Z_PARAM_LONG(options)
1343+
Z_PARAM_LONG(limit)
1344+
ZEND_PARSE_PARAMETERS_END();
1345+
1346+
async_coroutine_t *coroutine = THIS_COROUTINE;
1347+
1348+
// Return null if coroutine is not suspended
1349+
if (!ZEND_COROUTINE_SUSPENDED(&coroutine->coroutine)) {
1350+
RETURN_NULL();
1351+
}
1352+
1353+
async_fiber_context_t *fiber_context = coroutine->fiber_context;
1354+
1355+
// Additional safety check for fiber context
1356+
if (fiber_context == NULL || !fiber_context->execute_data) {
1357+
RETURN_NULL();
1358+
}
1359+
1360+
// Switch to the coroutine's VM stack to generate the backtrace
1361+
zend_vm_stack orig_vm_stack = EG(vm_stack);
1362+
zend_execute_data *orig_execute_data = EG(current_execute_data);
1363+
1364+
EG(vm_stack) = fiber_context->vm_stack;
1365+
EG(current_execute_data) = fiber_context->execute_data;
1366+
1367+
// Generate the backtrace using Zend's built-in function
1368+
// skip_last = 0 (skip zero frames from the end of the trace)
1369+
zend_fetch_debug_backtrace(return_value, 0, (int)options, (int)limit);
1370+
1371+
// Restore original VM stack and execute data
1372+
EG(vm_stack) = orig_vm_stack;
1373+
EG(current_execute_data) = orig_execute_data;
13391374
}
13401375

13411376
// Location Methods

coroutine.stub.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,13 @@ public function getException(): mixed {}
4242

4343
/**
4444
* Returns the Coroutine debug trace.
45+
* If the coroutine is in the suspended state, returns a backtrace array.
46+
* Otherwise, returns null.
47+
*
48+
* @param int $options Options for the backtrace (DEBUG_BACKTRACE_PROVIDE_OBJECT, DEBUG_BACKTRACE_IGNORE_ARGS)
49+
* @param int $limit Maximum number of stack frames to return (0 for no limit)
4550
*/
46-
public function getTrace(): array {}
51+
public function getTrace(int $options = DEBUG_BACKTRACE_PROVIDE_OBJECT, int $limit = 0): ?array {}
4752

4853
/**
4954
* Return spawn file and line.

coroutine_arginfo.h

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/* This is a generated file, edit the .stub.php file instead.
2-
* Stub hash: 866a851c4d87feafd6585532d622426dc453710f */
2+
* Stub hash: 2469ab708f75ecce11e559548c2462562fd39c2a */
33

44
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Async_Coroutine_getId, 0, 0, IS_LONG, 0)
55
ZEND_END_ARG_INFO()
@@ -15,7 +15,9 @@ ZEND_END_ARG_INFO()
1515

1616
#define arginfo_class_Async_Coroutine_getException arginfo_class_Async_Coroutine_getResult
1717

18-
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Async_Coroutine_getTrace, 0, 0, IS_ARRAY, 0)
18+
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Async_Coroutine_getTrace, 0, 0, IS_ARRAY, 1)
19+
ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, options, IS_LONG, 0, "DEBUG_BACKTRACE_PROVIDE_OBJECT")
20+
ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, limit, IS_LONG, 0, "0")
1921
ZEND_END_ARG_INFO()
2022

2123
#define arginfo_class_Async_Coroutine_getSpawnFileAndLine arginfo_class_Async_Coroutine_getTrace
Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,37 @@
11
--TEST--
2-
Coroutine: getTrace() - returns empty array (TODO implementation)
2+
Coroutine: getTrace() - returns null for non-suspended coroutine
33
--FILE--
44
<?php
55

66
use function Async\spawn;
7+
use function Async\await;
78

89
$coroutine = spawn(function() {
910
return "test";
1011
});
1112

13+
// Before the coroutine starts, trace should be null
14+
$traceBeforeStart = $coroutine->getTrace();
15+
echo "Trace before start is null: " . ($traceBeforeStart === null ? "yes" : "no") . "\n";
16+
17+
// Wait for coroutine to complete
18+
await($coroutine);
19+
20+
// After completion, trace should be null
1221
$trace = $coroutine->getTrace();
22+
echo "Trace after completion is null: " . ($trace === null ? "yes" : "no") . "\n";
23+
24+
// Test with options parameter - should still return null
25+
$trace2 = $coroutine->getTrace(DEBUG_BACKTRACE_IGNORE_ARGS);
26+
echo "Trace with IGNORE_ARGS is null: " . ($trace2 === null ? "yes" : "no") . "\n";
1327

14-
var_dump(is_array($trace));
15-
var_dump(count($trace));
28+
// Test with limit parameter - should still return null
29+
$trace3 = $coroutine->getTrace(DEBUG_BACKTRACE_PROVIDE_OBJECT, 5);
30+
echo "Trace with limit is null: " . ($trace3 === null ? "yes" : "no") . "\n";
1631

1732
?>
1833
--EXPECT--
19-
bool(true)
20-
int(0)
34+
Trace before start is null: yes
35+
Trace after completion is null: yes
36+
Trace with IGNORE_ARGS is null: yes
37+
Trace with limit is null: yes
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
--TEST--
2+
Coroutine: getTrace() - returns backtrace for suspended coroutine
3+
--FILE--
4+
<?php
5+
6+
use function Async\spawn;
7+
use function Async\suspend;
8+
use function Async\await;
9+
10+
$parentCoroutine = null;
11+
$childCoroutine = null;
12+
13+
// Spawn a parent coroutine
14+
$parentCoroutine = spawn(function() use (&$childCoroutine) {
15+
echo "Parent: Starting\n";
16+
17+
// Spawn a child coroutine that will suspend
18+
$childCoroutine = spawn(function() {
19+
echo "Child: Before suspend\n";
20+
suspend();
21+
echo "Child: After suspend\n";
22+
});
23+
24+
// Suspend to let child start and suspend
25+
suspend();
26+
27+
echo "Parent: Back from suspend\n";
28+
29+
// Now check child's trace while it's suspended
30+
$trace = $childCoroutine->getTrace();
31+
32+
if ($trace !== null) {
33+
echo "Child trace is array: " . (is_array($trace) ? "yes" : "no") . "\n";
34+
echo "Child trace has entries: " . (count($trace) > 0 ? "yes" : "no") . "\n";
35+
36+
// Test with DEBUG_BACKTRACE_IGNORE_ARGS
37+
$traceNoArgs = $childCoroutine->getTrace(DEBUG_BACKTRACE_IGNORE_ARGS);
38+
echo "Trace with IGNORE_ARGS is array: " . (is_array($traceNoArgs) ? "yes" : "no") . "\n";
39+
40+
// Test with limit
41+
$traceLimited = $childCoroutine->getTrace(DEBUG_BACKTRACE_PROVIDE_OBJECT, 2);
42+
echo "Trace with limit is array: " . (is_array($traceLimited) ? "yes" : "no") . "\n";
43+
} else {
44+
echo "Child trace is null\n";
45+
}
46+
47+
// Yield control to allow scheduler to resume the child coroutine
48+
suspend();
49+
50+
// After completion, trace should be null
51+
$traceAfter = $childCoroutine->getTrace();
52+
echo "Child trace after completion is null: " . ($traceAfter === null ? "yes" : "no") . "\n";
53+
});
54+
55+
?>
56+
--EXPECT--
57+
Parent: Starting
58+
Child: Before suspend
59+
Parent: Back from suspend
60+
Child trace is array: yes
61+
Child trace has entries: yes
62+
Trace with IGNORE_ARGS is array: yes
63+
Trace with limit is array: yes
64+
Child: After suspend
65+
Child trace after completion is null: yes

0 commit comments

Comments
 (0)