Skip to content

Commit 96a6f82

Browse files
committed
Improve code generation
1 parent 8c13cfe commit 96a6f82

18 files changed

+424
-558
lines changed

scripts/generate_builtins.py

Lines changed: 0 additions & 465 deletions
This file was deleted.

scripts/generate_tmpl.py

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
#! /usr/bin/env python3
2+
3+
import argparse
4+
from pathlib import Path
5+
from jinja2 import Environment, FileSystemLoader, StrictUndefined
6+
7+
from extension_api_parser import BuildConfig, parse_extension_api_json, ExtensionApi
8+
9+
10+
BASEDIR = Path(__file__).parent
11+
12+
13+
def make_jinja_env(import_dir: Path) -> Environment:
14+
env = Environment(
15+
loader=FileSystemLoader(import_dir),
16+
trim_blocks=True,
17+
lstrip_blocks=False,
18+
extensions=["jinja2.ext.loopcontrols"],
19+
undefined=StrictUndefined,
20+
)
21+
env.filters["merge"] = lambda x, **kwargs: {**x, **kwargs}
22+
return env
23+
24+
25+
def generate_gdapi_ptrs(api: ExtensionApi) -> str:
26+
env = make_jinja_env(BASEDIR / "../src/godot/_hazmat")
27+
template = env.get_template("gdapi_ptrs.tmpl.pxi")
28+
return template.render(api=api)
29+
30+
31+
def generate_builtins_pxd(api: ExtensionApi) -> str:
32+
env = make_jinja_env(BASEDIR / "../src/godot")
33+
template = env.get_template("_builtins.tmpl.pxd")
34+
return template.render(api=api)
35+
36+
37+
def generate_builtins_pyx(api: ExtensionApi) -> str:
38+
env = make_jinja_env(BASEDIR / "../src/godot")
39+
template = env.get_template("_builtins.tmpl.pyx")
40+
return template.render(api=api)
41+
42+
43+
def generate_builtins_pyi(api: ExtensionApi) -> str:
44+
env = make_jinja_env(BASEDIR / "../src/godot")
45+
template = env.get_template("_builtins.tmpl.pyi")
46+
return template.render(api=api)
47+
48+
49+
OUTPUT_TO_FN = {
50+
"gdapi_ptrs.pxi": generate_gdapi_ptrs,
51+
"_builtins.pxd": generate_builtins_pxd,
52+
"_builtins.pyx": generate_builtins_pyx,
53+
"_builtins.pyi": generate_builtins_pyi,
54+
}
55+
56+
57+
if __name__ == "__main__":
58+
parser = argparse.ArgumentParser(description="Generate code from templates")
59+
parser.add_argument(
60+
"--input",
61+
"-i",
62+
required=True,
63+
metavar="EXTENSION_API_PATH",
64+
type=Path,
65+
help="Path to Godot extension_api.json file",
66+
)
67+
parser.add_argument(
68+
"--output",
69+
"-o",
70+
required=True,
71+
type=Path,
72+
help=f"pyx/pxd/pyi to generate (choices: {', '.join(OUTPUT_TO_FN.keys())})",
73+
)
74+
parser.add_argument(
75+
"--build-config",
76+
required=True,
77+
type=BuildConfig,
78+
choices=BuildConfig,
79+
help=f"choices: {', '.join([x.value for x in BuildConfig])}",
80+
metavar="CONFIG",
81+
)
82+
83+
args = parser.parse_args()
84+
85+
api = parse_extension_api_json(path=args.input, build_config=args.build_config)
86+
87+
# We use # in the name to simulate folder hierarchy in the meson build
88+
*_, name = args.output.name.rsplit("#", 1)
89+
code = OUTPUT_TO_FN[name](api)
90+
args.output.write_text(code, encoding="utf8")

src/_pythonscript.pyx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
# godot_pluginscript_language_data,
1616
# )
1717
# from godot._hazmat.internal cimport set_pythonscript_verbose, get_pythonscript_verbose
18-
# from godot.builtins cimport GDString
18+
from godot._builtins cimport GDString, Vector2
1919

2020
# def _setup_config_entry(name, default_value):
2121
# gdname = GDString(name)
@@ -43,6 +43,10 @@ cdef api void _pythonscript_initialize() with gil:
4343
cooked_sys_version = '.'.join(map(str, sys.version_info))
4444
print(f"Pythonscript {pythonscript_version} (CPython {cooked_sys_version})")
4545
print(f"PYTHONPATH: {sys.path}")
46+
v = Vector2(3., 2.)
47+
v2 = Vector2(v)
48+
print("===========>", v.x, v.y)
49+
print("+++++++++++>", v2.x, v2.y)
4650

4751

4852
# # OS and ProjectSettings are singletons exposed as global python objects,

src/godot/__init__.pxd

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from ._builtins cimport *
2+
# from ._bindings cimport *
3+
from . cimport _hazmat

src/godot/__init__.py

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -32,14 +32,6 @@
3232
# export,
3333
# exposed,
3434
# )
35-
# from godot.pool_arrays import (
36-
# PoolIntArray,
37-
# PoolRealArray,
38-
# PoolByteArray,
39-
# PoolVector2Array,
40-
# PoolVector3Array,
41-
# PoolColorArray,
42-
# PoolStringArray,
43-
# )
44-
# from godot.builtins import *
35+
from godot._builtins import *
36+
4537
# from godot.bindings import *

src/godot/_builtins.tmpl.pxd

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,15 @@
1-
{%- from '_builtin.tmpl.pxi' import render_spec -%}
21
# /!\ Autogenerated code, modifications will be lost /!\
3-
# see `generation/generate_builtins.py`
2+
# see `scripts/generate_tmpl.py`
3+
4+
cimport cython
5+
{% for spec in api["builtins"] if not spec.is_scalar %}
6+
7+
8+
@cython.freelist(8)
9+
@cython.final
10+
cdef class {{ spec.name }}:
11+
cdef char _gd_data[{{ spec.size }}]
12+
13+
@staticmethod
14+
cdef inline {{ spec.name }} new()
15+
{% endfor %}

src/godot/_builtins.tmpl.pyi

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,20 @@
11
# /!\ Autogenerated code, modifications will be lost /!\
2-
# see `generation/generate_builtins.py`
2+
# see `scripts/generate_tmpl.py`
33

44

55
class GDObject:
66
...
77

88

9-
GDVariant = None | GDObject | {{ specs | map(attribute="name") | join(" | ") }}
9+
# GDAny represent any type that can be send to / retreive from Godot
10+
# (i.e. it is all the types than can be converted to a Godot Variant)
11+
GDAny = None | GDObject | {{ api["builtins"] | map(attribute="name") | join(" | ") }}
1012

1113
{% macro render_arg(arg) -%}
12-
{{arg.name}}: {{ arg.type.name if arg.type.name != "GDString" else "GDString | str"}}{{ "={}".format(arg.default_value) if arg.default_value is not none }}
14+
{{arg.name}}: {{ arg.type.py_type if arg.type.py_type != "GDString" else "GDString | str"}}{{ "={}".format(arg.default_value.value) if arg.default_value is not none }}
1315
{%- endmacro %}
1416

15-
{% for spec in specs %}
17+
{% for spec in api["builtins"] if not spec.is_scalar %}
1618

1719
class {{spec.name}}:
1820
{% for c in spec.constructors %}
@@ -22,19 +24,19 @@ class {{spec.name}}:
2224
# Members
2325
{% endif %}
2426
{% for m in spec.members %}
25-
{{m.name}}: {{ m.type.name }}
27+
{{m.name}}: {{ m.type.py_type }}
2628
{% endfor %}
2729
{% if spec.methods %}
2830
# Methods
2931
{% endif %}
3032
{% for m in spec.methods %}
31-
def {{m.name}}(self{% for a in m.arguments %}, {{ render_arg(a) }}{% endfor %}) -> {{ m.return_type.name if m.return_type is not none else "None" }}: ...
33+
def {{m.name}}(self{% for a in m.arguments %}, {{ render_arg(a) }}{% endfor %}) -> {{ m.return_type.py_type if m.return_type is not none else "None" }}: ...
3234
{% endfor %}
3335
{% if spec.constants %}
3436
# Constants
3537
{% endif %}
3638
{% for c in spec.constants %}
37-
{{c.name}}: {{c.type.name}}
39+
{{c.name}}: {{c.type.py_type}}
3840
{% endfor %}
3941

4042
{% endfor %}

src/godot/_builtins.tmpl.pyx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
1-
{%- from '_builtin.tmpl.pxi' import render_spec -%}
1+
{%- from '_builtins_pyx/class.tmpl.pxi' import render_spec -%}
22
# /!\ Autogenerated code, modifications will be lost /!\
3-
# see `generation/generate_builtins.py`
3+
# see `scripts/generate_tmpl.py`
44

55
cimport cython
66
from libc.math cimport INFINITY as inf # Needed by some constants
77

8-
from godot._hazmat.gdnative_interface cimport *
9-
from godot._hazmat.gdapi cimport pythonscript_gdapi as gdapi
8+
from ._hazmat.gdnative_interface cimport *
9+
from ._hazmat.gdapi cimport pythonscript_gdapi as gdapi
1010

11-
{% for item in specs -%}
12-
{{ render_spec(item) }}
11+
{% for spec in api["builtins"] if not spec.is_scalar -%}
12+
{{ render_spec(spec) }}
1313
{% endfor %}

src/godot/_builtin.tmpl.pxi renamed to src/godot/_builtins_pyx/class.tmpl.pxi

Lines changed: 39 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,17 @@
1+
{%- from '_builtins_pyx/constructor.tmpl.pxi' import render_constructor -%}
2+
{%- from '_builtins_pyx/gdapi.tmpl.pxi' import render_gdapi -%}
3+
14
{% macro render_spec(spec) -%}
25

3-
{% for c in spec.constructors %}
4-
cdef GDNativePtrConstructor __{{ spec.name }}_constructor_{{ c.index }} = gdapi.variant_get_ptr_constructor(
5-
{{ spec.variant_type_name }}, {{ c.index }}
6-
)
7-
{% endfor %}
8-
{% if spec.has_destructor %}
9-
cdef GDNativePtrDestructor __{{ spec.name }}_destructor = gdapi.variant_get_ptr_destructor(
10-
{{ spec.variant_type_name }}
11-
)
12-
{% endif %}
6+
{{ render_gdapi(spec) }}
137

8+
@cython.freelist(8)
149
@cython.final
1510
cdef class {{ spec.name }}:
1611
{# TODO: I guess only Nil has no size, so remove it and only use None ? #}
1712
{% if spec.size %}
18-
cdef char _gd_data[{{ spec.size }}]
19-
2013
# Constructors
21-
def __init__({{ spec.name }} self):
22-
__{{ spec.name }}_constructor_0(self._gd_data, NULL)
14+
{{ render_constructor(spec) | indent }}
2315

2416
@staticmethod
2517
cdef inline {{ spec.name }} new():
@@ -32,7 +24,7 @@ cdef class {{ spec.name }}:
3224
{% if spec.has_destructor %}
3325
# Destructor
3426
def __dealloc__({{ spec.name }} self):
35-
# /!\ if `__init__` is skipped, `_gd_data` must be initialized by
27+
# /!\ if `__cinit__` is skipped, `_gd_data` must be initialized by
3628
# hand otherwise we will get a segfault here
3729
__{{ spec.name }}_destructor(self._gd_data)
3830
{% else %}
@@ -49,12 +41,41 @@ cdef class {{ spec.name }}:
4941

5042
{% endif %}
5143
{% for c in spec.constants %}
52-
{{ c.name }} = {{ c.value }}
44+
# {{ c.name }} = {{ c.value }}
45+
{% endfor %}
46+
47+
{% if spec.members %}
48+
# Members
49+
{% endif %}
50+
{% for m in spec.members %}
51+
@property
52+
def {{ m.name }}(self) -> {{ m.type.py_type }}:
53+
{% if m.offset is not none %}
54+
{% if m.type.is_scalar %}
55+
return (<{{ m.type.c_type }}*>self._gd_data)[{{ m.offset }}]
56+
{# TODO: ensure m.type doesn't need a destructor #}
57+
{% else %}
58+
{# TODO: Find a way to avoid this copy ? #}
59+
cdef {{ m.type.py_type }} ret = {{ m.type.py_type }}.__new__() # Skips __init__()
60+
ret._gd_data = self._gd_data
61+
return ret
62+
{% endif %}
63+
@{{ m.name }}.setter
64+
def {{ m.name }}(self, {{ m.type.py_type }} value) -> None:
65+
{% if m.type.is_scalar %}
66+
(<{{ m.type.c_type }}*>self._gd_data)[{{ m.offset }}] = value
67+
{# TODO: ensure m.type doesn't need a destructor #}
68+
{% else %}
69+
self._gd_data = value._gd_data
70+
{% endif %}
71+
{% else %}
72+
# TODO: support properties !
73+
raise NotImplementedError
74+
{% endif %}
5375
{% endfor %}
5476

5577
{% if spec.methods %}
5678
# Methods
57-
5879
{% endif %}
5980
{% for m in spec.methods %}
6081
def {{ m.name }}(
@@ -75,7 +96,7 @@ cdef class {{ spec.name }}:
7596
{% if m.arguments | length == 0 %}
7697
# {% if m.return_type %}
7798
# # TODO
78-
# # cdef {{ m.return_type.cython_name }} ret = {{ m.return_type.cython_name }}.__new__()
99+
# # cdef {{ m.return_type.cy_type }} ret = {{ m.return_type.cy_type }}.__new__()
79100
# {% set retval_as_arg = "NULL" %}
80101
# {% else %}
81102
# {% set retval_as_arg = "NULL" %}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
{% macro render_constructor(spec) -%}
2+
3+
{% for constructor in spec.constructors %}
4+
# {{ constructor.index }} {{ constructor.arguments }}
5+
{% endfor %}
6+
7+
{% if spec.name == "Vector2" %}
8+
9+
def __cinit__({{ spec.name }} self, x=None, y=None):
10+
cdef GDNativeTypePtr args[2]
11+
cdef float args_x
12+
cdef float args_y
13+
if y is None:
14+
if x is None:
15+
__{{ spec.name }}_constructor_0(self._gd_data, NULL)
16+
return
17+
18+
try:
19+
args[0] = (<Vector2>x)._gd_data
20+
except TypeError:
21+
22+
try:
23+
args[0] = (<Vector2i>x)._gd_data
24+
except TypeError:
25+
pass
26+
27+
else:
28+
# Constructor from Vector2i
29+
__{{ spec.name }}_constructor_2(self._gd_data, args)
30+
return
31+
32+
else:
33+
# Constructor from Vector2
34+
__{{ spec.name }}_constructor_1(self._gd_data, args)
35+
return
36+
37+
elif isinstance(x, (float, int)) and isinstance(y, (float, int)):
38+
args_x = x
39+
args_y = y
40+
args[0] = &args_x
41+
args[1] = &args_y
42+
__{{ spec.name }}_constructor_3(self._gd_data, args)
43+
return
44+
45+
raise TypeError(f"Expected x&y as int/float, or x as Vector2/Vector2i")
46+
47+
{% else %}
48+
49+
def __cinit__({{ spec.name }} self):
50+
__{{ spec.name }}_constructor_0(self._gd_data, NULL)
51+
52+
{% endif %}
53+
54+
{% endmacro %}

0 commit comments

Comments
 (0)