diff --git a/.gitmodules b/.gitmodules index b03f7351c6e6a4eaaa5521f6c7a44deded20a9a2..eca2363e385ce7bd01c2e6f4d4b4c32af9b3fe94 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,6 @@ [submodule "ext/nanogui"] path = ext/nanogui url = https://github.com/wjakob/nanogui.git -[submodule "SDK/C++/public/ext/pybind11"] - path = SDK/C++/public/ext/pybind11 +[submodule "SDK/CPP/public/ext/pybind11"] + path = SDK/CPP/public/ext/pybind11 url = https://github.com/pybind/pybind11.git diff --git a/SDK/CPP/CMakeLists.txt b/SDK/CPP/CMakeLists.txt index b64eae3d9a119df248378b8b1e41f2b271187ba1..123671c2b6581beba4ab4bd5df167ab1e033a531 100644 --- a/SDK/CPP/CMakeLists.txt +++ b/SDK/CPP/CMakeLists.txt @@ -11,17 +11,41 @@ add_library(voltu SHARED private/property_impl.cpp ) +file(READ "public/include/voltu/voltu.hpp" VOLVER) + +string(REGEX MATCH "VOLTU_VERSION_MAJOR ([0-9]*)" _ ${VOLVER}) +set(VOLTU_MAJOR ${CMAKE_MATCH_1}) + +string(REGEX MATCH "VOLTU_VERSION_MINOR ([0-9]*)" _ ${VOLVER}) +set(VOLTU_MINOR ${CMAKE_MATCH_1}) + +string(REGEX MATCH "VOLTU_VERSION_PATCH ([0-9]*)" _ ${VOLVER}) +set(VOLTU_PATCH ${CMAKE_MATCH_1}) + +message("VolTu SDK version: ${VOLTU_MAJOR}.${VOLTU_MINOR}.${VOLTU_PATCH}") + +set_target_properties( voltu PROPERTIES + VERSION "${VOLTU_MAJOR}.${VOLTU_MINOR}" + SOVERSION "${VOLTU_MAJOR}.${VOLTU_MINOR}" +) + target_include_directories(voltu PUBLIC public/include PRIVATE src) target_link_libraries(voltu ftlcommon ftldata ftlctrl ftlrgbd ftlstreams ftlrender Threads::Threads ${OpenCV_LIBS} openvr ftlnet nanogui ${NANOGUI_EXTRA_LIBS} ceres nvidia-ml) +set(SDK_BINARY_DIR "${CMAKE_CURRENT_BINARY_DIR}/sdk") + ExternalProject_Add( voltu_sdk SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/public" - BINARY_DIR "${CMAKE_CURRENT_BINARY_DIR}/sdk" + BINARY_DIR ${SDK_BINARY_DIR} INSTALL_COMMAND "" BUILD_ALWAYS true - CMAKE_ARGS -DOpenCV_DIR=${OpenCV_DIR} + CMAKE_ARGS -DOpenCV_DIR=${OpenCV_DIR} -DWITH_PYTHON=True ) + +if (BUILD_TESTS) + add_subdirectory(tests) +endif() diff --git a/SDK/CPP/private/room_impl.cpp b/SDK/CPP/private/room_impl.cpp index 764b5740341202140bfaade2bbb0207ea46ef8b8..9fd0d2d437ca39ceeef3355dfb486a2dd270367d 100644 --- a/SDK/CPP/private/room_impl.cpp +++ b/SDK/CPP/private/room_impl.cpp @@ -16,7 +16,7 @@ RoomImpl::~RoomImpl() if (filter_) filter_->remove(); } -bool RoomImpl::waitNextFrame(int64_t timeout) +bool RoomImpl::waitNextFrame(int64_t timeout, bool except) { if (!filter_) { @@ -40,10 +40,15 @@ bool RoomImpl::waitNextFrame(int64_t timeout) return last_read_ < last_seen_; }); + if (except && last_read_ >= last_seen_) + { + throw voltu::exceptions::Timeout(); + } return last_read_ < last_seen_; } else if (timeout == 0) { + if (except) throw voltu::exceptions::Timeout(); return false; } else diff --git a/SDK/CPP/private/room_impl.hpp b/SDK/CPP/private/room_impl.hpp index be8516a637b316c9a3964b14952f11fcadd23459..67d44474de421f97ba178ce509d1e72e2b76be46 100644 --- a/SDK/CPP/private/room_impl.hpp +++ b/SDK/CPP/private/room_impl.hpp @@ -16,7 +16,7 @@ public: ~RoomImpl() override; - bool waitNextFrame(int64_t) override; + bool waitNextFrame(int64_t, bool except) override; voltu::FramePtr getFrame() override; diff --git a/SDK/CPP/private/system.cpp b/SDK/CPP/private/system.cpp index 84bdbcd406793b0a1b51300b2f7ef0e860e67287..f711a3783c3edc0751d90931b2806e3b5f67e10b 100644 --- a/SDK/CPP/private/system.cpp +++ b/SDK/CPP/private/system.cpp @@ -87,6 +87,11 @@ std::list<voltu::RoomId> SystemImpl::listRooms() voltu::RoomPtr SystemImpl::getRoom(voltu::RoomId id) { + if (feed_->getURI(id).size() == 0) + { + throw voltu::exceptions::InvalidRoomId(); + } + auto s = std::make_shared<voltu::internal::RoomImpl>(feed_); s->addFrameSet(id); return s; diff --git a/SDK/CPP/public/CMakeLists.txt b/SDK/CPP/public/CMakeLists.txt index b3a4d7dc20ec1df5fc529d4a65b805855ec09c11..f1ea62a4e8a428581f6a4a7f73915d7413ac5c66 100644 --- a/SDK/CPP/public/CMakeLists.txt +++ b/SDK/CPP/public/CMakeLists.txt @@ -5,13 +5,13 @@ project (voltu_sdk VERSION 0.0.1) include(GNUInstallDirs) option(WITH_OPENCV "Build with OpenCV wrapper" ON) -option(WITH_PYTHON "Build Python module" OFF) +option(WITH_PYTHON "Build Python module" ON) -find_package( Eigen3 REQUIRED NO_MODULE ) -find_package( Threads REQUIRED ) +find_package(Eigen3 REQUIRED NO_MODULE) +find_package(Threads REQUIRED) if (WITH_OPENCV) - find_package( OpenCV REQUIRED ) + find_package(OpenCV REQUIRED) endif() if(WIN32) @@ -68,7 +68,30 @@ add_executable(voltu_fusion_evaluator ) target_link_libraries(voltu_fusion_evaluator voltu_sdk) +find_package (Python COMPONENTS Development.Module Interpreter) + +function(find_python_module module) + string(TOUPPER ${module} module_upper) + if(NOT PY_${module_upper}) + execute_process(COMMAND "${Python_EXECUTABLE}" "-c" + "import ${module}; print(${module}.__file__.rstrip('__init__.py'))" + RESULT_VARIABLE _${module}_status + OUTPUT_VARIABLE _${module}_location + ERROR_QUIET OUTPUT_STRIP_TRAILING_WHITESPACE) + if(NOT _${module}_status) + set(PY_${module_upper} ${_${module}_location} CACHE STRING + "Location of Python module ${module}") + endif(NOT _${module}_status) + endif(NOT PY_${module_upper}) + find_package_handle_standard_args(PY_${module} DEFAULT_MSG PY_${module_upper}) +endfunction(find_python_module) + if (WITH_PYTHON) - add_subdirectory(ext/pybind11) - add_subdirectory(python) + find_python_module(ply) + if (Python_FOUND AND PY_PLY) + add_subdirectory(ext/pybind11) + add_subdirectory(python) + else() + message(WARNING "Python dependencies not found, Python module is not built") + endif() endif() diff --git a/SDK/CPP/public/ext/pybind11 b/SDK/CPP/public/ext/pybind11 new file mode 160000 index 0000000000000000000000000000000000000000..06b673a0daef1db4f921a19676a51abec6fb13e8 --- /dev/null +++ b/SDK/CPP/public/ext/pybind11 @@ -0,0 +1 @@ +Subproject commit 06b673a0daef1db4f921a19676a51abec6fb13e8 diff --git a/SDK/CPP/public/include/voltu/defines.hpp b/SDK/CPP/public/include/voltu/defines.hpp index 19f2416f2812cc41b69b6c2f73a7a7f3bd4f7588..513190c63f792199e063c857f48def876794660a 100644 --- a/SDK/CPP/public/include/voltu/defines.hpp +++ b/SDK/CPP/public/include/voltu/defines.hpp @@ -1,5 +1,5 @@ /** - * @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 + diff --git a/SDK/CPP/public/include/voltu/initialise.hpp b/SDK/CPP/public/include/voltu/initialise.hpp index 4a6e5b3ffc2c2326b5934d2577d0458605ddc51c..5d34a1e691797b8c4922aa274775d7f84ef401ac 100644 --- a/SDK/CPP/public/include/voltu/initialise.hpp +++ b/SDK/CPP/public/include/voltu/initialise.hpp @@ -13,34 +13,34 @@ namespace voltu { /** * @brief Get core VolTu instance. - * + * * This method returns a smart pointer to a singleton VolTu runtime * instance and must be the first VolTu call. On any given machine it is * only sensible and possible to have one runtime instance of VolTu due to * its use of hardware devices. Multiple real instances are not possible. - * + * * @code * int main(int argc, char** argv) { * auto vtu = voltu::instance(); - * + * * vtu->open("device:camera"); * ... * } * @endcode - * + * * @note * This method must only be called once. - * + * * @throw voltu::exceptions::LibraryLoadFailed * If runtime not found or is invalid. * * @throw voltu::exceptions::RuntimeVersionMismatch * If major or minor version does not match the SDK headers. - * + * * @throw voltu::exceptions::RuntimeAlreadyInUse * If a runtime instance is in use by another application. - * + * * @return Singleton VolTu runtime instance. */ - PY_API std::shared_ptr<voltu::System> instance(); + std::shared_ptr<voltu::System> instance(); } diff --git a/SDK/CPP/public/include/voltu/room.hpp b/SDK/CPP/public/include/voltu/room.hpp index 87f0f070cc3fae52bd52a6046c6cd4ae299680ea..f2578236c85c42bc64eb4413c8ba6c616be9bfb8 100644 --- a/SDK/CPP/public/include/voltu/room.hpp +++ b/SDK/CPP/public/include/voltu/room.hpp @@ -70,7 +70,7 @@ public: * @param timeout Millisecond timeout, or 0 or -1. * @return True if a new unseen frame is available. */ - PY_API virtual bool waitNextFrame(int64_t timeout) = 0; + PY_API virtual bool waitNextFrame(int64_t timeout, bool except=false) = 0; /** * @brief Check if a new frame is available. diff --git a/SDK/CPP/public/include/voltu/system.hpp b/SDK/CPP/public/include/voltu/system.hpp index 6a7e4cd96387a6e50b4933dad6892eb52104755d..8fb9c744cdf3c7646531803f349aeb7455962c12 100644 --- a/SDK/CPP/public/include/voltu/system.hpp +++ b/SDK/CPP/public/include/voltu/system.hpp @@ -20,7 +20,7 @@ namespace voltu /** * @brief Voltu semantic versioning information. */ -struct Version +PY_NO_SHARED_PTR struct Version { int major; ///< API Incompatible change int minor; ///< Possible binary incompatible, extensions @@ -29,50 +29,51 @@ struct Version /** * @brief Singleton Voltu system instance. - * + * * 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; - + /** * @brief Get the runtime version information. - * + * * This method gets the VolTu version of the runtime shared library, which * may not be the same as the version of the SDK here. - * + * * @see voltu.hpp - * + * * @return Always returns semantic versioning structure. */ virtual voltu::Version getVersion() const = 0; /** * @brief Make a virtual room or composite room. - * + * * @return A new virtual room instance. */ PY_API virtual voltu::RoomPtr createRoom() = 0; /** * @brief Create a virtual observer. - * + * * An observer renderers virtual camera views, audio and other data from * submitted framesets. It is possible and recommended that a single * observer instance be used to renderer multiple different views, rather * than creating lots of observers. This saves memory resources. - * + * * @return A new observer instance. */ PY_API virtual voltu::ObserverPtr createObserver() = 0; /** * @brief Open a file, device or network stream using a URI. - * + * * All data sources in VolTu are represented by Universal Resource * Identifiers (URIs), with some non-standard additions. A few examples * are: @@ -83,11 +84,11 @@ public: * * `./file.ftl` * * `device:camera` * * `device:screen` - * + * * Note that returning from this call does not guarantee that the source * is fully open and operational, this depends on network handshakes or * file processing that occurs asynchronously. - * + * * @throw voltu::exceptions::BadSourceURI If an unrecognised URI is given. * @return A feed management object for the data source. */ @@ -95,14 +96,14 @@ public: /** * @brief Get a list of all available rooms. - * + * * A room is a 3D captured physical space, or a combination of such spaces, * and is represented by a unique identifier within the local system. This * method obtains a list of all available rooms from all sources. To obtain * rooms, either use `open` or `createRoom`. - * + * * @return A list of room ids, which can be empty. - * + * * @see getRoom * @see open */ @@ -110,13 +111,13 @@ public: /** * @brief Get a room instance from identifier. - * + * * A room instance enables access to all data frames associated with that * room, including image data. Calling `getRoom` with the same ID * multiple times will return different smart pointers to room instances * but provides access to the same data regardless and is valid. An invalid * room ID will throw an exception. - * + * * @throw voltu::exceptions::InvalidRoomId If the ID does not exist. * @return Room instance or accessing room data. */ @@ -127,14 +128,14 @@ public: /** * @brief Make an empty operator pipeline for frame processing. - * + * * A pipeline allows a sequence of processing operations to be applied to * a data frame. These operations include stereo correspondence, fusion, * data evaluation and various image processing operations. Only some * of these operators are exposed in the SDK. Once a pipeline instance * is obtained, you can add specific operators to it, configure them and * then submit frames from processing. - * + * * @return A unique pipeline instance. */ PY_API virtual voltu::PipelinePtr createPipeline() = 0; diff --git a/SDK/CPP/public/include/voltu/types/errors.hpp b/SDK/CPP/public/include/voltu/types/errors.hpp index b1e77accc50f9975698db4f61be11a5dd7b1c330..b42964ef8002437e90b2352c9b46d35610faedec 100644 --- a/SDK/CPP/public/include/voltu/types/errors.hpp +++ b/SDK/CPP/public/include/voltu/types/errors.hpp @@ -47,6 +47,7 @@ VOLTU_EXCEPTION(NotImplemented, Exception, "Functionality not implemented"); VOLTU_EXCEPTION(ReadOnly, Exception, "Read only, write not allowed"); VOLTU_EXCEPTION(WriteOnly, Exception, "Write only, read not allowed"); VOLTU_EXCEPTION(IncompatibleOperation, Exception, "The input data and operator are incompatible"); +VOLTU_EXCEPTION(Timeout, Exception, "Request timed out"); } } diff --git a/SDK/CPP/public/python/CMakeLists.txt b/SDK/CPP/public/python/CMakeLists.txt index 40bd39c8b388c667af935563da800a62910f0deb..b00857e3722b06fe467f35fdd8d90dbcffe460a3 100644 --- a/SDK/CPP/public/python/CMakeLists.txt +++ b/SDK/CPP/public/python/CMakeLists.txt @@ -1,29 +1,29 @@ set(SDK_AUTO_HEADERS - voltu/types/channel.hpp - voltu/types/frame.hpp - voltu/types/image.hpp - voltu/types/intrinsics.hpp - voltu/observer.hpp - voltu/feed.hpp - voltu/initialise.hpp - voltu/room.hpp - voltu/source.hpp - voltu/system.hpp + voltu/types/channel.hpp + voltu/types/frame.hpp + voltu/types/image.hpp + voltu/types/intrinsics.hpp + voltu/observer.hpp + voltu/feed.hpp + voltu/initialise.hpp + voltu/room.hpp + voltu/source.hpp + voltu/system.hpp ) add_custom_command( - OUTPUT automatic_bindings.cpp - COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/gen.py - automatic_bindings.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/../include - ${SDK_AUTO_HEADERS} + OUTPUT automatic_bindings.cpp + COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/gen.py + automatic_bindings.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/../include + ${SDK_AUTO_HEADERS} - DEPENDS voltu_sdk + DEPENDS voltu_sdk gen.py ) pybind11_add_module(voltu_sdk_py MODULE - automatic_bindings.cpp - module.cpp + automatic_bindings.cpp + module.cpp ) target_include_directories(voltu_sdk_py PUBLIC include) @@ -31,14 +31,3 @@ target_include_directories(voltu_sdk_py PRIVATE .) target_link_libraries(voltu_sdk_py PUBLIC voltu_sdk) set_target_properties(voltu_sdk_py PROPERTIES OUTPUT_NAME voltu) - -enable_testing() -find_package(Python3 COMPONENTS Interpreter) - -function(add_python_test TEST_NAME TEST_SCRIPT) - add_test(NAME ${TEST_NAME} - COMMAND Python3::Interpreter -m unittest ${TEST_SCRIPT} - WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}) -endfunction() - -add_python_test(test_load tests/test_load.py) diff --git a/SDK/CPP/public/python/README.md b/SDK/CPP/public/python/README.md index a5cd30d6d96bab1733841acf5bca73947d6655f0..076ec86e3fca27d3778ba90522dc39cead275fba 100644 --- a/SDK/CPP/public/python/README.md +++ b/SDK/CPP/public/python/README.md @@ -1,7 +1,8 @@ # 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,13 @@ 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. Shared pointers are not used for structs. * 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 +31,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 +42,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 diff --git a/SDK/CPP/public/python/automatic_bindings.cpp.in b/SDK/CPP/public/python/automatic_bindings.cpp.in deleted file mode 100644 index 291de8913a266dd029a6e7ccbc4e7f3796ed82d3..0000000000000000000000000000000000000000 --- a/SDK/CPP/public/python/automatic_bindings.cpp.in +++ /dev/null @@ -1,15 +0,0 @@ -{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} -}} diff --git a/SDK/CPP/public/python/examples/example1.py b/SDK/CPP/public/python/examples/example1.py index 1cc1a26681470b9212662da2959224daf35bf89c..e124328892a0164790e029c0f447f8df58d0ba60 100644 --- a/SDK/CPP/public/python/examples/example1.py +++ b/SDK/CPP/public/python/examples/example1.py @@ -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) + if cv2.waitKey(10) == 27: + break diff --git a/SDK/CPP/public/python/examples/example2.py b/SDK/CPP/public/python/examples/example2.py index 629fec948de0f93c9d94184e8a79033afb211896..8b8bfcf777d558ef8e267ce4a6685af564d15b22 100644 --- a/SDK/CPP/public/python/examples/example2.py +++ b/SDK/CPP/public/python/examples/example2.py @@ -3,7 +3,6 @@ import cv2 import voltu -import time import sys import os @@ -11,25 +10,17 @@ 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() cv2.imshow("im", im) - if cv2.waitKey(10) == 27: - break - - except Exception as e: - print(e) + if cv2.waitKey(10) == 27: + break diff --git a/SDK/CPP/public/python/gen.py b/SDK/CPP/public/python/gen.py index 00e8252c7efea6975acc331c5076453aa5c3695b..c284c737e5077419463bb498cf7a82bc25cb303f 100755 --- a/SDK/CPP/public/python/gen.py +++ b/SDK/CPP/public/python/gen.py @@ -1,5 +1,30 @@ #!/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 @@ -12,23 +37,25 @@ def read_line(file, lineno): 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) + 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"] +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"] @@ -49,23 +76,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 +92,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") @@ -87,25 +146,28 @@ def create_function_bindings(func, parent=[], pybind_handle=None): return cpp -def create_class_bindings(cls, parent=[], pybind_handle=""): - cls_name = cls["name"] - full_name = parent + [cls_name] +def create_class_bindings(clsdata, parent=[], pybind_handle=""): + """ Create bindings for a class """ + + clsdata_name = clsdata["name"] + full_name = parent + [clsdata_name] cpp = [] - if "PY_NO_SHARED_PTR" not in cls["debug"]: - cls_cpp = "py::class_<{name}, std::shared_ptr<{name}>>({handle}, \"{name}\")" - else: + if no_shared_ptr_cls(clsdata): cls_cpp = "py::class_<{name}>({handle}, \"{name}\")" - cpp.append(cls_cpp.format(handle=pybind_handle, name=cls_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 cls["declaration_method"] == "struct": + if clsdata["declaration_method"] == "struct": cpp.append(".def(py::init<>())") - for method in cls["methods"]["public"]: + for method in clsdata["methods"]["public"]: if include_in_api(method): cpp.append("." + create_function_bindings(method, full_name)) - for field in cls["properties"]["public"]: + for field in clsdata["properties"]["public"]: if field["constant"]: field_cpp = ".def_property_readonly(\"{name}\", &{cpp_name})" else: @@ -116,8 +178,30 @@ def create_class_bindings(cls, parent=[], pybind_handle=""): return "\n\t\t".join(cpp) -if __name__ == "__main__": +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): @@ -131,6 +215,7 @@ if __name__ == "__main__": out = [] includes = [] + for fname in fsin: includes.append(fname) @@ -145,7 +230,13 @@ 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"]) - out.append(create_class_bindings(data, [ns], handle) + ";") + + 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 @@ -153,9 +244,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) diff --git a/SDK/CPP/public/python/module.cpp b/SDK/CPP/public/python/module.cpp index 5ba4e9e2a48f24cbe34353796bf23728f2f93656..636e7b809fcb6757776ef711dd837eb330130c9f 100644 --- a/SDK/CPP/public/python/module.cpp +++ b/SDK/CPP/public/python/module.cpp @@ -6,7 +6,11 @@ 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"); @@ -14,7 +18,11 @@ void py_exceptions(pybind11::module& m) { py::register_exception<voltu::exceptions::BadSourceURI>(m, "ErrorBadSourceURI"); } +static auto module = PyModule(); + PYBIND11_MODULE(voltu, m) { - py_exceptions(m); - py_automatic_bindings(m); + m.attr("version") = py::make_tuple(VOLTU_VERSION_MAJOR, VOLTU_VERSION_MINOR, VOLTU_VERSION_PATCH); + + module.py_exceptions(m); + module.py_automatic_bindings(m); } diff --git a/SDK/CPP/public/python/module.hpp b/SDK/CPP/public/python/module.hpp index b605cf7f6b7030701858cf8d4cf2ffbba95636bf..f59e4d6280356357b56d4d2db928653aadd4a973 100644 --- a/SDK/CPP/public/python/module.hpp +++ b/SDK/CPP/public/python/module.hpp @@ -1,5 +1,15 @@ #pragma once #include <pybind11/pybind11.h> +#include <voltu/voltu.hpp> +#include <voltu/system.hpp> -void py_automatic_bindings(pybind11::module& m); +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; +}; diff --git a/SDK/CPP/public/python/tests/test_load.py b/SDK/CPP/public/python/tests/test_load.py deleted file mode 100644 index 8e5131d4d4299af3ac116f3585de9403b6ee2f9d..0000000000000000000000000000000000000000 --- a/SDK/CPP/public/python/tests/test_load.py +++ /dev/null @@ -1,12 +0,0 @@ -import unittest - -class LoadLibrary(unittest.TestCase): - - def test_get_instance(self): - import voltu - self.assertIsNotNone(voltu.instance()) - # second call to instance() returns None - #self.assertIsNotNone(voltu.instance()) - -if __name__ == '__main__': - unittest.main() diff --git a/SDK/CPP/public/voltu.cpp b/SDK/CPP/public/voltu.cpp index bf63ed6b4c1f145f1a36547030e97c898359f78f..2c4d19712428be34af7d8fd04c93a7c615fdec7b 100644 --- a/SDK/CPP/public/voltu.cpp +++ b/SDK/CPP/public/voltu.cpp @@ -10,10 +10,17 @@ #if defined(WIN32) #include <windows.h> +#pragma comment(lib, "User32.lib") #else #include <dlfcn.h> +#include <sys/types.h> +#include <sys/stat.h> +#include <unistd.h> #endif +#include <cstdlib> +#include <iostream> + static bool g_init = false; typedef void* Library; @@ -48,13 +55,50 @@ static void unloadLibrary(Library lib) #endif } +static bool is_file(const std::string &path) +{ +#ifdef WIN32 + WIN32_FIND_DATA ffd; + HANDLE hFind = FindFirstFile(path.c_str(), &ffd); + + if (hFind == INVALID_HANDLE_VALUE) return false; + FindClose(hFind); + return true; +#else + struct stat s; + if (::stat(path.c_str(), &s) == 0) + { + return true; + } + else + { + return false; + } +#endif +} + static std::string locateLibrary() { // TODO: Use full paths and find correct versions #if defined(WIN32) return "voltu.dll"; #else - return "libvoltu.so"; + std::string name = "libvoltu.so"; + std::string vname = name + std::string(".") + std::to_string(VOLTU_VERSION_MAJOR) + std::string(".") + std::to_string(VOLTU_VERSION_MINOR); + + if (const char* env_p = std::getenv("VOLTU_RUNTIME")) + { + if (is_file(env_p)) return env_p; + } + + // FIXME: Should eventually not have these... + if (is_file(std::string("./") + vname)) return std::string("./") + vname; + //else if (is_file(std::string("./") + name)) return std::string("./") + name; + else if (is_file(std::string("../") + vname)) return std::string("../") + vname; + /*else if (is_file(std::string("../") + name)) return std::string("../") + name; + else if (is_file(std::string("/usr/local/lib/") + vname)) return std::string("/usr/local/lib/") + vname; + else if (is_file(std::string("/usr/local/lib/") + name)) return std::string("/usr/local/lib/") + name;*/ + return vname; #endif } @@ -63,6 +107,7 @@ std::shared_ptr<voltu::System> voltu::instance() if (g_init) return nullptr; std::string name = locateLibrary(); + std::cout << "Loading VolTu Runtime: " << name << std::endl; Library handle = loadLibrary(name.c_str()); if (handle) diff --git a/SDK/CPP/tests/CMakeLists.txt b/SDK/CPP/tests/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..489f6e3380a20d296427308c61f667d5f5c35936 --- /dev/null +++ b/SDK/CPP/tests/CMakeLists.txt @@ -0,0 +1,15 @@ +find_package(Python3 COMPONENTS Interpreter) + +function(add_python_test TEST_NAME TEST_SCRIPT) + add_test(NAME ${TEST_NAME} + COMMAND Python3::Interpreter -B -m unittest ${TEST_SCRIPT} + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}) + + # binary module directory to PYTHONPATH, additional variables have to be + # separated by semicolon + set_tests_properties(${TEST_NAME} PROPERTIES ENVIRONMENT + "PYTHONPATH=${SDK_BINARY_DIR}/python;LD_LIBRARY_PATH=${CMAKE_CURRENT_BINARY_DIR}/..") + set_property(TEST ${TEST_NAME} APPEND PROPERTY DEPENDS voltu_sdk) +endfunction() + +add_python_test(Py_TestLoad test_load.py) diff --git a/SDK/CPP/public/python/tests/__init__.py b/SDK/CPP/tests/__init__.py similarity index 100% rename from SDK/CPP/public/python/tests/__init__.py rename to SDK/CPP/tests/__init__.py diff --git a/SDK/CPP/tests/test_load.py b/SDK/CPP/tests/test_load.py new file mode 100644 index 0000000000000000000000000000000000000000..df4292cc6af08fcced3017415ed8fe097ee3e3ab --- /dev/null +++ b/SDK/CPP/tests/test_load.py @@ -0,0 +1,21 @@ +import unittest +import os + +class LoadLibrary(unittest.TestCase): + + def test_import(self): + import voltu + + def test_import_twice(self): + # verify that System instance is created just once, even if module + # imported multiple times + import voltu + import voltu + self.assertIsNotNone(voltu.System) + + def test_version(self): + import voltu + major, minor, patch = voltu.version + +if __name__ == '__main__': + unittest.main()