Skip to content

Commit e4130eb

Browse files
committed
support await
1 parent 086b2e0 commit e4130eb

File tree

6 files changed

+87
-6
lines changed

6 files changed

+87
-6
lines changed

demo/MyScript.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,19 @@ def _ready(self):
2929
print('==> vector2:', Vector2(1.5, 2.5).angle())
3030

3131
self.health_changed.emit(100, 200)
32+
start_coroutine(self.coro(3))
33+
34+
def coro_one_sec(self, i: int):
35+
print(f' coro_one_sec({i}) start')
36+
yield self.owner.get_tree().create_timer(1).timeout
37+
yield 3
38+
print(f' coro_one_sec({i}) end')
39+
40+
def coro(self, secs: int):
41+
print(f'coro({secs}) start')
42+
for i in range(secs):
43+
yield from self.coro_one_sec(i)
44+
print(f'coro({secs}) end')
3245

3346
def _process(self, delta: float) -> None:
3447
if Input.is_key_pressed(KEY_SPACE):

demo/main.tscn

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
1-
[gd_scene load_steps=3 format=3 uid="uid://tudbnvddbxp6"]
1+
[gd_scene format=3 uid="uid://tudbnvddbxp6"]
22

33
[ext_resource type="Script" uid="uid://dof0ku5rrw77i" path="res://new_script.gd" id="1_ig7tw"]
44
[ext_resource type="Script" uid="uid://drm3k4ac6d87p" path="res://MyScript.py" id="2_0xm2m"]
55

6-
[node name="root" type="Node2D"]
6+
[node name="root" type="Node2D" unique_id=166675096]
77

8-
[node name="gds_node" type="Node" parent="." node_paths=PackedStringArray("y")]
8+
[node name="gds_node" type="Node" parent="." unique_id=1026672657 node_paths=PackedStringArray("y")]
99
script = ExtResource("1_ig7tw")
1010
y = NodePath("../py_node")
1111
z = 4.8
1212

13-
[node name="py_node" type="Node" parent="." node_paths=PackedStringArray("y")]
13+
[node name="py_node" type="Node" parent="." unique_id=420174823 node_paths=PackedStringArray("y")]
1414
script = ExtResource("2_0xm2m")
1515
z = 4.8
1616
y = NodePath("../gds_node")

src/lang/Bindings.cpp

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,60 @@ namespace pkpy {
1313

1414
void setup_bindings_generated();
1515

16+
static bool call_next_for_coroutine(py_i64 id);
17+
18+
static void call_next_for_coroutine_no_error(py_i64 id) {
19+
py_Ref p0 = py_peek(0);
20+
bool ok = call_next_for_coroutine(id);
21+
if(!ok) log_python_error_and_clearexc(p0);
22+
}
23+
24+
static bool call_next_for_coroutine(py_i64 id) {
25+
std::thread::id current_thread_id = std::this_thread::get_id();
26+
if (current_thread_id != pyctx()->main_thread_id) {
27+
ERR_PRINT("coroutine can only be resumed in the main thread");
28+
std::abort();
29+
}
30+
py_ItemRef gen = pythreadctx()->pending_coroutines.getptr(id);
31+
if(gen == NULL) {
32+
return RuntimeError("cannot find coroutine by id: %i", id);
33+
}
34+
int res = py_next(gen);
35+
if(res == 1) {
36+
if(py_retval()->type != pyctx()->tp_Variant) {
37+
return TypeError("coroutine yielded value must be 'godot.Signal', got '%t'", py_typeof(py_retval()));
38+
}
39+
Variant v = to_variant_exact(py_retval());
40+
if(v.get_type() != Variant::SIGNAL) {
41+
CharString type_name = Variant::get_type_name(v.get_type()).utf8();
42+
return TypeError("coroutine yielded value must be 'godot.Signal', got '%s'", type_name.get_data());
43+
}
44+
Signal signal = v;
45+
Callable callable = callable_mp_static(call_next_for_coroutine_no_error);
46+
signal.connect(callable.bind(id), Object::CONNECT_ONE_SHOT | Object::CONNECT_DEFERRED);
47+
py_newnone(py_retval());
48+
return true;
49+
} else if (res == -1) {
50+
pythreadctx()->pending_coroutines.erase(id);
51+
return false; // error
52+
} else {
53+
// generator finished
54+
pythreadctx()->pending_coroutines.erase(id);
55+
py_newnone(py_retval());
56+
return true;
57+
}
58+
}
59+
60+
static void setup_awaitables() {
61+
py_bindfunc(pyctx()->godot, "start_coroutine", [](int argc, py_Ref argv) -> bool {
62+
PY_CHECK_ARGC(1);
63+
PY_CHECK_ARG_TYPE(0, tp_generator);
64+
py_i64 id = argv[0]._i64;
65+
pythreadctx()->pending_coroutines[id] = argv[0];
66+
return call_next_for_coroutine(id);
67+
});
68+
}
69+
1670
static void setup_exports() {
1771
// export
1872
pyctx()->tp_DefineStatement = py_newtype("_DefineStatement", tp_object, pyctx()->godot, [](void *ud) {
@@ -304,6 +358,9 @@ void setup_python_bindings() {
304358
#undef DEF_UNARY_OP
305359

306360
setup_bindings_generated();
361+
362+
setup_awaitables();
363+
307364
printf("==> setup_python_bindings() done!\n");
308365
}
309366

src/lang/Common.hpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ struct GDNativeClass {
105105
struct PythonThreadContext {
106106
Vector<Callable> pending_callables;
107107
Vector<std::pair<GDNativeClass, py_Name>> pending_nativecalls;
108+
HashMap<py_i64, py_TValue> pending_coroutines;
108109
};
109110

110111
struct DefineStatement {

src/lang/PythonScriptInstance.cpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,9 @@ void PythonScriptInstance::gc_mark_instances(void (*f)(py_Ref val, void *ctx), v
216216
PythonScriptInstance *instance = kv.value;
217217
f(&instance->py, ctx);
218218
}
219+
for(auto &kv: pythreadctx()->pending_coroutines) {
220+
f(&kv.value, ctx);
221+
}
219222
}
220223

221224
HashMap<Object *, PythonScriptInstance *> PythonScriptInstance::known_instances;

stubgen/map.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -541,7 +541,7 @@ def gen_alias_pyi_writer(gdt_all_in_one: GodotInOne, pyi_writer: Writer) -> Writ
541541

542542
def gen_init_pyi_writer(gdt_all_in_one: GodotInOne, pyi_writer: Writer, global_variant_classes: list[str]) -> Writer:
543543
pyi_writer.write(
544-
"""\
544+
'''\
545545
from . import classes
546546
547547
class PythonScriptInstance[T: classes.Object]:
@@ -553,7 +553,14 @@ def export[T](cls: type[T], default=None) -> T: ...
553553
def export_range[T: int | float](min: T, max: T, step: T, default: T | None = None) -> T: ...
554554
def signal(*args: str) -> classes.Signal: ...
555555
def load(path: str) -> classes.Resource: ...
556-
"""
556+
557+
def start_coroutine(gen) -> None:
558+
"""Start a coroutine. The argument should be a `generator` object.
559+
560+
- To await a `godot.Signal`, use `yield your_signal`
561+
- To await another coroutine, use `yield from another_coroutine()`
562+
"""
563+
'''
557564
)
558565

559566
writer = pyi_writer

0 commit comments

Comments
 (0)