diff --git a/MANIFEST.in b/MANIFEST.in index 9056b62c..0396ccfa 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,3 +1,2 @@ recursive-include robotpy_build/pybind11/include *.h -recursive-include robotpy_build/autowrap *.j2 recursive-include robotpy_build/include *.h \ No newline at end of file diff --git a/robotpy_build/autowrap/buffer.py b/robotpy_build/autowrap/buffer.py new file mode 100644 index 00000000..d693e85b --- /dev/null +++ b/robotpy_build/autowrap/buffer.py @@ -0,0 +1,45 @@ +import io +import contextlib +import inspect + + +class RenderBuffer: + def __init__(self) -> None: + self._buffer = io.StringIO() + + self._indent = "" + self._indentlen = 0 + + def getvalue(self) -> str: + return self._buffer.getvalue() + + def rel_indent(self, spaces: int): + self._indentlen += spaces + self._indent = " " * self._indentlen + + @contextlib.contextmanager + def indent(self, spaces: int = 2): + self._indentlen += spaces + self._indent = " " * self._indentlen + try: + yield + finally: + self._indentlen -= spaces + self._indent = " " * self._indentlen + + def writeln(self, s: str = ""): + if not s: + self._buffer.write("\n") + else: + for line in s.splitlines(False): + if line: + self._buffer.write(f"{self._indent}{line}\n") + else: + self._buffer.write("\n") + + def write_trim(self, s: str): + for line in inspect.cleandoc(s).splitlines(False): + if line: + self._buffer.write(f"{self._indent}{line}\n") + else: + self._buffer.write("\n") diff --git a/robotpy_build/autowrap/cls_prologue.cpp.j2 b/robotpy_build/autowrap/cls_prologue.cpp.j2 deleted file mode 100644 index 38408cea..00000000 --- a/robotpy_build/autowrap/cls_prologue.cpp.j2 +++ /dev/null @@ -1,28 +0,0 @@ -// This file is autogenerated. DO NOT EDIT -#include -{% if has_vcheck %} -#include -{% endif %} - -{% for inc in extra_includes_first %} -#include <{{ inc }}> -{% endfor %} - -#include <{{ rel_fname }}> - -{% for inc in type_caster_includes %} -#include <{{ inc }}> -{% endfor %} - -{% if need_operators_h %} -#include -{% endif %} - -{% for decl in using_declarations %} -using {{ decl.format() }}; -{% endfor %} - -{% for cls in classes_with_trampolines %} -#define RPYGEN_ENABLE_{{ cls.full_cpp_name_identifier }}_PROTECTED_CONSTRUCTORS -#include -{% endfor %} \ No newline at end of file diff --git a/robotpy_build/autowrap/cls_rpy_include.hpp.j2 b/robotpy_build/autowrap/cls_rpy_include.hpp.j2 deleted file mode 100644 index cacb0901..00000000 --- a/robotpy_build/autowrap/cls_rpy_include.hpp.j2 +++ /dev/null @@ -1,30 +0,0 @@ -{# - Pieces that go into an rpy-include file for a class - - - Trampoline base class (if applicable) - - Template constructors/method fillers (if applicable) - -#} - -// This file is autogenerated. DO NOT EDIT - -#pragma once -#include - -{% for inc in extra_includes_first %} -#include <{{ inc }}> -{% endfor %} - -#include <{{ rel_fname }}> - -{% for inc in extra_includes %} -#include <{{ inc }}> -{% endfor %} - -{% if cls.trampoline %} -{% include 'cls_trampoline.hpp.j2' %} -{% endif %} - -{% if cls.template %} -{% include 'cls_tmpl_impl.hpp.j2' %} -{% endif %} diff --git a/robotpy_build/autowrap/cls_tmpl_impl.hpp.j2 b/robotpy_build/autowrap/cls_tmpl_impl.hpp.j2 deleted file mode 100644 index 8b3a90ac..00000000 --- a/robotpy_build/autowrap/cls_tmpl_impl.hpp.j2 +++ /dev/null @@ -1,56 +0,0 @@ -{# - C++ template support -#} -{% import "pybind11.cpp.j2" as pybind11 %} - -{% for inc in type_caster_includes %} -#include <{{ inc }}> -{% endfor %} - -namespace rpygen { - -{% if cls.namespace %} -using namespace {{ cls.namespace }}; -{% endif %} - -{% for decl in using_declarations %} -using {{ decl.format() }}; -{% endfor %} - -template <{{ cls.template.parameter_list }}> -struct bind_{{ cls.full_cpp_name_identifier }} { - - {{ pybind11.cls_user_using(cls) }} - {{ pybind11.cls_auto_using(cls) }} - {{ pybind11.cls_consts(cls) }} - {{ pybind11.cls_decl(cls) }} - - py::module &m; - std::string clsName; - -bind_{{ cls.full_cpp_name_identifier }}(py::module &m, const char * clsName) : - {# TODO: embedded structs will fail here #} - {{ pybind11.cls_init(cls, "clsName") }} - m(m), - clsName(clsName) -{ - {{ pybind11.cls_def_enum(cls, cls.var_name) }} -} - -void finish(const char * set_doc = NULL, const char * add_doc = NULL) { - - {{ pybind11.cls_def(cls, cls.var_name) }} - - if (set_doc) { - {{ cls.var_name }}.doc() = set_doc; - } - if (add_doc) { - {{ cls.var_name }}.doc() = py::cast({{ cls.var_name }}.doc()) + add_doc; - } - - {{ cls.template.inline_code }} -} - -}; // struct bind_{{ cls.full_cpp_name_identifier }} - -}; // namespace rpygen diff --git a/robotpy_build/autowrap/cls_tmpl_inst.cpp.j2 b/robotpy_build/autowrap/cls_tmpl_inst.cpp.j2 deleted file mode 100644 index 79d64055..00000000 --- a/robotpy_build/autowrap/cls_tmpl_inst.cpp.j2 +++ /dev/null @@ -1,23 +0,0 @@ -{% include "cls_prologue.cpp.j2" %} - -#include -#include "{{ hname }}_tmpl.hpp" - -namespace rpygen { - -using BindType = rpygen::bind_{{ tmpl_data.full_cpp_name_identifier }}<{{ tmpl_data.params | join(', ') }}>; -static std::unique_ptr inst; - -{{ tmpl_data.binder_typename }}::{{ tmpl_data.binder_typename }}(py::module &m, const char * clsName) -{ - inst = std::make_unique(m, clsName); -} - -void {{ tmpl_data.binder_typename }}::finish(const char *set_doc, const char *add_doc) -{ - inst->finish(set_doc, add_doc); - inst.reset(); -} - -}; // namespace rpygen - diff --git a/robotpy_build/autowrap/cls_tmpl_inst.hpp.j2 b/robotpy_build/autowrap/cls_tmpl_inst.hpp.j2 deleted file mode 100644 index ff1069fe..00000000 --- a/robotpy_build/autowrap/cls_tmpl_inst.hpp.j2 +++ /dev/null @@ -1,14 +0,0 @@ -// This file is autogenerated. DO NOT EDIT -#pragma once -#include - -namespace rpygen { - -{% for tmpl_data in template_instances %} -struct {{ tmpl_data.binder_typename }} { - {{ tmpl_data.binder_typename }}(py::module &m, const char * clsName); - void finish(const char *set_doc, const char *add_doc); -}; -{% endfor %} - -}; // namespace rpygen diff --git a/robotpy_build/autowrap/cls_trampoline.hpp.j2 b/robotpy_build/autowrap/cls_trampoline.hpp.j2 deleted file mode 100644 index 2623866c..00000000 --- a/robotpy_build/autowrap/cls_trampoline.hpp.j2 +++ /dev/null @@ -1,218 +0,0 @@ -{# - Generate trampoline classes to be used for two purposes: - - * Allow python programs to override virtual functions - * Allow python programs access to protected members - - This trampoline is used from two different places: - - To generate a trampoline usable by the class itself - - Generate a trampoline usable by child classes - - Sometimes these are the same trampoline. The exception is when - a 'final' method is in the base class, then a separate - - Each trampoline type is placed in a different namespace - to make our life easier. - - Trampoline functions can be disabled via RPY_DISABLE_name_[type_type..] -#} - -{%- macro precomma(v) -%} - {%- if v %}, {{ v }}{%- endif -%} -{%- endmacro -%} - -{%- macro postcomma(v) -%} - {%- if v %}{{ v }}, {% endif -%} -{%- endmacro -%} - -{%- set trampoline = cls.trampoline -%} -{%- if cls.template -%} - {%- set template_argument_list = cls.template.argument_list -%} - {%- set template_parameter_list = cls.template.parameter_list -%} -{%- else -%} - {%- set template_argument_list = "" %} - {%- set template_parameter_list = "" %} -{%- endif -%} - -{# delete specified methods #} -{% for fn in trampoline.methods_to_disable %} -#define RPYGEN_DISABLE_{{ trampoline_signature(fn) }} -{% endfor %} - -{# include override files for each base -- TODO: exclude some bases? #} -{% if cls.bases %} -{% for base in cls.bases %} -#include -{% endfor %} -{% endif %} - -namespace rpygen { - -{% if cls.namespace %} -using namespace {{ cls.namespace }}; -{% endif %} - -{% for decl in using_declarations %} -using {{ decl.format() }}; -{% endfor %} - -{# - Each trampoline has a configuration struct. - - - Stores the base class that the trampoline is wrapping - - Provides a mechanism to detect which base class to use when calling an - overloaded virtual function (each class defines the overloads they have, - and so if it's not defined in this config, then it falls back to the - parent configuration) -#} -template <{{ postcomma(template_parameter_list) }}typename CfgBase = EmptyTrampolineCfg> -struct PyTrampolineCfg_{{ cls.full_cpp_name_identifier }} : -{% if cls.bases %} -{% for base in cls.bases %} - PyTrampolineCfg_{{ base.full_cpp_name_identifier }}<{{ postcomma(base.template_params) }} -{% endfor %} -CfgBase -{% for base in cls.bases %}>{% endfor %} -{% else %} - CfgBase -{% endif %} -{ - using Base = {{ cls.full_cpp_name }}; - - {# specify base class to use for each virtual function #} - {% for fn in trampoline.virtual_methods %} - using override_base_{{ trampoline_signature(fn) }} = {{ cls.full_cpp_name }}; - {% endfor %} -}; - - -{% if cls.bases %} -{# - To avoid multiple inheritance here, we define a single base with bases that - are all template bases.. - - PyTrampolineBase is another trampoline or our base class -#} -template -using PyTrampolineBase_{{ cls.full_cpp_name_identifier }} = -{% for base in cls.bases %} - PyTrampoline_{{ base.full_cpp_name_identifier }}< -{% endfor %} - PyTrampolineBase -{% for base in (cls.bases | reverse) %} - {{ precomma(base.template_params) }} - , PyTrampolineCfg - > -{% endfor %} -; - -template -struct PyTrampoline_{{ cls.full_cpp_name_identifier }} : PyTrampolineBase_{{ cls.full_cpp_name_identifier }} { - using PyTrampolineBase_{{ cls.full_cpp_name_identifier }}::PyTrampolineBase_{{ cls.full_cpp_name_identifier }}; -{% else %} -template -struct PyTrampoline_{{ cls.full_cpp_name_identifier }} : PyTrampolineBase, virtual py::trampoline_self_life_support { - using PyTrampolineBase::PyTrampolineBase; -{% endif %} - -{% for ccls in cls.child_classes if not ccls.template %} - using {{ ccls.cpp_name }} [[maybe_unused]] = typename {{ ccls.full_cpp_name }}; -{% endfor %} -{% for enum in cls.enums if enum.cpp_name %} - using {{ enum.cpp_name }} [[maybe_unused]] = typename {{ enum.full_cpp_name }}; -{% endfor %} -{% for typealias in cls.user_typealias %} - {{ typealias }}; -{% endfor %} -{% for typealias in cls.auto_typealias %} - {{ typealias }}; -{% endfor %} -{% for name, constant in cls.constants %} - static constexpr auto {{ name }} = {{ constant }}; -{% endfor %} - -{# protected constructors -- only used by the direct child #} -{% for fn in trampoline.protected_constructors %} -#ifdef RPYGEN_ENABLE_{{ cls.full_cpp_name_identifier }}_PROTECTED_CONSTRUCTORS - PyTrampoline_{{ cls.full_cpp_name_identifier }}({{ fn.all_params | join(', ', attribute='decl') }}) : - {% if cls.bases -%} - PyTrampolineBase_{{ cls.full_cpp_name_identifier }} - {%- else -%} - PyTrampolineBase - {%- endif -%} - ({{ fn.all_params | join(', ', attribute='arg_name') }}) - {} -#endif -{% endfor %} - - {# virtual methods #} - {% for fn in trampoline.virtual_methods %} -#ifndef RPYGEN_DISABLE_{{ trampoline_signature(fn) }} - {{ fn.cpp_return_type }} {{ fn.cpp_name }}({{ fn.all_params | join(', ', attribute='decl') }}) - {%- if fn.const %} const{% endif -%} - {{ fn.ref_qualifiers }} override { - {% if fn.trampoline_cpp_code %} - {{ fn.trampoline_cpp_code }} - {% else %} - {# TODO: probably will break for things like out parameters, etc #} - {% if fn.ignore_pure %} - throw std::runtime_error("not implemented"); - {% else %} - {% if fn.virtual_xform %} - auto custom_fn = {{ fn.virtual_xform }}; - {% endif %} - using LookupBase = typename PyTrampolineCfg::Base; - {# - We define a "LookupBase" and "CallBase" here because to find the python - override we need to use the actual class currently being overridden, but - to make the actual call we might need to use a base class. - - .. lots of duplication here, but it's worse without it - #} - {% if fn.is_pure_virtual and fn.virtual_xform %} - RPYBUILD_OVERRIDE_PURE_CUSTOM_NAME({{ cls.cpp_name }}, PYBIND11_TYPE({{ fn.cpp_return_type }}), LookupBase, - "{{ fn.py_name }}", {{ fn.cpp_name }}, {{ fn.all_params | join(', ', attribute='arg_name') }}); - {% elif fn.is_pure_virtual %} - RPYBUILD_OVERRIDE_PURE_NAME({{ cls.cpp_name }}, PYBIND11_TYPE({{ fn.cpp_return_type }}), LookupBase, - "{{ fn.py_name }}", {{ fn.cpp_name }}, {{ fn.all_params | join(', ', attribute='arg_name') }}); - {% elif fn.virtual_xform %} - using CxxCallBase = typename PyTrampolineCfg::override_base_{{ trampoline_signature(fn) }}; - RPYBUILD_OVERRIDE_CUSTOM_IMPL(PYBIND11_TYPE({{ fn.cpp_return_type }}), LookupBase, - "{{ fn.py_name }}", {{ fn.cpp_name }}, {{ fn.all_params | join(', ', attribute='arg_name') }}); - return CxxCallBase::{{ fn.cpp_name }}({{ fn.all_params | join(', ', attribute='virtual_call_name') }}); - {% else %} - using CxxCallBase = typename PyTrampolineCfg::override_base_{{ trampoline_signature(fn) }}; - PYBIND11_OVERRIDE_IMPL(PYBIND11_TYPE({{ fn.cpp_return_type }}), LookupBase, - "{{ fn.py_name }}", {{ fn.all_params | join(', ', attribute='arg_name') }}); - return CxxCallBase::{{ fn.cpp_name }}({{ fn.all_params | join(', ', attribute='virtual_call_name') }}); - {% endif %} - {% endif %} - {% endif %} - } -#endif - - {% endfor %} - - {# non-virtual protected methods/attributes #} - {% for fn in trampoline.non_virtual_protected_methods %} -#ifndef RPYBLD_DISABLE_{{ trampoline_signature(fn) }} -{# hack to ensure we don't do 'using' twice' in the same class, while - also ensuring that the overrides can be selectively disabled by - child trampoline functions #} - #ifndef RPYBLD_UDISABLE_{{ using_signature(cls, fn) }} - using {{ cls.full_cpp_name }}::{{ fn.cpp_name }}; - #define RPYBLD_UDISABLE_{{ using_signature(cls, fn) }} - #endif -#endif - {% endfor %} - - {% for prop in cls.protected_properties %} - using {{ cls.full_cpp_name }}::{{ prop.cpp_name }}; - {% endfor %} - - {% if trampoline.inline_code %} - {{ trampoline.inline_code }} - {% endif %} -}; - -}; // namespace rpygen diff --git a/robotpy_build/autowrap/j2_context.py b/robotpy_build/autowrap/context.py similarity index 95% rename from robotpy_build/autowrap/j2_context.py rename to robotpy_build/autowrap/context.py index fcd0aca6..63f13c87 100644 --- a/robotpy_build/autowrap/j2_context.py +++ b/robotpy_build/autowrap/context.py @@ -1,8 +1,9 @@ # -# These dataclasses hold data to be rendered by the *.j2 files in templates +# These dataclasses hold data to be rendered # -# To simplify the templates, where possible we try to do logic in the code -# that produces this data instead of in the templates. +# To make the rendering functions easier to understand, where possible we try +# to do logic in the code that produces this data instead of in the rendering +# functions. # # We also prefer to copy over data from the autowrap YAML file instead of using # those data structures directly. While there's some slight overhead added, @@ -118,11 +119,11 @@ class ParamContext: #: Name to pass to function when generating a virtual function virtual_call_name: str - #: Not used in jinja template, but is used to determine variable + #: Not used in renderer, but is used to determine variable #: name when used as an out parameter cpp_retname: str - #: Not used in jinja template, used for filtering + #: Not used in renderer, used for filtering category: ParamCategory # type + name, rarely used @@ -361,8 +362,6 @@ class ClassTemplateData: class ClassContext: """ Render data for each class encountered in a header - - Available in class .j2 files as `cls` """ parent: typing.Optional["ClassContext"] @@ -428,7 +427,7 @@ class ClassContext: # Everything else # - # Not used in jinja_template + # Not used in rendering functions but used elsewhere has_constructor: bool = False is_polymorphic: bool = False @@ -503,7 +502,7 @@ class TemplateInstanceContext: @dataclass class HeaderContext: """ - Globals in all .j2 files + Data derived from parsing a single header """ # Name in toml @@ -513,9 +512,6 @@ class HeaderContext: extra_includes: typing.List[str] inline_code: typing.Optional[str] - trampoline_signature: typing.Callable[[FunctionContext], str] - using_signature: typing.Callable[[ClassContext, FunctionContext], str] - #: Path to the parsed header relative to some root rel_fname: str diff --git a/robotpy_build/autowrap/cxxparser.py b/robotpy_build/autowrap/cxxparser.py index 725bb940..83b3891e 100644 --- a/robotpy_build/autowrap/cxxparser.py +++ b/robotpy_build/autowrap/cxxparser.py @@ -68,7 +68,7 @@ ReturnValuePolicy, ) from .generator_data import GeneratorData, OverloadTracker -from .j2_context import ( +from .context import ( BaseClassData, ClassContext, ClassTemplateData, @@ -84,7 +84,6 @@ TemplateInstanceContext, TrampolineData, ) -from .mangle import trampoline_signature class HasSubpackage(Protocol): @@ -114,13 +113,13 @@ def _gen_int_types(): _rvp_map = { - ReturnValuePolicy.TAKE_OWNERSHIP: ", py::return_value_policy::take_ownership", - ReturnValuePolicy.COPY: ", py::return_value_policy::copy", - ReturnValuePolicy.MOVE: ", py::return_value_policy::move", - ReturnValuePolicy.REFERENCE: ", py::return_value_policy::reference", - ReturnValuePolicy.REFERENCE_INTERNAL: ", py::return_value_policy::reference_internal", + ReturnValuePolicy.TAKE_OWNERSHIP: "py::return_value_policy::take_ownership", + ReturnValuePolicy.COPY: "py::return_value_policy::copy", + ReturnValuePolicy.MOVE: "py::return_value_policy::move", + ReturnValuePolicy.REFERENCE: "py::return_value_policy::reference", + ReturnValuePolicy.REFERENCE_INTERNAL: "py::return_value_policy::reference_internal", ReturnValuePolicy.AUTOMATIC: "", - ReturnValuePolicy.AUTOMATIC_REFERENCE: ", py::return_value_policy::automatic_reference", + ReturnValuePolicy.AUTOMATIC_REFERENCE: "py::return_value_policy::automatic_reference", } # fmt: off @@ -137,7 +136,6 @@ def _gen_int_types(): _qualname_bad = ":<>=" _qualname_trans = str.maketrans(_qualname_bad, "_" * len(_qualname_bad)) -_lambda_predent = " " _default_param_data = ParamData() _default_enum_value = EnumValue() @@ -268,10 +266,6 @@ class _ReturnParamContext: cpp_retname: str -def _using_signature(cls: ClassContext, fn: FunctionContext) -> str: - return f"{cls.full_cpp_name_identifier}_{fn.cpp_name}" - - # # Visitor implementation # @@ -1595,7 +1589,7 @@ def _on_fn_make_lambda(self, data: FunctionData, fctx: FunctionContext): # Return values (original return value + any out parameters) fn_retval = fctx.cpp_return_type if fn_retval and fn_retval != "void": - call_start = "auto __ret =" + call_start = "auto __ret = " ret_params = [_ReturnParamContext(cpp_retname="__ret", cpp_type=fn_retval)] ret_params.extend(out_params) else: @@ -1618,7 +1612,7 @@ def _on_fn_make_lambda(self, data: FunctionData, fctx: FunctionContext): else: lambda_pre.insert(0, f"{out.cpp_type} {out.arg_name} = {odef}") - pre = _lambda_predent + f";\n{_lambda_predent}".join(lambda_pre) + ";" + pre = f";\n".join(lambda_pre) + ";" fctx.genlambda = GeneratedLambda( pre=pre, @@ -1964,8 +1958,6 @@ def parse_header( extra_includes_first=user_cfg.extra_includes_first, extra_includes=user_cfg.extra_includes, inline_code=user_cfg.inline_code, - trampoline_signature=trampoline_signature, - using_signature=_using_signature, rel_fname=str(header_path.relative_to(header_root)), ) diff --git a/robotpy_build/autowrap/generator_data.py b/robotpy_build/autowrap/generator_data.py index be01bcf2..942aafd5 100644 --- a/robotpy_build/autowrap/generator_data.py +++ b/robotpy_build/autowrap/generator_data.py @@ -7,7 +7,7 @@ PropData, FunctionData, ) -from .j2_context import OverloadTracker +from .context import OverloadTracker from cxxheaderparser.types import Function diff --git a/robotpy_build/autowrap/header.cpp.j2 b/robotpy_build/autowrap/header.cpp.j2 deleted file mode 100644 index 565e0b60..00000000 --- a/robotpy_build/autowrap/header.cpp.j2 +++ /dev/null @@ -1,164 +0,0 @@ -{# - This contains the primary binding code generated from parsing a single - header file. There are also per-class headers generated (templates, - trampolines), and those are included/used by this. -#} -{% include "cls_prologue.cpp.j2" %} - -{% import "pybind11.cpp.j2" as pybind11 %} - -{% if template_instances %} -#include "{{ hname }}_tmpl.hpp" -{% endif %} - -{% for inc in extra_includes %} -#include <{{ inc }}> -{% endfor %} - -{%- for typealias in user_typealias %} -{{ typealias }}; -{% endfor %} - -{# - Ordering of the initialization function - - - namespace/typealiases - - global enums - - templates (because CRTP) - - class declarations - - class enums - - class methods - - global methods - - Additionally, we use two-part initialization to ensure that documentation - strings are generated properly. First part is to register the class with - pybind11, second part is to generate all the methods/etc for it. - - TODO: make type_traits optional by detecting trampoline -#} -#include - -{% for ns in namespaces %} - using namespace {{ ns }}; -{% endfor %} - - -struct rpybuild_{{ hname }}_initializer { - -{% for cls in classes if not cls.template %} - {{ pybind11.cls_user_using(cls) }} - {{ pybind11.cls_consts(cls) }} -{% endfor %} - -{% for pkg, vname in subpackages.items() %} - py::module {{ vname }}; -{% endfor %} - -{# enums #} -{% for enum in enums %} - {{ pybind11.enum_decl(enum) }} enum{{ loop.index }}; -{% endfor %} - -{# template decls #} -{% for tmpl_data in template_instances if not tmpl_data.matched %} - rpygen::{{ tmpl_data.binder_typename }} {{ tmpl_data.var_name }}; -{% endfor %} - -{# class decls #} -{%- for cls in classes %} - {% if not cls.template -%} - {{ pybind11.cls_decl(cls) }} - {%- else -%} - {%- for tmpl_data in cls.template.instances %} - rpygen::{{ tmpl_data.binder_typename }} {{ tmpl_data.var_name }}; - {% endfor -%} - {%- endif -%} -{% endfor %} - - py::module &m; - - {# register classes with pybind11 #} - rpybuild_{{ hname }}_initializer(py::module &m) : - - {% for pkg, vname in subpackages.items() %} - {{ vname }}(m.def_submodule("{{ pkg }}")), - {% endfor %} - - {% for enum in enums %} - enum{{ loop.index }}{{ pybind11.enum_init(enum.scope_var, enum) }}, - {% endfor %} - - {% for tmpl_data in template_instances if not tmpl_data.matched %} - {{ tmpl_data.var_name }}({{ tmpl_data.scope_var }}, "{{ tmpl_data.py_name }}"), - {% endfor %} - - {% for cls in classes %} - {% if not cls.template -%} - {{ pybind11.cls_init(cls, '"' + cls.py_name + '"') }} - {%- else -%} - {%- for tmpl_data in cls.template.instances %} - {{ tmpl_data.var_name }}({{ tmpl_data.scope_var }}, "{{ tmpl_data.py_name }}"), - {% endfor -%} - {%- endif %} - {% endfor %} - - m(m) - { - {# - enums can go in the initializer because they cant have dependencies, - and then we dont need to figure out class dependencies for enum arguments - #} - {% for enum in enums %} - enum{{ loop.index }}{{ pybind11.enum_def(enum.scope_var, enum) }} - {% endfor %} - - {% for cls in classes %} - {{ pybind11.cls_def_enum(cls, cls.var_name) }} - {% for ccls in cls.child_classes %} - {{ pybind11.cls_def_enum(ccls, ccls.var_name) }} - {% endfor %} - {% endfor %} - } - -void finish() { - -{# templates #} -{% for tdata in template_instances %} - {{ tdata.var_name }}.finish( - {% if tdata.doc_set %}{{ pybind11.docv(tdata.doc_set) }}{% else %}NULL{% endif %}, - {% if tdata.doc_add %}{{ pybind11.docv(tdata.doc_add) }}{% else %}NULL{% endif %} - ); -{% endfor %} - -{# class methods #} -{%- for cls in classes if not cls.template %} - { - {{ pybind11.cls_auto_using(cls) }} - - {{ pybind11.cls_def(cls, cls.var_name) }} - } -{% endfor %} - -{# global methods #} -{% for fn in functions if not fn.ignore_py -%} - {{ fn.scope_var }}{{ pybind11.genmethod(None, fn, None) }}; -{% endfor %} - -{% if inline_code %} - - {{ inline_code }} -{% endif %} -} - -}; // struct rpybuild_{{ hname }}_initializer - -static std::unique_ptr cls; - -void begin_init_{{ hname }}(py::module &m) { - cls = std::make_unique(m); -} - -void finish_init_{{ hname }}() { - cls->finish(); - cls.reset(); -} \ No newline at end of file diff --git a/robotpy_build/autowrap/mangle.py b/robotpy_build/autowrap/mangle.py index b0b90d99..d54f3667 100644 --- a/robotpy_build/autowrap/mangle.py +++ b/robotpy_build/autowrap/mangle.py @@ -1,6 +1,6 @@ import typing -from .j2_context import FunctionContext +from .context import FunctionContext from cxxheaderparser.types import ( Array, DecoratedType, diff --git a/robotpy_build/autowrap/pybind11.cpp.j2 b/robotpy_build/autowrap/pybind11.cpp.j2 deleted file mode 100644 index c4410bb9..00000000 --- a/robotpy_build/autowrap/pybind11.cpp.j2 +++ /dev/null @@ -1,344 +0,0 @@ -{# - Macros for binding things with pybind11 -#} - -{%- macro docv(value) -%} - {%- for dq in value -%} - {{ dq }}{% if loop.nextitem is defined %}{{ '\n' }}{% endif %} - {%- endfor -%} -{%- endmacro -%} - -{%- macro doc(o, pre, post) -%} - {%- if o.doc %}{{ pre }} - {{ docv(o.doc) }}{{ post }} - {%- endif -%} -{%- endmacro -%} - -{%- macro fndef(fn) %} -{% if fn.is_static_method %}def_static{% else %}def{% endif %} -{% endmacro -%} - -{%- macro fnptr(cls_qualname, fn, trampoline_qualname, tmpl) -%} - {%- if fn.cpp_code -%} - {{ fn.cpp_code }} - {%- elif not fn.genlambda -%} - & - {%- if trampoline_qualname -%} - {{ trampoline_qualname }}:: - {%- elif cls_qualname -%} - {{ cls_qualname }}:: - {%- else -%} - {{ fn.namespace }}:: - {%- endif -%} - {% if tmpl %}template {% endif %}{{ fn.cpp_name }}{{ tmpl }} - {%- else -%} - {%- set genlambda = fn.genlambda -%} - []( - {%- if cls_qualname -%} - {{ cls_qualname }} * __that - {%- if genlambda.in_params %},{% endif -%} - {% endif -%} - {{ genlambda.in_params | join(', ', attribute='decl') -}} - ) { - {{ genlambda.pre }} - {{ genlambda.call_start }} - {%- if trampoline_qualname -%} - (({{ trampoline_qualname }}*)__that)-> - {%- elif cls_qualname -%} - __that-> - {%- else -%} - {{ fn.namespace }}:: - {%- endif -%} - {{ fn.cpp_name }}{{ tmpl }}({{ fn.all_params | join(', ', attribute='call_name') }}); - {{ genlambda.ret }} - } - {%- endif -%} -{%- endmacro -%} - -{%- macro gensig(cls_qualname, fn) -%} - {#- - py::overload_cast fails in some obscure cases, so we don't use it here - https://github.com/pybind/pybind11/issues/1153 - -#} - {{ fn.cpp_return_type }}( - {%- if cls_qualname and not fn.is_static_method -%} - {{ cls_qualname }}:: - {%- endif -%} - *)( - {{- fn.all_params | join(', ', attribute='full_cpp_type') -}} - ) - {%- if fn.const %} const{% endif -%} - {%- if fn.ref_qualifiers %} {{ fn.ref_qualifiers }}{% endif -%} -{%- endmacro -%} - -{%- macro _genmethod(cls_qualname, fn, trampoline_qualname, tmpl) -%} - {%- set ns = namespace(qualname=cls_qualname, arg_params=fn.filtered_params) -%} - {%+ if fn.ifdef %} - - #ifdef {{ fn.ifdef }} - {% endif %} - {%+ if fn.ifndef %} - - #ifndef {{ fn.ifndef }} - {% endif %} - {%- if fn.operator -%} - .def({{ fn.cpp_code }} - {%- set ns.arg_params = [] -%} - {%- elif fn.is_constructor -%} - {%- if fn.cpp_code -%} - .def(py::init({{ fn.cpp_code }}) - {%- elif trampoline_qualname -%} - .def(py::init_alias<{{ ns.arg_params | join(', ', attribute='full_cpp_type') }}>() - {%- else -%} - .def(py::init<{{ ns.arg_params | join(', ', attribute='full_cpp_type') }}>() - {%- endif -%} - {%- else -%} - .{{ fndef(fn) }}("{{ fn.py_name }}",{{ ' ' }} - {%- if not fn.cpp_code and not fn.genlambda -%} - {%- if trampoline_qualname -%} - {%- set ns.qualname = trampoline_qualname -%} - static_cast<{{ gensig(cls_qualname, fn) }}>( - {%- endif -%} - {%- if fn.is_overloaded -%} - static_cast<{{ gensig(ns.qualname, fn) }}>( - {% endif -%} - {%- endif -%} - {%- if fn.genlambda -%}{%- set ns.arg_params = fn.genlambda.in_params -%}{%- endif -%} - {{- fnptr(cls_qualname, fn, trampoline_qualname, tmpl) -}} - {%- if not fn.cpp_code and not fn.genlambda -%} - {%- if fn.is_overloaded -%}){%- endif -%} - {%- if trampoline_qualname -%}){%- endif -%} - {%- endif -%} - {%- endif -%} - - {%- if ns.arg_params -%}, - {{ ns.arg_params | join(', ', attribute='py_arg') }} - {%- endif -%} - - {%- if fn.release_gil -%} - , release_gil() - {%- endif -%} - - {%- for nurse, patient in fn.keepalives %} - , py::keep_alive<{{ nurse }}, {{ patient }}>() - {%- endfor -%} - - {{- fn.return_value_policy -}} - - {{ doc(fn, ', py::doc(', ')') }} - ) - {%+ if fn.ifdef %} - #endif // {{ fn.ifdef }} - {% endif %} - {%+ if fn.ifndef %} - #endif // {{ fn.ifndef }} - {% endif %} -{%- endmacro -%} - -{%- macro genmethod(cls_qualname, fn, trampoline_qualname) -%} - {%- if not fn.template_impls -%} - {{ _genmethod(cls_qualname, fn, trampoline_qualname, "") }} - {%- else -%} - {%- for tmpl in fn.template_impls -%} - {{ _genmethod(cls_qualname, fn, trampoline_qualname, "<" + (tmpl | join(", ")) + ">") }} - {% endfor -%} - {%- endif -%} -{%- endmacro %} - -{%- macro genprop(qualname, prop) -%} - {%- if prop.array_size -%} - .def_property_readonly("{{ prop.py_name }}", []({{ qualname }}& inst) { - return py::memoryview::from_buffer( - &inst.{{ prop.cpp_name }}, sizeof({{ prop.cpp_type }}), - py::format_descriptor<{{ prop.cpp_type }}>::value, - {{ "{" }}{{ prop.array_size }}{{ "}" }}, {sizeof({{ prop.cpp_type }})}, - {% if prop.readonly %}true{% else %}false{% endif %} - ); - } - {{- doc(prop, ', py::doc(', ')') }}) - {%- elif prop.array -%} - {# cannot sensibly autowrap an array of incomplete size #} - {%- elif prop.reference or prop.bitfield -%} - .def_property - {%- if prop.readonly -%} - _readonly - {%- endif -%} - ("{{ prop.py_name }}", {{ ' ' }} - [](const {{ qualname }}& inst) -> {{ prop.cpp_type }} { return inst.{{ prop.cpp_name}}; } - {%- if not prop.readonly %}, - []({{ qualname }}& inst, {{ prop.cpp_type }} v) {inst.{{ prop.cpp_name}} = v; } - {%- endif -%} - {{- doc(prop, ', py::doc(', ')') }} - ) - - {%- else -%} - .def_ - {%- if prop.readonly -%} - readonly - {%- else -%} - readwrite - {%- endif -%} - {%- if prop.static %}_static{% endif -%} - ("{{ prop.py_name }}", &{{ qualname }}::{{ prop.cpp_name}} - {{- doc(prop, ', py::doc(', ')') }}) - {%- endif -%} -{%- endmacro -%} - -{%- macro enum_decl(enum) %} - py::enum_<{{ enum.full_cpp_name }}> -{%- endmacro -%} - -{%- macro enum_init(scope, enum) %} - ({{ scope }}, "{{ enum.py_name }}" - {{ doc(enum, ',', '') }} - {%- if enum.arithmetic %}, py::arithmetic(){% endif %}) -{%- endmacro -%} - -{%- macro enum_def(scope, enum) %} - {% for val in enum.values %} - .value("{{ val.py_name }}", {{ val.full_cpp_name }} - {%- if val.doc %}, - {% for dq in val.doc -%} - {{ dq }}{% if loop.nextitem is defined %}{{ '\n' }}{% endif %} - {%- endfor -%} - {%- endif -%}) - {% endfor -%} - {{ enum.inline_code or "" }}; -{% endmacro -%} - -{%- macro unnamed_enums(x, unnamed_enums) %} -{% for enum in unnamed_enums -%} - {% for val in enum.values -%} - {{ x }}.attr("{{ val.py_name }}") = (int){{ val.full_cpp_name }}; - {% endfor %} -{% endfor %} -{% endmacro -%} - -{%- macro cls_user_using(cls) -%} - {% for typealias in cls.user_typealias %} - {{ typealias }}; - {% endfor %} -{% endmacro -%} - -{%- macro cls_auto_using(cls) -%} - {% for ccls in cls.child_classes if not ccls.template %} - using {{ ccls.cpp_name }} [[maybe_unused]] = typename {{ ccls.full_cpp_name }}; - {% endfor %} - {% for enum in cls.enums if enum.cpp_name %} - using {{ enum.cpp_name }} [[maybe_unused]] = typename {{ enum.full_cpp_name }}; - {% endfor %} - {% for typealias in cls.auto_typealias %} - {{ typealias }}; - {% endfor %} -{% endmacro -%} - -{%- macro cls_consts(cls) -%} - {% for constant in cls.constants -%} - static constexpr auto {{ constant[0] }} = {{ constant[1] }}; - {% endfor %} -{% endmacro -%} - - -{%- macro cls_decl(cls) -%} - {%- if cls.trampoline %}{% set tctx = cls.trampoline %} - using {{ tctx.var }} = {{ tctx.full_cpp_name }}; - static_assert(std::is_abstract<{{ tctx.var }}>::value == false, "{{ cls.full_cpp_name }} " RPYBUILD_BAD_TRAMPOLINE); - {% endif -%} - py::class_ - {%- endif -%} - {%- if cls.trampoline -%} - , {{ cls.trampoline.var }} - {%- endif -%} - - {%- if cls.bases -%} - , {{ cls.bases | join(', ', attribute='full_cpp_name_w_templates') }} - {%- endif -%} - > {{ cls.var_name }}; - - {% for enum in cls.enums %} - {{ enum_decl(enum) }} {{ cls.var_name }}_enum{{ loop.index }}; - {% endfor %} - - {# recurse #} - {% for ccls in cls.child_classes if not ccls.template %} - {{ cls_decl(ccls) }} - {% endfor -%} - -{%- endmacro -%} - -{%- macro cls_init(cls, name) -%} - {{ cls.var_name }}({{ cls.scope_var }}, {{ name }} - {%- if cls.final -%} - , py::is_final() - {%- endif -%} - {%- if cls.force_multiple_inheritance -%} - , py::multiple_inheritance() - {%- endif -%} - - ), - - {% for enum in cls.enums %} - {{ cls.var_name }}_enum{{ loop.index }}{{ enum_init(cls.var_name, enum) }}, - {% endfor %} - - {# recurse #} - {% for ccls in cls.child_classes if not ccls.template %} - {{ cls_init(ccls, '"' + ccls.py_name + '"') }} - {% endfor -%} - -{%- endmacro -%} - -{%- macro cls_def_enum(cls, varname) %} - {% for enum in cls.enums %} - {{ cls.var_name }}_enum{{ loop.index }}{{ enum_def(cls.var_name, enum) }} - {% endfor %} -{% endmacro -%} - -{%- macro cls_def(cls, varname) -%} - - {% if cls.vcheck_fns -%} - {%- for fn in cls.vcheck_fns %} - { - auto vcheck = {{ fn.cpp_code.strip() }}; - static_assert(std::is_convertible>::value, - "{{ cls.full_cpp_name }}::{{ fn.cpp_name }} must have virtual_xform if cpp_code signature doesn't match original function"); - } - {% endfor -%} - {%- endif %} - - {{ doc(cls, varname + '.doc() =', ';') }} - - {{ varname }} - {% if cls.add_default_constructor %} - .def(py::init<>(), release_gil()) - {% endif -%} - {%- for fn in cls.wrapped_public_methods %} - {{ genmethod(cls.full_cpp_name, fn, None) }} - {% endfor -%} - - {%- if cls.trampoline -%} - {%- for fn in cls.wrapped_protected_methods %} - {{ genmethod(cls.full_cpp_name, fn, cls.trampoline.var ) }} - {% endfor -%} - {%- endif -%} - - {%- for prop in cls.public_properties %} - {{ genprop(cls.full_cpp_name, prop) }} - {% endfor -%} - {%- for prop in cls.protected_properties %} - {{ genprop(cls.trampoline.full_cpp_name, prop) }} - {%- endfor %}{{ cls.inline_code }}; - - {{ unnamed_enums(varname, cls.unnamed_enums) }} - - {#- recurse -#} - {%- for ccls in cls.child_classes if not ccls.template %} - {{ cls_def(ccls, ccls.var_name) }} - {% endfor -%} - -{%- endmacro -%} diff --git a/robotpy_build/autowrap/render_cls_prologue.py b/robotpy_build/autowrap/render_cls_prologue.py new file mode 100644 index 00000000..98d0ce64 --- /dev/null +++ b/robotpy_build/autowrap/render_cls_prologue.py @@ -0,0 +1,41 @@ +from .buffer import RenderBuffer +from .context import HeaderContext + + +def render_class_prologue(r: RenderBuffer, hctx: HeaderContext): + # fmt: off + r.writeln( + "// This file is autogenerated. DO NOT EDIT\n" + "#include " + ) + # fmt: on + + if hctx.has_vcheck: + r.writeln("#include ") + + if hctx.extra_includes_first: + r.writeln() + for inc in hctx.extra_includes_first: + r.writeln(f"#include <{inc}>") + + r.writeln(f"\n#include <{hctx.rel_fname}>") + + if hctx.type_caster_includes: + r.writeln() + for inc in hctx.type_caster_includes: + r.writeln(f"#include <{inc}>") + + if hctx.need_operators_h: + r.writeln(f"\n#include ") + + if hctx.using_declarations: + r.writeln() + for decl in hctx.using_declarations: + r.writeln(f"using {decl.format()};") + + for cls in hctx.classes_with_trampolines: + r.writeln() + r.writeln( + f"#define RPYGEN_ENABLE_{cls.full_cpp_name_identifier}_PROTECTED_CONSTRUCTORS" + ) + r.writeln(f"#include ") diff --git a/robotpy_build/autowrap/render_cls_rpy_include.py b/robotpy_build/autowrap/render_cls_rpy_include.py new file mode 100644 index 00000000..5e245136 --- /dev/null +++ b/robotpy_build/autowrap/render_cls_rpy_include.py @@ -0,0 +1,435 @@ +from .buffer import RenderBuffer +from .context import ( + HeaderContext, + ClassContext, + ClassTemplateData, + FunctionContext, + TrampolineData, +) +from .mangle import trampoline_signature + +from . import render_pybind11 as rpybind11 + + +def precomma(v: str) -> str: + return f", {v}" if v else "" + + +def postcomma(v: str) -> str: + return f"{v}, " if v else "" + + +def using_signature(cls: ClassContext, fn: FunctionContext) -> str: + return f"{cls.full_cpp_name_identifier}_{fn.cpp_name}" + + +def render_cls_rpy_include_hpp(ctx: HeaderContext, cls: ClassContext) -> str: + """ + Pieces that go into an rpy-include file for a class + + - Trampoline base class (if applicable) + - Template constructors/method fillers (if applicable) + """ + + r = RenderBuffer() + r.writeln( + "// This file is autogenerated. DO NOT EDIT\n" + "\n" + "#pragma once\n" + "#include " + ) + + if ctx.extra_includes_first: + r.writeln() + for inc in ctx.extra_includes_first: + r.writeln(f"#include <{inc}>") + + r.writeln(f"\n#include <{ctx.rel_fname}>") + + if ctx.extra_includes: + r.writeln() + for inc in ctx.extra_includes: + r.writeln(f"#include <{inc}>") + + if cls.trampoline is not None: + _render_cls_trampoline(r, ctx, cls, cls.trampoline) + + if cls.template is not None: + _render_cls_template_impl(r, ctx, cls, cls.template) + + return r.getvalue() + + +def _render_cls_trampoline( + r: RenderBuffer, hctx: HeaderContext, cls: ClassContext, trampoline: TrampolineData +): + """ + Generate trampoline classes to be used for two purposes: + + * Allow python programs to override virtual functions + * Allow python programs access to protected members + + This trampoline is used from two different places: + - To generate a trampoline usable by the class itself + - Generate a trampoline usable by child classes + + Sometimes these are the same trampoline. The exception is when + a 'final' method is in the base class, then a separate + + Each trampoline type is placed in a different namespace + to make our life easier. + + Trampoline functions can be disabled via RPY_DISABLE_name_[type_type..] + """ + + if cls.template: + template_argument_list = cls.template.argument_list + template_parameter_list = cls.template.parameter_list + else: + template_argument_list = "" + template_parameter_list = "" + + # delete specified methods + if trampoline.methods_to_disable: + r.writeln() + for fn in trampoline.methods_to_disable: + r.writeln(f"#define RPYGEN_DISABLE_{ trampoline_signature(fn) }") + + # include override files for each base -- TODO: exclude some bases? + if cls.bases: + r.writeln() + for base in cls.bases: + r.writeln(f"#include ") + + r.writeln("\nnamespace rpygen {") + + if cls.namespace: + r.writeln(f"\nusing namespace {cls.namespace};") + + if hctx.using_declarations: + r.writeln() + for decl in hctx.using_declarations: + r.writeln(f"using {decl.format()};") + + # + # Each trampoline has a configuration struct. + # + # - Stores the base class that the trampoline is wrapping + # - Provides a mechanism to detect which base class to use when calling an + # overloaded virtual function (each class defines the overloads they have, + # and so if it's not defined in this config, then it falls back to the + # parent configuration) + # + + r.writeln( + f"\ntemplate <{postcomma(template_parameter_list)}typename CfgBase = EmptyTrampolineCfg>" + ) + + if cls.bases: + r.writeln(f"struct PyTrampolineCfg_{cls.full_cpp_name_identifier} :") + + with r.indent(): + for base in cls.bases: + r.writeln( + f"PyTrampolineCfg_{base.full_cpp_name_identifier}<{postcomma(base.template_params)}" + ) + + r.writeln("CfgBase") + + for base in cls.bases: + r.writeln(">") + else: + r.writeln(f"struct PyTrampolineCfg_{cls.full_cpp_name_identifier} : CfgBase") + + r.writeln("{") + + with r.indent(): + r.writeln(f"using Base = {cls.full_cpp_name};\n") + + # specify base class to use for each virtual function + for fn in trampoline.virtual_methods: + r.writeln( + f"using override_base_{ trampoline_signature(fn) } = { cls.full_cpp_name };" + ) + + r.writeln("};") + + if cls.bases: + # To avoid multiple inheritance here, we define a single base with bases that + # are all template bases.. + # + # PyTrampolineBase is another trampoline or our base class + r.writeln() + + r.writeln( + f"template " + ) + r.writeln(f"using PyTrampolineBase_{cls.full_cpp_name_identifier} =") + + for base in cls.bases: + r.rel_indent(2) + r.writeln(f"PyTrampoline_{base.full_cpp_name_identifier}<") + + with r.indent(): + r.writeln("PyTrampolineBase") + + for base in reversed(cls.bases): + if base.template_params: + r.writeln(f", {base.template_params}") + r.writeln(", PyTrampolineCfg>") + r.rel_indent(-2) + + r.write_trim( + f""" + ; + + template + struct PyTrampoline_{ cls.full_cpp_name_identifier } : PyTrampolineBase_{ cls.full_cpp_name_identifier } {{ + using PyTrampolineBase_{ cls.full_cpp_name_identifier }::PyTrampolineBase_{ cls.full_cpp_name_identifier }; + """ + ) + + else: + r.writeln() + r.write_trim( + f""" + template + struct PyTrampoline_{ cls.full_cpp_name_identifier } : PyTrampolineBase, virtual py::trampoline_self_life_support {{ + using PyTrampolineBase::PyTrampolineBase; + """ + ) + + with r.indent(): + for ccls in cls.child_classes: + if not ccls.template: + r.writeln( + f"using {ccls.cpp_name} [[maybe_unused]] = typename {ccls.full_cpp_name};" + ) + + for enum in cls.enums: + if enum.cpp_name: + r.writeln( + f"using {enum.cpp_name} [[maybe_unused]] = typename {enum.full_cpp_name};" + ) + + for typealias in cls.user_typealias: + r.writeln(f"{typealias};") + + for typealias in cls.auto_typealias: + r.writeln(f"{typealias};") + + if cls.constants: + r.writeln() + for name, constant in cls.constants: + r.writeln(f"static constexpr auto {name} = {constant};") + + # + # protected constructors -- only used by the direct child + # + + for fn in trampoline.protected_constructors: + r.writeln( + f"\n#ifdef RPYGEN_ENABLE_{cls.full_cpp_name_identifier}_PROTECTED_CONSTRUCTORS" + ) + with r.indent(): + all_decls = ", ".join(p.decl for p in fn.all_params) + all_names = ", ".join(p.arg_name for p in fn.all_params) + r.writeln(f"PyTrampoline_{cls.full_cpp_name_identifier}({all_decls}) :") + + if cls.bases: + r.writeln( + f" PyTrampolineBase_{cls.full_cpp_name_identifier}({all_names})" + ) + else: + r.writeln(f" PyTrampolineBase({all_names})") + + r.writeln("{}") + r.writeln("#endif") + + # + # virtual methods + # + + for fn in trampoline.virtual_methods: + _render_cls_trampoline_virtual_method(r, cls, fn) + + # + # non-virtual protected methods/attributes + # + + for fn in trampoline.non_virtual_protected_methods: + r.writeln(f"\n#ifndef RPYBLD_DISABLE_{ trampoline_signature(fn) }") + + # hack to ensure we don't do 'using' twice' in the same class, while + # also ensuring that the overrides can be selectively disabled by + # child trampoline functions + with r.indent(): + r.writeln(f"#ifndef RPYBLD_UDISABLE_{ using_signature(cls, fn) }") + with r.indent(): + r.write_trim( + f""" + using { cls.full_cpp_name }::{ fn.cpp_name }; + #define RPYBLD_UDISABLE_{ using_signature(cls, fn) } + """ + ) + r.writeln("#endif") + r.writeln("#endif") + + if cls.protected_properties: + r.writeln() + for prop in cls.protected_properties: + r.writeln(f"using {cls.full_cpp_name}::{prop.cpp_name};") + + if trampoline.inline_code: + r.writeln() + r.write_trim(trampoline.inline_code) + + r.writeln("};\n\n}; // namespace rpygen") + + +def _render_cls_trampoline_virtual_method( + r: RenderBuffer, cls: ClassContext, fn: FunctionContext +): + r.writeln(f"\n#ifndef RPYGEN_DISABLE_{ trampoline_signature(fn) }") + with r.indent(): + + all_decls = ", ".join(p.decl for p in fn.all_params) + const = " const" if fn.const else "" + decl = f"{fn.cpp_return_type} {fn.cpp_name}({all_decls}){const}{fn.ref_qualifiers} override {{" + r.writeln(decl) + + with r.indent(): + if fn.trampoline_cpp_code: + r.write_trim(fn.trampoline_cpp_code) + elif fn.ignore_pure: + r.writeln('throw std::runtime_error("not implemented");') + + else: + # TODO: probably will break for things like out parameters, etc #} + if fn.virtual_xform: + r.writeln(f"auto custom_fn = {fn.virtual_xform};") + + # We define a "LookupBase" and "CallBase" here because to find the python + # override we need to use the actual class currently being overridden, but + # to make the actual call we might need to use a base class. + # + # .. lots of duplication here, but it's worse without it + + r.writeln("using LookupBase = typename PyTrampolineCfg::Base;") + + all_names = ", ".join(p.arg_name for p in fn.all_params) + all_vnames = ", ".join(p.virtual_call_name for p in fn.all_params) + + if fn.is_pure_virtual and fn.virtual_xform: + r.write_trim( + f""" + RPYBUILD_OVERRIDE_PURE_CUSTOM_NAME({cls.cpp_name}, PYBIND11_TYPE({fn.cpp_return_type}), LookupBase, + "{fn.py_name}", {fn.cpp_name}, {all_names}); + """ + ) + elif fn.is_pure_virtual: + r.write_trim( + f""" + RPYBUILD_OVERRIDE_PURE_NAME({cls.cpp_name}, PYBIND11_TYPE({fn.cpp_return_type}), LookupBase, + "{fn.py_name}", {fn.cpp_name}, {all_names}); + """ + ) + elif fn.virtual_xform: + r.write_trim( + f""" + using CxxCallBase = typename PyTrampolineCfg::override_base_{trampoline_signature(fn)}; + RPYBUILD_OVERRIDE_CUSTOM_IMPL(PYBIND11_TYPE({fn.cpp_return_type}), LookupBase, + "{fn.py_name}", {fn.cpp_name}, {all_names}); + return CxxCallBase::{fn.cpp_name}({all_vnames}); + """ + ) + else: + r.write_trim( + f""" + using CxxCallBase = typename PyTrampolineCfg::override_base_{trampoline_signature(fn)}; + PYBIND11_OVERRIDE_IMPL(PYBIND11_TYPE({fn.cpp_return_type}), LookupBase, + "{fn.py_name}", {all_names}); + return CxxCallBase::{fn.cpp_name}({all_vnames}); + """ + ) + + r.writeln("}") + r.writeln("#endif") + + +def _render_cls_template_impl( + r: RenderBuffer, hctx: HeaderContext, cls: ClassContext, template: ClassTemplateData +): + if hctx.type_caster_includes: + r.writeln() + for inc in hctx.type_caster_includes: + r.writeln(f"#include <{inc}>") + + r.writeln("\nnamespace rpygen {") + + if cls.namespace: + r.writeln(f"\nusing namespace {cls.namespace};") + + if hctx.using_declarations: + r.writeln() + for decl in hctx.using_declarations: + r.writeln(f"using {decl.format()};") + + r.writeln(f"\ntemplate <{template.parameter_list}>") + r.writeln(f"struct bind_{cls.full_cpp_name_identifier} {{") + + with r.indent(): + rpybind11.cls_user_using(r, cls) + rpybind11.cls_auto_using(r, cls) + rpybind11.cls_consts(r, cls) + rpybind11.cls_decl(r, cls) + + r.writeln("\npy::module &m;\nstd::string clsName;") + + r.writeln( + f"bind_{cls.full_cpp_name_identifier}(py::module &m, const char * clsName) :" + ) + + with r.indent(): + + # TODO: embedded structs will fail here + rpybind11.cls_init(r, cls, "clsName") + r.writeln("m(m),") + r.writeln("clsName(clsName) {") + + rpybind11.cls_def_enum(r, cls, cls.var_name) + + r.write_trim( + """ + } + + void finish(const char * set_doc = NULL, const char * add_doc = NULL) { + """ + ) + with r.indent(): + rpybind11.cls_def(r, cls, cls.var_name) + + r.write_trim( + f""" + if (set_doc) {{ + {cls.var_name}.doc() = set_doc; + }} + if (add_doc) {{ + {cls.var_name}.doc() = py::cast({cls.var_name}.doc()) + add_doc; + }} + """ + ) + + if template.inline_code: + r.writeln() + r.write_trim(template.inline_code) + + r.writeln("}") + + r.write_trim( + f""" + }}; // struct bind_{cls.full_cpp_name_identifier} + + }}; // namespace rpygen + """ + ) diff --git a/robotpy_build/autowrap/render_pybind11.py b/robotpy_build/autowrap/render_pybind11.py new file mode 100644 index 00000000..e6962b87 --- /dev/null +++ b/robotpy_build/autowrap/render_pybind11.py @@ -0,0 +1,412 @@ +import inspect +import typing as T + +from .buffer import RenderBuffer +from .context import ( + ClassContext, + Documentation, + EnumContext, + FunctionContext, + PropContext, +) + + +def mkdoc(pre: str, doc: Documentation, post: str) -> str: + if doc: + if len(doc) == 1: + return f"{pre}{doc[0]}{post}" + + sep = "\n " + return f"{pre}\n {sep.join(doc)}{post}" + + return "" + + +def _gensig(cls_qualname: T.Optional[str], fn: FunctionContext) -> str: + """ + py::overload_cast fails in some obscure cases, so we compute the signature here + https://github.com/pybind/pybind11/issues/1153 + """ + if fn.const: + trailing = " const" + else: + trailing = "" + if fn.ref_qualifiers: + trailing = f"{trailing} {fn.ref_qualifiers}" + + params = ", ".join(param.full_cpp_type for param in fn.all_params) + + return ( + f"{fn.cpp_return_type} (" + f"{cls_qualname + '::' if cls_qualname and not fn.is_static_method else ''}*)" + f"({params}){trailing}" + ) + + +def _genmethod( + r: RenderBuffer, + cls_qualname: T.Optional[str], + fn: FunctionContext, + trampoline_qualname: T.Optional[str], + tmpl: str, +): + qualname = cls_qualname + arg_params = fn.filtered_params + + if fn.ifdef: + r.writeln(f"\n#ifdef {fn.ifdef}") + r.rel_indent(2) + + if fn.ifndef: + r.writeln(f"\n#ifndef {fn.ifndef}") + r.rel_indent(2) + + if fn.operator: + r.writeln(f".def({fn.cpp_code}") + arg_params = [] + elif fn.is_constructor: + if fn.cpp_code: + r.writeln(f".def(py::init({fn.cpp_code})") + elif trampoline_qualname: + r.writeln( + f".def(py::init_alias<{', '.join(param.full_cpp_type for param in arg_params)}>()" + ) + else: + r.writeln( + f".def(py::init<{', '.join(param.full_cpp_type for param in arg_params)}>()" + ) + else: + if fn.is_static_method: + fn_def = f'.def_static("{fn.py_name}"' + else: + fn_def = f'.def("{fn.py_name}"' + + if fn.cpp_code: + cpp_code = inspect.cleandoc(fn.cpp_code) + r.writeln(f"{fn_def}, {cpp_code}") + + elif fn.genlambda: + genlambda = fn.genlambda + arg_params = genlambda.in_params + + lam_params = [param.decl for param in genlambda.in_params] + if cls_qualname: + lam_params = [f"{cls_qualname} &self"] + lam_params + + r.writeln(f"{fn_def}, []({', '.join(lam_params)}) {{") + + with r.indent(): + if genlambda.pre: + r.writeln(genlambda.pre) + + if trampoline_qualname: + call_qual = f"(({trampoline_qualname}*)&self)->" + elif cls_qualname: + call_qual = f"(({cls_qualname}*)&self)->" + else: + call_qual = f"{fn.namespace}::" + + call_params = ", ".join(p.call_name for p in fn.all_params) + + r.writeln( + f"{genlambda.call_start}{call_qual}{fn.cpp_name}{tmpl}({call_params});" + ) + + if genlambda.ret: + r.writeln(genlambda.ret) + + r.writeln("}") + + else: + if trampoline_qualname: + fn_ns = trampoline_qualname + elif cls_qualname: + fn_ns = cls_qualname + else: + fn_ns = fn.namespace + + if trampoline_qualname: + fn_cast = f"static_cast<{_gensig(cls_qualname, fn)}>(" + paren = ")" + elif fn.is_overloaded: + fn_cast = f"static_cast<{_gensig(qualname, fn)}>(" + paren = ")" + else: + fn_cast = "" + paren = "" + + if tmpl: + fn_name = f"template {fn.cpp_name}" + else: + fn_name = fn.cpp_name + + r.writeln(f"{fn_def}, {fn_cast}&{fn_ns}::{fn_name}{tmpl}{paren}") + + if arg_params: + r.writeln(f" , {', '.join(param.py_arg for param in arg_params)}") + + other_params = [] + + if fn.release_gil: + other_params.append("release_gil()") + + for nurse, patient in fn.keepalives: + other_params.append(f"py::keep_alive<{nurse}, {patient}>()") + + if fn.return_value_policy: + other_params.append(fn.return_value_policy) + + if other_params: + r.writeln(f" , {', '.join(other_params)}") + + if fn.doc: + r.writeln(mkdoc(" , py::doc(", fn.doc, ")")) + r.writeln(")") + + if fn.ifdef: + r.rel_indent(-2) + r.writeln(f"#endif // {fn.ifdef}\n") + + if fn.ifndef: + r.rel_indent(-2) + r.writeln(f"#endif // {fn.ifndef}\n") + + +def genmethod( + r: RenderBuffer, + cls_qualname: T.Optional[str], + fn: FunctionContext, + trampoline_qualname: T.Optional[str], +): + if not fn.template_impls: + _genmethod(r, cls_qualname, fn, trampoline_qualname, "") + else: + for tmpl in fn.template_impls: + _genmethod( + r, + cls_qualname, + fn, + trampoline_qualname, + f"<{', '.join(tmpl)}>", + ) + + +def _genprop(r: RenderBuffer, qualname: str, prop: PropContext): + doc = "" + if prop.doc: + doc = mkdoc(", py::doc(", prop.doc, ")") + + if prop.array_size: + r.writeln( + f'.def_property_readonly("{prop.py_name}", []({qualname}& self) {{\n' + f" return py::memoryview::from_buffer(\n" + f" &self.{prop.cpp_name}, sizeof({prop.cpp_type}),\n" + f" py::format_descriptor<{prop.cpp_type}>::value,\n" + f" {{{prop.array_size}}}, {{sizeof({prop.cpp_type})}},\n" + f' { "true" if prop.readonly else "false" }\n' + " );\n" + f"}}{doc})" + ) + elif prop.array: + # cannot sensibly autowrap an array of incomplete size + pass + elif prop.reference or prop.bitfield: + propdef = ".def_property_readonly" if prop.readonly else ".def_property" + + r.writeln(f'{propdef}("{prop.py_name}",') + with r.indent(2): + lines = [ + f"[](const {qualname}& self) -> {prop.cpp_type} {{ return self.{prop.cpp_name}; }}" + ] + if not prop.readonly: + lines.append( + f"[]({qualname}& self, {prop.cpp_type} v) {{ self.{prop.cpp_name} = v; }}" + ) + if doc: + lines.append(doc[2:]) + + r.writeln(",\n".join(lines)) + r.writeln(")") + + else: + propdef = ".def_readonly" if prop.readonly else ".def_readwrite" + if prop.static: + propdef = f"{propdef}_static" + + r.writeln(f'{propdef}("{prop.py_name}", &{qualname}::{prop.cpp_name}{doc})') + + +def enum_decl(r: RenderBuffer, enum: EnumContext, varname: str): + r.writeln(f"py::enum_<{ enum.full_cpp_name }> {varname};") + + +def enum_init_args(scope: str, enum: EnumContext): + params = [scope, f'"{enum.py_name}"'] + + if enum.arithmetic: + params.append("py::arithmetic()") + + if enum.doc: + params.append(mkdoc("", enum.doc, "")) + + return ", ".join(params) + + +def enum_def(r: RenderBuffer, varname: str, enum: EnumContext): + for val in enum.values: + doc = mkdoc(",", val.doc, "") + r.writeln(f'.value("{val.py_name}", {val.full_cpp_name}{doc})') + + if enum.inline_code: + r.write_trim(enum.inline_code) + r.writeln(";") + + +def cls_user_using(r: RenderBuffer, cls: ClassContext): + for typealias in cls.user_typealias: + r.writeln(f"{typealias};") + + +def cls_auto_using(r: RenderBuffer, cls: ClassContext): + for ccls in cls.child_classes: + if not ccls.template: + r.writeln( + f"using {ccls.cpp_name} [[maybe_unused]] = typename {ccls.full_cpp_name};" + ) + for enum in cls.enums: + if enum.cpp_name: + r.writeln( + f"using {enum.cpp_name} [[maybe_unused]] = typename {enum.full_cpp_name};" + ) + for typealias in cls.auto_typealias: + r.writeln(f"{typealias};") + + +def cls_consts(r: RenderBuffer, cls: ClassContext): + if cls.constants: + r.writeln() + for constant in cls.constants: + r.writeln(f"static constexpr auto {constant[0]} = {constant[1]};") + + +def cls_decl(r: RenderBuffer, cls: ClassContext): + if cls.trampoline: + tctx = cls.trampoline + r.writeln(f"using {tctx.var} = {tctx.full_cpp_name};") + r.writeln( + f'static_assert(std::is_abstract<{tctx.var}>::value == false, "{cls.full_cpp_name} " RPYBUILD_BAD_TRAMPOLINE);' + ) + + class_params = [f"typename {cls.full_cpp_name}"] + if cls.nodelete: + class_params.append( + f"std::unique_ptr" + ) + + if cls.trampoline: + class_params.append(cls.trampoline.var) + + if cls.bases: + bases = ", ".join(base.full_cpp_name_w_templates for base in cls.bases) + class_params.append(bases) + + r.writeln(f"py::class_<{', '.join(class_params)}> {cls.var_name};") + + if cls.enums: + r.writeln() + for index, enum in enumerate(cls.enums, start=1): + enum_decl(r, enum, f"{cls.var_name}_enum{index}") + + for ccls in cls.child_classes: + if not ccls.template: + cls_decl(r, ccls) + + +def cls_init(r: RenderBuffer, cls: ClassContext, name: str): + + init_params = [cls.scope_var, name] + + if cls.final: + init_params.append("py::is_final()") + + if cls.force_multiple_inheritance: + init_params.append("py::multiple_inheritance()") + + r.writeln(f'{cls.var_name}({", ".join(init_params)}),') + + for idx, enum in enumerate(cls.enums, start=1): + r.writeln(f"{cls.var_name}_enum{idx}({enum_init_args(cls.var_name, enum)}),") + + for ccls in cls.child_classes: + if not ccls.template: + cls_init(r, ccls, f'"{ccls.py_name}"') + + +def cls_def_enum(r: RenderBuffer, cctx: ClassContext, varname: str): + for idx, enum in enumerate(cctx.enums, start=1): + r.writeln(f"{cctx.var_name}_enum{idx}") + with r.indent(): + enum_def(r, cctx.var_name, enum) + + +def cls_def(r: RenderBuffer, cls: ClassContext, varname: str): + if cls.vcheck_fns: + for fn in cls.vcheck_fns: + assert fn.cpp_code is not None + + r.writeln("{") + with r.indent(): + r.writeln(f"auto vcheck = {fn.cpp_code.strip()};") + + sig_params = [f"{cls.full_cpp_name}*"] + sig_params.extend(p.full_cpp_type for p in fn.all_params) + + signature = f"{fn.cpp_return_type}({', '.join(sig_params)})" + r.writeln( + f"static_assert(std::is_convertible>::value," + ) + r.writeln( + f' "{cls.full_cpp_name}::{fn.cpp_name} must have virtual_xform if cpp_code signature doesn\'t match original function");' + ) + r.writeln("}") + + if cls.doc: + r.writeln(f'{varname}.doc() = {mkdoc("", cls.doc, "")};') + + r.writeln(varname) + with r.indent(): + + if cls.add_default_constructor: + r.writeln(".def(py::init<>(), release_gil())") + + for fn in cls.wrapped_public_methods: + genmethod(r, cls.full_cpp_name, fn, None) + + if cls.trampoline is not None: + for fn in cls.wrapped_protected_methods: + genmethod(r, cls.full_cpp_name, fn, cls.trampoline.var) + + for prop in cls.public_properties: + _genprop(r, cls.full_cpp_name, prop) + + if cls.trampoline is not None: + for prop in cls.protected_properties: + _genprop(r, cls.trampoline.full_cpp_name, prop) + + if cls.inline_code: + r.writeln() + r.write_trim(cls.inline_code) + + r.writeln(";") + + if cls.unnamed_enums: + r.writeln() + for enum in cls.unnamed_enums: + for val in enum.values: + r.writeln( + f'{varname}.attr("{val.py_name}") = (int){val.full_cpp_name};' + ) + + for ccls in cls.child_classes: + if not ccls.template: + cls_def(r, ccls, ccls.var_name) diff --git a/robotpy_build/autowrap/render_tmpl_inst.py b/robotpy_build/autowrap/render_tmpl_inst.py new file mode 100644 index 00000000..028233ba --- /dev/null +++ b/robotpy_build/autowrap/render_tmpl_inst.py @@ -0,0 +1,67 @@ +from .buffer import RenderBuffer +from .context import HeaderContext, TemplateInstanceContext + +from .render_cls_prologue import render_class_prologue + + +def render_template_inst_cpp( + hctx: HeaderContext, tmpl_data: TemplateInstanceContext +) -> str: + r = RenderBuffer() + render_class_prologue(r, hctx) + + tmpl_params = ", ".join(tmpl_data.params) + + r.write_trim( + f""" + #include + #include "{ hctx.hname }_tmpl.hpp" + + namespace rpygen {{ + + using BindType = rpygen::bind_{ tmpl_data.full_cpp_name_identifier }<{tmpl_params}>; + static std::unique_ptr inst; + + { tmpl_data.binder_typename }::{ tmpl_data.binder_typename }(py::module &m, const char * clsName) + {{ + inst = std::make_unique(m, clsName); + }} + + void { tmpl_data.binder_typename }::finish(const char *set_doc, const char *add_doc) + {{ + inst->finish(set_doc, add_doc); + inst.reset(); + }} + + }}; // namespace rpygen + """ + ) + r.writeln() + return r.getvalue() + + +def render_template_inst_hpp(hctx: HeaderContext) -> str: + r = RenderBuffer() + r.write_trim( + f""" + // This file is autogenerated. DO NOT EDIT + #pragma once + #include + + namespace rpygen {{ + """ + ) + + for tmpl_data in hctx.template_instances: + r.writeln() + r.write_trim( + f""" + struct {tmpl_data.binder_typename} {{ + {tmpl_data.binder_typename}(py::module &m, const char * clsName); + void finish(const char *set_doc, const char *add_doc); + }}; + """ + ) + + r.writeln("\n}; // namespace rpygen") + return r.getvalue() diff --git a/robotpy_build/autowrap/render_wrapped.py b/robotpy_build/autowrap/render_wrapped.py new file mode 100644 index 00000000..7ed29616 --- /dev/null +++ b/robotpy_build/autowrap/render_wrapped.py @@ -0,0 +1,196 @@ +from .buffer import RenderBuffer +from .context import HeaderContext + +from . import render_pybind11 as rpybind11 +from .render_cls_prologue import render_class_prologue + + +def render_wrapped_cpp(hctx: HeaderContext) -> str: + """ + This contains the primary binding code generated from parsing a single + header file. There are also per-class headers generated (templates, + trampolines), and those are included/used by this. + """ + r = RenderBuffer() + + render_class_prologue(r, hctx) + + if hctx.template_instances: + r.writeln(f'\n#include "{hctx.hname}_tmpl.hpp"') + + if hctx.extra_includes: + r.writeln() + for inc in hctx.extra_includes: + r.writeln(f"#include <{inc}>") + + if hctx.user_typealias: + r.writeln() + for typealias in hctx.user_typealias: + r.writeln(f"{typealias};") + + # + # Ordering of the initialization function + # + # - namespace/typealiases + # - global enums + # - templates (because CRTP) + # - class declarations + # - class enums + # - class methods + # - global methods + # + # Additionally, we use two-part initialization to ensure that documentation + # strings are generated properly. First part is to register the class with + # pybind11, second part is to generate all the methods/etc for it. + # + # TODO: make type_traits optional by detecting trampoline + + r.writeln("\n#include ") + + if hctx.namespaces: + r.writeln() + for ns in hctx.namespaces: + r.writeln(f"using namespace {ns};") + + r.writeln(f"\nstruct rpybuild_{hctx.hname}_initializer {{\n") + + with r.indent(): + for cls in hctx.classes: + if not cls.template: + rpybind11.cls_user_using(r, cls) + rpybind11.cls_consts(r, cls) + + if hctx.subpackages: + r.writeln() + for vname in hctx.subpackages.values(): + r.writeln(f"py::module {vname};") + + # enums + for index, enum in enumerate(hctx.enums, start=1): + rpybind11.enum_decl(r, enum, f"enum{index}") + + # template decls + for tmpl_data in hctx.template_instances: + if not tmpl_data.matched: + r.writeln(f"rpygen::{tmpl_data.binder_typename} {tmpl_data.var_name};") + + # class decls + for cls in hctx.classes: + if cls.template is None: + r.writeln() + rpybind11.cls_decl(r, cls) + elif cls.template.instances: + r.writeln() + for tmpl_data in cls.template.instances: + r.writeln( + f"rpygen::{tmpl_data.binder_typename} {tmpl_data.var_name};" + ) + + r.writeln("\npy::module &m;\n") + r.writeln(f"rpybuild_{hctx.hname}_initializer(py::module &m) :") + + with r.indent(): + for pkg, vname in hctx.subpackages.items(): + r.writeln(f'{vname}(m.def_submodule("{pkg}")),') + + for index, enum in enumerate(hctx.enums, start=1): + r.writeln( + f"enum{index}({rpybind11.enum_init_args(enum.scope_var, enum)})," + ) + + for tmpl_data in hctx.template_instances: + if not tmpl_data.matched: + r.writeln( + f'{tmpl_data.var_name}({tmpl_data.scope_var}, "{tmpl_data.py_name}"),' + ) + + for cls in hctx.classes: + if not cls.template: + rpybind11.cls_init(r, cls, f'"{cls.py_name}"') + else: + for tmpl_data in cls.template.instances: + r.writeln( + f'{tmpl_data.var_name}({tmpl_data.scope_var}, "{tmpl_data.py_name}"),' + ) + + r.writeln("m(m)") + + if hctx.enums or hctx.classes: + r.writeln("{") + with r.indent(): + # enums can go in the initializer because they cant have dependencies, + # and then we dont need to figure out class dependencies for enum arguments + + for index, enum in enumerate(hctx.enums, start=1): + r.writeln(f"enum{index}") + with r.indent(): + rpybind11.enum_def(r, enum.scope_var, enum) + + for cls in hctx.classes: + rpybind11.cls_def_enum(r, cls, cls.var_name) + for ccls in cls.child_classes: + rpybind11.cls_def_enum(r, ccls, ccls.var_name) + r.writeln("}") + else: + r.writeln("{}") + + r.writeln("\nvoid finish() {\n") + + with r.indent(): + + # Templates + for tdata in hctx.template_instances: + r.writeln(f"\n{tdata.var_name}.finish(") + with r.indent(): + if tdata.doc_set: + r.writeln(f'{rpybind11.mkdoc("", tdata.doc_set, "")},') + else: + r.writeln("nullptr,") + + if tdata.doc_add: + r.writeln(rpybind11.mkdoc("", tdata.doc_add, "")) + else: + r.writeln("nullptr") + r.writeln(");") + + # Class methods + for cls in hctx.classes: + if not cls.template: + r.writeln("{") + with r.indent(): + rpybind11.cls_auto_using(r, cls) + rpybind11.cls_def(r, cls, cls.var_name) + r.writeln("}") + + # Global methods + if hctx.functions: + r.writeln() + for fn in hctx.functions: + if not fn.ignore_py: + r.writeln(fn.scope_var) + with r.indent(1): + rpybind11.genmethod(r, None, fn, None) + r.writeln(";") + + if hctx.inline_code: + r.writeln() + r.write_trim(hctx.inline_code) + + r.writeln("}") + + r.writeln( + f"}}; // struct rpybuild_{hctx.hname}_initializer\n" + "\n" + f"static std::unique_ptr cls;\n" + "\n" + f"void begin_init_{hctx.hname}(py::module &m) {{\n" + f" cls = std::make_unique(m);\n" + "}\n" + "\n" + f"void finish_init_{hctx.hname}() {{\n" + " cls->finish();\n" + " cls.reset();\n" + "}\n" + ) + + return r.getvalue() diff --git a/robotpy_build/autowrap/writer.py b/robotpy_build/autowrap/writer.py index 8eba1cec..3b06da62 100644 --- a/robotpy_build/autowrap/writer.py +++ b/robotpy_build/autowrap/writer.py @@ -1,43 +1,18 @@ import json import os from os.path import join -import pathlib import pprint import typing -import jinja2 - -from .j2_context import HeaderContext - -templates_path = pathlib.Path(__file__).parent.absolute() +from .context import HeaderContext +from .render_wrapped import render_wrapped_cpp +from .render_cls_rpy_include import render_cls_rpy_include_hpp +from .render_tmpl_inst import render_template_inst_cpp, render_template_inst_hpp _emit_j2_debug = os.getenv("RPYBUILD_J2_DEBUG") == "1" class WrapperWriter: - def __init__(self) -> None: - # - # Load all the templates first - # - - self.env = jinja2.Environment( - loader=jinja2.FileSystemLoader(searchpath=templates_path), - undefined=jinja2.StrictUndefined, - autoescape=False, - auto_reload=False, - # trim_blocks=True, - # lstrip_blocks=True, - ) - - # c++ file generated per-header - self.header_cpp_j2 = self.env.get_template("header.cpp.j2") - - # class templates - self.cls_tmpl_inst_cpp_j2 = self.env.get_template("cls_tmpl_inst.cpp.j2") - self.cls_tmpl_inst_hpp_j2 = self.env.get_template("cls_tmpl_inst.hpp.j2") - - # rpy-include trampoline file - self.rpy_include_hpp_j2 = self.env.get_template("cls_rpy_include.hpp.j2") def write_files( self, @@ -62,9 +37,9 @@ def write_files( fname = join(cxx_gen_dir, f"{name}.cpp") generated_sources.append(fname) with open(fname, "w", encoding="utf-8") as fp: - fp.write(self.header_cpp_j2.render(data)) + fp.write(render_wrapped_cpp(hctx)) - # Then the json, no need for jinja here + # Then the json with open(classdeps_json_fname, "w", encoding="utf-8") as fp: json.dump(hctx.class_hierarchy, fp) @@ -74,12 +49,11 @@ def write_files( if not cls.template and not cls.trampoline: continue - data["cls"] = cls fname = join( hppoutdir, f"{cls.namespace.replace(':', '_')}__{cls.cpp_name}.hpp" ) with open(fname, "w", encoding="utf-8") as fp: - fp.write(self.rpy_include_hpp_j2.render(data)) + fp.write(render_cls_rpy_include_hpp(hctx, cls)) # Each class template is instantiated in a separate cpp file to lessen # compiler memory requirements when compiling obnoxious templates @@ -87,7 +61,7 @@ def write_files( # Single header output that holds all the struct outlines fname = join(cxx_gen_dir, f"{name}_tmpl.hpp") with open(fname, "w", encoding="utf-8") as fp: - fp.write(self.cls_tmpl_inst_hpp_j2.render(data)) + fp.write(render_template_inst_hpp(hctx)) # Each cpp file has a single class template instance for i, tmpl_data in enumerate(hctx.template_instances): @@ -95,12 +69,6 @@ def write_files( fname = join(hppoutdir, f"{name}_tmpl{i+1}.cpp") generated_sources.append(fname) with open(fname, "w", encoding="utf-8") as fp: - fp.write(self.cls_tmpl_inst_cpp_j2.render(data)) + fp.write(render_template_inst_cpp(hctx, tmpl_data)) return generated_sources - - def _render_template(self, tmpl_src: str, dst: str, data: dict): - jtmpl = self.env.get_template(tmpl_src) - content = jtmpl.render(data) - with open(dst, "w", encoding="utf-8") as fp: - fp.write(content) diff --git a/setup.cfg b/setup.cfg index d439b90c..e1d3bd2e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -27,7 +27,6 @@ install_requires = setuptools_scm >= 6.2, < 8 sphinxify >= 0.7.3 pydantic >= 1.7.0, < 2 - jinja2 cxxheaderparser[pcpp] ~= 1.2 tomli tomli_w diff --git a/tests/cpp/gen/ft/nested.yml b/tests/cpp/gen/ft/nested.yml index e68d3bd6..1ddbaddd 100644 --- a/tests/cpp/gen/ft/nested.yml +++ b/tests/cpp/gen/ft/nested.yml @@ -2,6 +2,8 @@ classes: OuterNested: methods: OuterNested: + getInner: + return_value_policy: reference_internal OuterNested::InnerNested: methods: fn: diff --git a/tests/cpp/rpytest/ft/include/docstrings.h b/tests/cpp/rpytest/ft/include/docstrings.h index 0c5c2394..f8ed1d37 100644 --- a/tests/cpp/rpytest/ft/include/docstrings.h +++ b/tests/cpp/rpytest/ft/include/docstrings.h @@ -26,6 +26,9 @@ struct DocClass /** An awesome variable, use it for something */ int sweet_var; + /** this is a bitfield */ + int bitfield: 1; + /** * Construct a Ramsete unicycle controller. * diff --git a/tests/cpp/rpytest/ft/include/nested.h b/tests/cpp/rpytest/ft/include/nested.h index 22eb3140..c7639113 100644 --- a/tests/cpp/rpytest/ft/include/nested.h +++ b/tests/cpp/rpytest/ft/include/nested.h @@ -14,9 +14,14 @@ struct OuterNested { // bug where InnerNested needs to be in binding scope OuterNested(std::vector i) {} + InnerNested &getInner() { + return in; + } + private: // lol bug struct InnerPrivate {}; - + + InnerNested in; }; diff --git a/tests/cpp/run_install.py b/tests/cpp/run_install.py index 28199ac1..2b45723c 100755 --- a/tests/cpp/run_install.py +++ b/tests/cpp/run_install.py @@ -65,6 +65,9 @@ def http_server(): if len(sys.argv) == 2 and sys.argv[1] == "wheel": cmd_args = [sys.executable, "-m", "build", "--wheel", "--no-isolation"] cwd = root + elif len(sys.argv) == 2 and sys.argv[1] == "develop": + cmd_args = [sys.executable, "setup.py", "develop", "-N"] + cwd = root else: # run pip install cmd_args = [