Skip to content

Commit eab16c9

Browse files
committed
Add vararg support for builtins methods
1 parent 1d1e6c2 commit eab16c9

File tree

5 files changed

+249
-220
lines changed

5 files changed

+249
-220
lines changed

src/godot/builtins.pxd.j2

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
{% from 'builtins_pxd/class.pxd.j2' import render_class with context %}
22
{% from 'builtins_pxd/conversion.pxd.j2' import render_all_conversions with context %}
33
cimport cython
4+
from cpython.mem cimport PyMem_Malloc, PyMem_Free
45

56
# Forward declarations
67
{% for builtin in api.builtins %}

src/godot/builtins.pyi.j2

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,11 @@ class {{ builtin.cy_type }}:
9696
# Methods
9797
{% endif %}
9898
{% for m in builtin.methods %}
99+
{% if m.is_vararg %}
100+
def {{m.name}}(self, *args: IntoGDAny) -> {{ "None" if m.return_type.is_nil else m.return_type.py_type }}:
101+
{% else %}
99102
def {{m.name}}(self{% for a in m.arguments %}, {{ render_arg(a) }}{% endfor %}) -> {{ "None" if m.return_type.is_nil else m.return_type.py_type }}:
103+
{% endif %}
100104
{% if m.description %}
101105
"""{{ m.description | gd_description_to_py_doc }}"""
102106
{% endif %}

src/godot/builtins.pyx.j2

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
{%- from 'builtins_pyx/class.pyx.j2' import render_class with context -%}
22
{%- from 'builtins_pyx/conversion.pyx.j2' import render_all_conversions with context -%}
33
cimport cython
4+
from cpython.mem cimport PyMem_Malloc, PyMem_Free
45
from libc.math cimport INFINITY as inf # Needed by some constants
56
from libc.string cimport memset # Needed to create zeroized builtins used in `__bool__`
67

Lines changed: 123 additions & 115 deletions
Original file line numberDiff line numberDiff line change
@@ -1,166 +1,174 @@
1+
{#
2+
Note we don't use `cpdef` here since Cython would then add the Python flavour of
3+
those methods to any .pyx that include `builtins.pxd`.
4+
Worst: this would also make the compiler consider all `gdptr_*` symbols in
5+
`builtins.so` are used, hence bloating the final shared library of tons of useless
6+
symbols to resolve at runtime !
7+
#}
8+
9+
110
{% macro render_method_signature(m) -%}
211
{# TODO: hack given currently event `GDObject` is lazy binded, so we have to use its parent `BaseGDObject` here... #}
312
{%- if m.return_type.is_nil -%}void{%- else -%}{{ "BaseGDObject" if m.return_type.cy_type == "GDObject" else m.return_type.cy_type }}{%- endif %}
4-
{{ "_" + m.name if m.is_static else m.name }}(
13+
{{ m.name }}(
514
{%- if not m.is_static -%}self, {% endif -%}
6-
{%- for arg in m.arguments -%}
7-
{%- if arg.type.original_name in ("String", "StringName", "NodePath") -%}
8-
object {{ arg.name }}
15+
{%- if m.is_vararg -%}
16+
{# cdef function doesn't support `*args` so we have to explicitly pass an iterable here instead #}
17+
object args
918
{%- else -%}
19+
{%- for arg in m.arguments -%}
20+
{%- if arg.type.original_name in ("String", "StringName", "NodePath") -%}
21+
object {{ arg.name }}
22+
{%- else -%}
1023
{{ arg.type.cy_type }} {{ arg.name }}
11-
{%- endif -%}
12-
{%- if arg.default_value -%}
24+
{%- endif -%}
25+
{%- if arg.default_value -%}
1326
={{ arg.default_value.cy_value or "None" }}
27+
{%- endif -%}
28+
{%- if not loop.last -%}, {% endif -%}
29+
{%- endfor -%}
1430
{%- endif -%}
15-
{%- if not loop.last -%}, {% endif -%}
16-
{%- endfor -%})
31+
)
1732
{%- endmacro %}
1833

1934

20-
{% macro _render_non_static_method(builtin, m) %}
21-
{#
22-
Note we don't use `cpdef` here since Cython would then add the Python flavour of
23-
those methods to any .pyx that include `builtins.pxd`.
24-
Worst: this would also make the compiler consider all `gdptr_*` symbols in
25-
`builtins.so` are used, hence bloating the final shared library of tons of useless
26-
symbols to resolve at runtime !
27-
28-
So instead we define each method twice:
29-
- Once here that will become a static inline C function. Hence the compiler
30-
will add it to any module importing `builtins.pxd` and discard automatically
31-
if it is in fact never used in the module.
32-
- Once in `builtins.pyx` as a regular Python method.
33-
#}
34-
cdef inline {{ render_method_signature(m) }}:
35-
{% if m.contains_unsuported_types %}
36-
raise NotImplementedError # TODO
37-
{% else %}
38-
{% for arg in m.arguments %}
39-
{% if arg.type.is_variant %}
40-
cdef gd_variant_t __arg{{ loop.index0 }}
41-
__arg{{ loop.index0 }} = ensure_is_gdany_and_borrow_ref({{ arg.name }})
42-
{% elif arg.type.original_name == "String" %}
43-
cdef GDString __arg{{ loop.index0 }}
44-
__arg{{ loop.index0 }} = ensure_is_gdstring({{ arg.name }})
45-
{% elif arg.type.original_name == "StringName" %}
46-
cdef StringName __arg{{ loop.index0 }}
47-
__arg{{ loop.index0 }} = ensure_is_stringname({{ arg.name }})
48-
{% elif arg.type.original_name == "NodePath" %}
49-
cdef NodePath __arg{{ loop.index0 }}
50-
__arg{{ loop.index0 }} = ensure_is_nodepath({{ arg.name }})
35+
{% macro _render_method_vararg(builtin, m) %}
36+
{% if m.is_static %}
37+
@staticmethod
5138
{% endif %}
52-
{% endfor %}
53-
{% if m.return_type.is_scalar %}
39+
cdef inline {{ render_method_signature(m) }}:
40+
{# 1) Param cooking #}
41+
cdef gd_variant_t **gd_args_ptrs
42+
cdef gd_variant_t[8] gd_args_stack
43+
cdef (gd_variant_t *)[8] gd_args_ptrs_stack
44+
cdef gd_variant_t *gd_args_heap
45+
cdef gd_variant_t **gd_args_ptrs_heap
46+
cdef bint stack_args_passing = len(args) <= 8
47+
if stack_args_passing:
48+
for i, arg in enumerate(args):
49+
gd_args_stack[i] = ensure_is_gdany_and_borrow_ref(arg)
50+
gd_args_ptrs_stack[i] = &gd_args_stack[i]
51+
gd_args_ptrs = <gd_variant_t **>&gd_args_ptrs_stack
52+
else:
53+
gd_args_heap = <gd_variant_t*>PyMem_Malloc(sizeof(gd_variant_t) * len(args))
54+
gd_args_ptrs_heap = <gd_variant_t**>PyMem_Malloc(sizeof(gd_variant_t*) * len(args))
55+
for i, arg in enumerate(args):
56+
gd_args_heap[i] = ensure_is_gdany_and_borrow_ref(arg)
57+
gd_args_ptrs_heap[i] = &gd_args_heap[i]
58+
gd_args_ptrs = gd_args_ptrs_heap
59+
{# 2) Actual function call with return value initialization #}
60+
{% if m.return_type.is_scalar %}
5461
cdef {{m.return_type.c_type }} ret = gdapi.gd_{{ builtin.snake_name }}_meth_{{ m.name }}(
55-
{% elif m.return_type.is_builtin %}
62+
{% elif m.return_type.is_builtin %}
5663
# Call to __new__ bypasses __init__ constructor
5764
cdef {{ m.return_type.cy_type }} ret = {{ m.return_type.cy_type }}.__new__({{ m.return_type.cy_type }})
5865
ret._gd_data = gdapi.gd_{{ builtin.snake_name }}_meth_{{ m.name }}(
59-
{% elif m.return_type.is_object or m.return_type.is_variant %}
66+
{% elif m.return_type.is_object or m.return_type.is_variant %}
6067
cdef {{ m.return_type.c_type }} ret = gdapi.gd_{{ builtin.snake_name }}_meth_{{ m.name }}(
61-
{% else %}
68+
{% else %}
6269
gdapi.gd_{{ builtin.snake_name }}_meth_{{ m.name }}(
63-
{% endif %}
70+
{% endif %}
71+
{% if m.is_static %}
72+
NULL,
73+
{% else %}
6474
&self._gd_data,
65-
{% for arg in m.arguments %}
66-
{% if arg.type.is_scalar %}
67-
{{ arg.name }},
68-
{% elif arg.type.original_name in ("String", "StringName", "NodePath") %}
69-
&__arg{{ loop.index0 }}._gd_data,
70-
{% elif arg.type.is_builtin %}
71-
&{{ arg.name }}._gd_data,
72-
{% elif arg.type.is_object %}
73-
&{{ arg.name }}._gd_ptr,
74-
{% elif arg.type.is_variant %}
75-
&__arg{{ loop.index0 }},
76-
{% else %}
77-
<crash !!!!> # {{ arg.type }} Unsupported type, crash the compilation !
78-
{% endif %}
79-
{% endfor %}
75+
{% endif %}
76+
{# 3) Parameter provided to the function call #}
77+
gd_args_ptrs,
78+
len(args),
8079
)
81-
{% if not m.return_type.is_nil %}
82-
{% if m.return_type.is_object %}
80+
# Note we don't need to destroy the variants since they have only borrowed their data from the PyObjects
81+
if not stack_args_passing:
82+
PyMem_Free(gd_args_heap)
83+
PyMem_Free(gd_args_ptrs_heap)
84+
{# 4) Optional return value conversion #}
85+
{% if not m.return_type.is_nil %}
86+
{% if m.return_type.is_object %}
8387
return BaseGDObject.cast_from_object(ret)
84-
{% elif m.return_type.is_variant %}
85-
# Note `gd_variant_steal_into_pyobj(&ret)` calls `ret`'s destructor
88+
{% elif m.return_type.is_variant %}
89+
# Note the "steal" means we don't need a final `gdapi.gd_variant_del(&ret)`
8690
return gd_variant_steal_into_pyobj(&ret)
87-
{% else %}
91+
{% else %}
8892
return ret
89-
{% endif %}
90-
{% endif %}
9193
{% endif %}
94+
{% endif %}
9295
{% endmacro %}
9396

9497

95-
{# TODO: Cython currently doesn't support cpdef with @staticmethod #}
96-
{# see: https://github.com/cython/cython/issues/3327 #}
97-
{% macro _render_static_method(builtin, m) %}
98+
{% macro _render_method_standard(builtin, m) %}
99+
{% if m.is_static %}
98100
@staticmethod
99-
cdef inline {{ render_method_signature(m) }}:
100-
{% if m.contains_unsuported_types %}
101-
raise NotImplementedError # TODO
102-
{% else %}
103-
{% for arg in m.arguments %}
104-
{% if arg.type.is_variant %}
105-
cdef gd_variant_t __arg{{ loop.index0 }}
106-
__arg{{ loop.index0 }} = ensure_is_gdany_and_borrow_ref({{ arg.name }})
107-
{% elif arg.type.original_name == "String" %}
108-
cdef GDString __arg{{ loop.index0 }}
109-
__arg{{ loop.index0 }} = ensure_is_gdstring({{ arg.name }})
110-
{% elif arg.type.original_name == "StringName" %}
111-
cdef StringName __arg{{ loop.index0 }}
112-
__arg{{ loop.index0 }} = ensure_is_stringname({{ arg.name }})
113-
{% elif arg.type.original_name == "NodePath" %}
114-
cdef NodePath __arg{{ loop.index0 }}
115-
__arg{{ loop.index0 }} = ensure_is_nodepath({{ arg.name }})
116101
{% endif %}
102+
cdef inline {{ render_method_signature(m) }}:
103+
{# 1) Param cooking #}
104+
{% for arg in m.arguments %}
105+
{% if arg.type.is_variant %}
106+
{# Note the `__` prefix in those variables to avoid clashing with the arguments #}
107+
cdef gd_variant_t __arg_{{ arg.name }}
108+
__arg_{{ arg.name }} = ensure_is_gdany_and_borrow_ref({{ arg.name }})
109+
{% elif arg.type.original_name == "String" %}
110+
cdef GDString __arg_{{ arg.name }}
111+
__arg_{{ arg.name }} = ensure_is_gdstring({{ arg.name }})
112+
{% elif arg.type.original_name == "StringName" %}
113+
cdef StringName __arg_{{ arg.name }}
114+
__arg_{{ arg.name }} = ensure_is_stringname({{ arg.name }})
115+
{% elif arg.type.original_name == "NodePath" %}
116+
cdef NodePath __arg_{{ arg.name }}
117+
__arg_{{ arg.name }} = ensure_is_nodepath({{ arg.name }})
118+
{% endif %}
117119
{% endfor %}
118-
{% if m.return_type.is_scalar %}
119-
cdef {{ m.return_type.cy_type }} ret = gdapi.gd_{{ builtin.snake_name }}_meth_{{ m.name }}(
120-
{% elif m.return_type.is_builtin %}
120+
{# 2) Actual function call with return value initialization #}
121+
{% if m.return_type.is_scalar %}
122+
cdef {{m.return_type.c_type }} ret = gdapi.gd_{{ builtin.snake_name }}_meth_{{ m.name }}(
123+
{% elif m.return_type.is_builtin %}
121124
# Call to __new__ bypasses __init__ constructor
122125
cdef {{ m.return_type.cy_type }} ret = {{ m.return_type.cy_type }}.__new__({{ m.return_type.cy_type }})
123126
ret._gd_data = gdapi.gd_{{ builtin.snake_name }}_meth_{{ m.name }}(
124-
{% elif m.return_type.is_object or m.return_type.is_variant %}
125-
cdef {{ m.return_type.c_type }} ret = gdapi.gd_{{ builtin.snake_name }}_meth_{{ m.name }}(
126-
{% else %}
127+
{% elif m.return_type.is_object or m.return_type.is_variant %}
128+
cdef {{ m.return_type.c_type }} ret = gdapi.gd_{{ builtin.snake_name }}_meth_{{ m.name }}(
129+
{% else %}
127130
gdapi.gd_{{ builtin.snake_name }}_meth_{{ m.name }}(
128-
{% endif %}
131+
{% endif %}
132+
{% if m.is_static %}
129133
NULL,
130-
{% for arg in m.arguments %}
131-
{% if arg.type.is_scalar %}
134+
{% else %}
135+
&self._gd_data,
136+
{% endif %}
137+
{# 3) Parameter provided to the function call #}
138+
{% for arg in m.arguments %}
139+
{% if arg.type.is_scalar %}
132140
{{ arg.name }},
133-
{% elif arg.type.original_name in ("String", "StringName", "NodePath") %}
134-
&__arg{{ loop.index0 }}._gd_data,
135-
{% elif arg.type.is_builtin %}
141+
{% elif arg.type.original_name in ("String", "StringName", "NodePath") %}
142+
&__arg_{{ arg.name }}._gd_data,
143+
{% elif arg.type.is_builtin %}
136144
&{{ arg.name }}._gd_data,
137-
{% elif arg.type.is_object %}
145+
{% elif arg.type.is_object %}
138146
&{{ arg.name }}._gd_ptr,
139-
{% elif arg.type.is_variant %}
140-
&__arg{{ loop.index0 }},
141-
{% else %}
147+
{% elif arg.type.is_variant %}
148+
&__arg_{{ arg.name }},
149+
{% else %}
142150
<crash !!!!> # {{ arg.type }} Unsupported type, crash the compilation !
143-
{% endif %}
144-
{% endfor %}
151+
{% endif %}
152+
{% endfor %}
145153
)
146-
{% if not m.return_type.is_nil %}
147-
{% if m.return_type.is_object %}
154+
{# 4) Optional return value conversion #}
155+
{% if not m.return_type.is_nil %}
156+
{% if m.return_type.is_object %}
148157
return BaseGDObject.cast_from_object(ret)
149-
{% elif m.return_type.is_variant %}
150-
# Note `gd_variant_steal_into_pyobj(&ret)` calls `ret`'s destructor
158+
{% elif m.return_type.is_variant %}
159+
# Note the "steal" means we don't need a final `gdapi.gd_variant_del(&ret)`
151160
return gd_variant_steal_into_pyobj(&ret)
152-
{% else %}
161+
{% else %}
153162
return ret
154-
{% endif %}
155-
{% endif %}
156163
{% endif %}
164+
{% endif %}
157165
{% endmacro %}
158166

159167

160168
{% macro render_method(builtin, m) %}
161-
{% if m.is_static %}
162-
{{ _render_static_method(builtin, m) }}
169+
{% if m.is_vararg %}
170+
{{ _render_method_vararg(builtin, m) }}
163171
{% else %}
164-
{{ _render_non_static_method(builtin, m) }}
172+
{{ _render_method_standard(builtin, m) }}
165173
{% endif %}
166174
{% endmacro %}

0 commit comments

Comments
 (0)