Skip to content
Snippets Groups Projects
gen.py 5.08 KiB
Newer Older
Nicolas Pope's avatar
Nicolas Pope committed
#!/usr/bin/env python3

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("WARNING: %s" % msg, file=sys.stderr)

def print_err(msg, loc=None):
    if loc is not None:
        msg += " " + get_loc_msg(loc)
    print("ERROR: %s" % msg, file=sys.stderr)

def include_in_api(data):
    return "PY_API" in data["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 create_function_bindings(func, parent=[], pybind_handle=None):
    func_name = func["name"]
    full_name = parent + [func_name]
    full_name_cpp = "::".join(full_name)

    if "PY_API" not in func["debug"]:
        print_err("%s not included in Python API" % full_name_cpp, func)
        raise ValueError("No PY_API")

    args = ["\"{0}\"".format(func_name), "&{0}".format(full_name_cpp)]

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

        if  param_name == "&":
            print_warn("Argument name missing for %s" % full_name_cpp, func)

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

    if "PY_RV_LIFETIME_PARENT" in func["debug"]:
        if func["parent"] is None:
            print_err("PY_RV_LIFETIME_PARENT used for function", 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(cls, parent=[], pybind_handle=""):
    cls_name = cls["name"]
    full_name = parent + [cls_name]
    cpp = []

    if "PY_NO_SHARED_PTR" not in cls["debug"]:
        cls_cpp = "py::class_<{name}, std::shared_ptr<{name}>>({handle}, \"{name}\")"
    else:
        cls_cpp = "py::class_<{name}>({handle}, \"{name}\")"
    cpp.append(cls_cpp.format(handle=pybind_handle, name=cls_name))

    if cls["declaration_method"] == "struct":
        cpp.append(".def(py::init<>())")

    for method in cls["methods"]["public"]:
        if include_in_api(method):
            cpp.append("." + create_function_bindings(method, full_name))

    for field in cls["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)

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"])
            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)
    template_file = os.path.join(os.path.dirname(os.path.realpath(__file__)), "automatic_bindings.cpp.in")
    with open(template_file, "r") as f:
        template = f.read()
    out = template.format(includes=includes, code="\n\t".join(out))
    with open(fout, "w",) as f:
        f.write(out)