Skip to content
Snippets Groups Projects
gen.py 7.76 KiB
Newer Older
Nicolas Pope's avatar
Nicolas Pope committed
#!/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}
}}
"""

Nicolas Pope's avatar
Nicolas Pope committed
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)
Nicolas Pope's avatar
Nicolas Pope committed

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)
Nicolas Pope's avatar
Nicolas Pope committed

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"])

Nicolas Pope's avatar
Nicolas Pope committed
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 = []
Nicolas Pope's avatar
Nicolas Pope committed

    for param in func["parameters"]:
        param_name = param["name"]

        if  param_name in ["&", "", "*"]:
            print_warn("Argument name missing for %s" % func["name"])
Nicolas Pope's avatar
Nicolas Pope committed
            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)

Nicolas Pope's avatar
Nicolas Pope committed
    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)
Nicolas Pope's avatar
Nicolas Pope committed
            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]
Nicolas Pope's avatar
Nicolas Pope committed
    cpp = []

    if no_shared_ptr_cls(clsdata):
Nicolas Pope's avatar
Nicolas Pope committed
        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":
Nicolas Pope's avatar
Nicolas Pope committed
        cpp.append(".def(py::init<>())")

    for method in clsdata["methods"]["public"]:
Nicolas Pope's avatar
Nicolas Pope committed
        if include_in_api(method):
            cpp.append("." + create_function_bindings(method, full_name))

    for field in clsdata["properties"]["public"]:
Nicolas Pope's avatar
Nicolas Pope committed
        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__":
Nicolas Pope's avatar
Nicolas Pope committed
    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 = []
Nicolas Pope's avatar
Nicolas Pope committed
    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) + ";")
Nicolas Pope's avatar
Nicolas Pope committed

        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)
Nicolas Pope's avatar
Nicolas Pope committed
    out = template.format(includes=includes, code="\n\t".join(out))
    with open(fout, "w",) as f:
        f.write(out)