Skip to content
Snippets Groups Projects
Commit 711447d7 authored by Sebastian Hahta's avatar Sebastian Hahta
Browse files

py: automatically create instance, fix exit crash

parent 916d57a7
Branches
No related tags found
No related merge requests found
Pipeline #33788 passed
/**
* @file system.hpp
* @file defines.hpp
* @copyright Copyright (c) 2020 Sebastian Hahta, MIT License
* @author Sebastian Hahta
*/
......@@ -20,3 +20,15 @@
/// Lifetime of the return value is tied to the lifetime of a parent object
#define PY_RV_LIFETIME_PARENT
#endif
#ifndef PY_SINGLETON
/// Singleton instance, members exported to module. Requires creating the
/// instance in PyModule constructor.
#define PY_SINGLETON
#endif
#ifndef PY_SINGLETON_OBJECT
/// Export as singleton instance instead of exporting members to module
#define PY_SINGLETON_OBJECT
#endif
......@@ -42,5 +42,5 @@ namespace voltu
*
* @return Singleton VolTu runtime instance.
*/
PY_API std::shared_ptr<voltu::System> instance();
std::shared_ptr<voltu::System> instance();
}
......@@ -32,9 +32,10 @@ struct Version
*
* Provides access to the key components such as opening streams or files and
* creating virtual cameras. Use `voltu::instance()` to obtain the object. All
* object instances in VolTu are managed by shared smart pointers.
* object instances in VolTu are managed by shared smart pointers. Python API
* automatically creates instance available as voltu.System.
*/
class System
PY_SINGLETON_OBJECT class System
{
public:
virtual ~System() = default;
......
......@@ -18,7 +18,7 @@ add_custom_command(
${CMAKE_CURRENT_SOURCE_DIR}/../include
${SDK_AUTO_HEADERS}
DEPENDS voltu_sdk
DEPENDS voltu_sdk gen.py
)
pybind11_add_module(voltu_sdk_py MODULE
......
# Python API generator
Dependencies
* python3-ply (build only)
* python3-dev
* python3-ply
Build system uses Pybind11 to generate a python module. Most of the bindings are
automatically generated by gen.py script which is automatically called by CMake
......@@ -14,11 +15,14 @@ build. Several (empty) macros are used in headers to annoate Python API details.
* PY_API function/method is to be included in Python API
* PY_NO_SHARED_PTR shared_ptr<> is not used with instances of this class.
https://pybind11.readthedocs.io/en/latest/advanced/smart_ptrs.html?#std-shared-ptr
See [pybind11 documentation](https://pybind11.readthedocs.io/en/latest/advanced/smart_ptrs.html?#std-shared-ptr)
for techncal details.
* PY_RV_LIFETIME_PARENT lifetime of method's return valued is tied to
lifetime of parent objects (this). (return_value_policy::reference_internal
lifetime of parent objects (this). (
[return_value_policy::reference_internal](https://pybind11.readthedocs.io/en/latest/advanced/functions.html#return-value-policies)
is set for this method)
https://pybind11.readthedocs.io/en/latest/advanced/functions.html#return-value-policies
* PY_SINGLETON Singleton class, methods are exported to to module scope.
* PY_SINGLETON_OBJECT Singleton instance is accessible as module attribute.
## Notes:
* Binding to default constructor is generated for structs. Class constructors
......@@ -28,6 +32,10 @@ build. Several (empty) macros are used in headers to annoate Python API details.
* Default arguments are supported (extracted from header).
* Public class properties are available in python, read-only if const,
otherwise read write.
* Singletons have to be created in PyModule constructor and be available
as class members.
* Exceptions have to be included manually in module.cpp
* C++ preprocessor is not used
## Not supported (yet) in automatic binding generation:
* Nested classes
......@@ -35,3 +43,4 @@ build. Several (empty) macros are used in headers to annoate Python API details.
* Constructors
* Automatic documentation (Doxygen)
* Generator does not verify that shared_ptr<> is used consistently/correctly
* Member variables of singleton classes
{includes}
#include <pybind11/pybind11.h>
#include <pybind11/stl.h>
#include <pybind11/eigen.h>
#include "types/image.hpp"
namespace py = pybind11;
using namespace voltu;
void py_automatic_bindings(py::module& m) {{
{code}
}}
......@@ -3,7 +3,6 @@
import cv2
import voltu
import time
import sys
import os
......@@ -11,23 +10,15 @@ if len(sys.argv) != 2:
print("%s filename" % os.path.basename(__file__))
exit(1)
#if not os.path.exists(sys.argv[1]):
# print("can't find %s" % sys.argv[1])
# exit(1)
api = voltu.instance()
api = voltu.System
api.open(sys.argv[1])
room = api.getRoom(0)
while True:
try:
room.waitNextFrame(1000)
if room.waitNextFrame(1000):
frames = room.getFrame().getImageSet(voltu.Channel.kColour)
im = frames[0].getHost()
cv2.imshow("im", im)
if cv2.waitKey(10) == 27:
break
except Exception as e:
print(e)
......@@ -3,7 +3,6 @@
import cv2
import voltu
import time
import sys
import os
......@@ -11,18 +10,13 @@ if len(sys.argv) != 2:
print("%s filename" % os.path.basename(__file__))
exit(1)
#if not os.path.exists(sys.argv[1]):
# print("can't find %s" % sys.argv[1])
# exit(1)
api = voltu.instance()
api = voltu.System
api.open(sys.argv[1])
room = api.getRoom(0)
cam = api.createCamera()
while True:
try:
room.waitNextFrame(1000)
if room.waitNextFrame(1000):
cam.submit(room.getFrame())
frames = cam.getFrame().getImageSet(voltu.Channel.kColour)
im = frames[0].getHost()
......@@ -30,6 +24,3 @@ while True:
if cv2.waitKey(10) == 27:
break
except Exception as e:
print(e)
#!/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
......@@ -19,12 +44,12 @@ def get_loc_msg(data):
def print_warn(msg, loc=None):
if loc is not None:
msg += " " + get_loc_msg(loc)
print("WARNING: %s" % msg, file=sys.stderr)
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("ERROR: %s" % msg, file=sys.stderr)
print("gen.py error: %s" % msg, file=sys.stderr)
def include_in_api(data):
return "PY_API" in data["debug"]
......@@ -49,23 +74,14 @@ def create_enum_bindigs(enum, parent=[], pybind_handle="", export_values=False):
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)]
def process_func_args(func):
args = []
for param in func["parameters"]:
param_name = param["name"]
if param_name == "&":
print_warn("Argument name missing for %s" % full_name_cpp, func)
if param_name in ["&", "", "*"]:
print_warn("Argument name missing for %s" % func["name"])
continue
if "default" in param:
......@@ -74,9 +90,50 @@ def create_function_bindings(func, parent=[], pybind_handle=None):
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:
print_err("PY_RV_LIFETIME_PARENT used for function", func)
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")
......@@ -88,6 +145,8 @@ def create_function_bindings(func, parent=[], pybind_handle=None):
return cpp
def create_class_bindings(cls, parent=[], pybind_handle=""):
""" Create bindings for a class """
cls_name = cls["name"]
full_name = parent + [cls_name]
cpp = []
......@@ -116,8 +175,30 @@ def create_class_bindings(cls, parent=[], pybind_handle=""):
return "\n\t\t".join(cpp)
if __name__ == "__main__":
def create_singleton_bindings(instance_ptr, cls, 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(cls, parent, pybind_handle) + ";",
"{0}.attr(\"{1}\") = {2};".format(pybind_handle, cls["name"], instance_ptr)
])
else:
# use C++ lambdas to wrap all methods
cpp = []
for func in cls["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):
......@@ -131,6 +212,7 @@ if __name__ == "__main__":
out = []
includes = []
for fname in fsin:
includes.append(fname)
......@@ -145,6 +227,12 @@ if __name__ == "__main__":
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:
......@@ -153,9 +241,7 @@ if __name__ == "__main__":
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)
#include "module.hpp"
#include <voltu/voltu.hpp>
#include <voltu/initialise.hpp>
#include <voltu/types/errors.hpp>
namespace py = pybind11;
void py_exceptions(pybind11::module& m) {
PyModule::PyModule() {
instance_System = voltu::instance();
}
void PyModule::py_exceptions(pybind11::module& m) {
py::register_exception<voltu::exceptions::Exception>(m, "Error");
py::register_exception<voltu::exceptions::BadImageChannel>(m, "ErrorBadImageChannel");
py::register_exception<voltu::exceptions::NoFrame>(m, "ErrorNoFrame");
......@@ -15,8 +18,11 @@ void py_exceptions(pybind11::module& m) {
py::register_exception<voltu::exceptions::BadSourceURI>(m, "ErrorBadSourceURI");
}
static auto module = PyModule();
PYBIND11_MODULE(voltu, m) {
m.attr("version") = py::make_tuple(VOLTU_VERSION_MAJOR, VOLTU_VERSION_MINOR, VOLTU_VERSION_PATCH);
py_exceptions(m);
py_automatic_bindings(m);
module.py_exceptions(m);
module.py_automatic_bindings(m);
}
#pragma once
#include <pybind11/pybind11.h>
#include <voltu/voltu.hpp>
#include <voltu/system.hpp>
class PyModule {
public:
PyModule();
void py_automatic_bindings(pybind11::module& m);
void py_exceptions(pybind11::module& m);
private:
std::shared_ptr<voltu::System> instance_System;
};
import unittest
import os
import voltu
class LoadLibrary(unittest.TestCase):
def test_get_instance(self):
self.assertIsNotNone(voltu.instance())
# second call to instance() returns None; should
# return same instance instead?
# self.assertIsNotNone(voltu.instance())
def test_import(self):
import voltu
def test_import_twice(self):
# verify that System instance is created just once
import voltu
import voltu
self.assertIsNotNone(voltu.System)
def test_version(self):
import voltu
major, minor, patch = voltu.version
if __name__ == '__main__':
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment