#!/usr/bin/env python3 """ Python binding generator. Creates pybind11 bindings for given headers. See README.md for details. """ template = """ /* This file was automatically generated by gen.py. */ {includes} #include <pybind11/pybind11.h> #include <pybind11/stl.h> #include <pybind11/eigen.h> #include "types/image.hpp" #include "module.hpp" namespace py = pybind11; using namespace voltu; void PyModule::py_automatic_bindings(py::module& m) {{ {code} }} """ import sys import os from CppHeaderParser import CppHeader, CppParseError # Extract line from file, as not directly with CppHeader def read_line(file, lineno): with open(file) as f: for i, line in enumerate(f, 1): if i == lineno: return line def get_loc_msg(data): return "({0}:{1})".format(data["filename"], data["line_number"]) def print_warn(msg, loc=None): if loc is not None: msg += " " + get_loc_msg(loc) print("gen.py warning: %s" % msg, file=sys.stderr) def print_err(msg, loc=None): if loc is not None: msg += " " + get_loc_msg(loc) print("gen.py error: %s" % msg, file=sys.stderr) def include_in_api(data): return "PY_API" in data["debug"] def no_shared_ptr_cls(clsdata): return (clsdata["declaration_method"] == "struct") or ("PY_NO_SHARED_PTR" in clsdata["debug"]) def create_enum_bindigs(enum, parent=[], pybind_handle="", export_values=False): name_full = parent + [enum["name"]] name_py = enum["name"] cpp = [] cpp.append("py::enum_<{name_full}>({handle}, \"{name}\")".format( name_full = "::".join(name_full), handle = pybind_handle, name = name_py )) for val in enum["values"]: cpp.append("\t.value(\"{name}\", {name_cpp})".format( name = val["name"], name_cpp = "::".join(name_full + [val["name"]]) )) if export_values: cpp.append("\t.export_values()") return "\n\t".join(cpp) def process_func_args(func): args = [] for param in func["parameters"]: param_name = param["name"] if param_name in ["&", "", "*"]: print_warn("Argument name missing for %s" % func["name"]) continue if "default" in param: args.append("py::arg(\"{0}\") = {1}".format( param_name, param["default"])) else: args.append("py::arg(\"{0}\")".format(param_name)) return args def wrap_lambda(func, instance_ptr, capture=True): """ Wrap func to instance (singleton) with C++ lambda """ args_lambda = [] args_lambda_sig = [] for i, param in enumerate(func["parameters"]): argname = "p" + str(i) args_lambda_sig.append("%s %s" % (param["type"], argname)) args_lambda.append(argname) return "[%s](%s){ return %s->%s(%s); }" % ( ("&" if capture else ""), ", ".join(args_lambda_sig), instance_ptr, func["name"], ", ".join(args_lambda)) def create_function_bindings(func, parent=[], pybind_handle=None, bind_to_singleton=None): """ Create function bindings, if bind_to_singleton is set, C++ lamda is generated to wrap the call to instance (named by bind_by_singleton). """ func_name = func["name"] full_name = parent + [func_name] full_name_cpp = "::".join(full_name) if not include_in_api(func): print_err("%s not included in Python API" % full_name_cpp, func) raise ValueError("No PY_API") args = ["\"{0}\"".format(func_name)] if bind_to_singleton is None: args.append("&{0}".format(full_name_cpp)) else: args.append(wrap_lambda(func, bind_to_singleton)) args += process_func_args(func) if "PY_RV_LIFETIME_PARENT" in func["debug"]: if func["parent"] is None or bind_to_singleton is not None: print_err("PY_RV_LIFETIME_PARENT used for function or singleton", func) raise ValueError() args.append("py::return_value_policy::reference_internal") cpp = "def({0})".format(", ".join(args)) if pybind_handle is not None: cpp = pybind_handle + "." + cpp return cpp def create_class_bindings(clsdata, parent=[], pybind_handle=""): """ Create bindings for a class """ clsdata_name = clsdata["name"] full_name = parent + [clsdata_name] cpp = [] if no_shared_ptr_cls(clsdata): cls_cpp = "py::class_<{name}>({handle}, \"{name}\")" else: cls_cpp = "py::class_<{name}, std::shared_ptr<{name}>>({handle}, \"{name}\")" cpp.append(cls_cpp.format(handle=pybind_handle, name=clsdata_name)) if clsdata["declaration_method"] == "struct": cpp.append(".def(py::init<>())") for method in clsdata["methods"]["public"]: if include_in_api(method): cpp.append("." + create_function_bindings(method, full_name)) for field in clsdata["properties"]["public"]: if field["constant"]: field_cpp = ".def_property_readonly(\"{name}\", &{cpp_name})" else: field_cpp = ".def_readwrite(\"{name}\", &{cpp_name})" field_name = field["name"] field_name_cpp = "::".join(full_name + [field_name]) cpp.append(field_cpp.format(name=field_name, cpp_name=field_name_cpp)) return "\n\t\t".join(cpp) def create_singleton_bindings(instance_ptr, clsdata, parent=[], pybind_handle="", export=False): """ Singleton class bindings, either creates a singleton instance or exports functions directly to module. Singleton pointer available with instance_ptr, which should be class member of PyModule. """ if not export: # use usual bindings and export as attribute to pybind handle return "\n\t".join([ create_class_bindings(clsdata, parent, pybind_handle) + ";", "{0}.attr(\"{1}\") = {2};".format(pybind_handle, clsdata["name"], instance_ptr) ]) else: # use C++ lambdas to wrap all methods cpp = [] for func in clsdata["methods"]["public"]: if not include_in_api(func): continue cpp.append(create_function_bindings(func, parent, pybind_handle, instance_ptr) + ";") return "\n\t".join(cpp) if __name__ == "__main__": from pprint import pprint if (len(sys.argv) < 4): print("gen.py output include_directory input files ...") exit(1) handle = "m" fout = sys.argv[1] includedir = sys.argv[2] fsin = sys.argv[3:] out = [] includes = [] for fname in fsin: includes.append(fname) hdr = CppHeader(os.path.join(includedir, fname)) # note .strip("::"), inconsistent behavior for classes vs enum/func for data in hdr.enums: ns = data["namespace"].strip("::") # bug? in parser out.append(create_enum_bindigs(data, [ns], handle) + ";") for data in hdr.classes.values(): ns = data["namespace"] # workaround: parser does not save debug in same way as for funcs data["debug"] = read_line(data["filename"], data["line_number"]) if "PY_SINGLETON_OBJECT" in data["debug"]: out.append(create_singleton_bindings("instance_" + data["name"], data, [ns], handle, False)) elif "PY_SINGLETON" in data["debug"]: out.append(create_singleton_bindings("instance_" + data["name"], data, [ns], handle, True)) else: out.append(create_class_bindings(data, [ns], handle) + ";") for data in hdr.functions: ns = data["namespace"].strip("::") # bug? in parser if include_in_api(data): out.append(create_function_bindings(data, [ns], handle) + ";") includes = "\n".join("#include <{0}>".format(i) for i in includes) out = template.format(includes=includes, code="\n\t".join(out)) with open(fout, "w",) as f: f.write(out)