11.08., 9:00 - 11:00: Due to updates GitLab will be unavailable for some minutes between 09:00 and 11:00.

Commit f6e6f3d7 authored by Nikola Dinev's avatar Nikola Dinev Committed by Tobias Lasser

Feature/python bindings

parent 5d96e144
Pipeline #278200 passed with stages
in 52 minutes and 26 seconds
......@@ -10,3 +10,6 @@
[submodule "thirdparty/quickvec"]
path = thirdparty/quickvec
url = https://gitlab.lrz.de/IP/quickvec.git
[submodule "thirdparty/pybind11"]
path = thirdparty/pybind11
url = https://github.com/pybind/pybind11.git
......@@ -26,6 +26,7 @@ option(ELSA_INSTALL "Enable generating the install targets for make install" ${E
option(ELSA_BUILD_EXAMPLES "Enable building of examples" ${ELSA_MASTER_PROJECT})
option(ELSA_BUILD_CUDA_PROJECTORS "Enable building (or attempting to) the CUDA projectors" ON)
option(ELSA_BUILD_PYTHON_BINDINGS "Attempt to build python bindings if enabled (required libclang-dev)" ON)
option(ELSA_BUILD_WITH_MORE_WARNINGS "Enable all and extra warnings when building (-Wall -Wextra)" ON)
option(ELSA_CUDA_VECTOR "Build elsa with GPU DataContainer support and default" OFF)
......@@ -87,6 +88,7 @@ set(INSTALL_CONFIG_DIR ${CMAKE_INSTALL_LIBDIR}/cmake/elsa)
# include the InstallElsaModule function
include(InstallElsaModule)
add_subdirectory(thirdparty/pybind11)
# ------------ setup VectorCUDA --------
# ------------
......@@ -171,6 +173,11 @@ endif(ELSA_TESTING OR ELSA_BENCHMARKS)
# ------------ add code/docs -----------
# ------------
# the bindings generator should be added before elsa, as we check whether the target can be built
if (ELSA_BUILD_PYTHON_BINDINGS)
add_subdirectory(tools/bindings_generation EXCLUDE_FROM_ALL)
endif()
# the elsa library
add_subdirectory(elsa)
......
find_program(LLVM_CONFIG_FOUND "llvm-config")
if (LLVM_CONFIG_FOUND)
execute_process(
COMMAND llvm-config --cxxflags
OUTPUT_VARIABLE LibClang_Flags)
string(STRIP ${LibClang_Flags} LibClang_Flags)
separate_arguments(LibClang_Flags NATIVE_COMMAND ${LibClang_Flags})
execute_process(
COMMAND llvm-config --includedir
OUTPUT_VARIABLE LibClang_INCLUDE_DIR)
find_library(LibClangCpp_LIBRARY NAMES clang-cpp PATH_SUFFIXES llvm-9/lib llvm-10/lib)
find_library(LibLLVM_LIBRARY NAMES LLVM PATH_SUFFIXES llvm-9/lib llvm-10/lib)
set(LibClang_LIBRARIES ${LibClangCpp_LIBRARY} ${LibLLVM_LIBRARY})
set(LibClang_INCLUDE_DIRS ${LibClang_INCLUDE_DIR})
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(LibClang DEFAULT_MSG LibClang_LIBRARIES LibClang_INCLUDE_DIR)
else()
message(WARNING "llvm-config couln't be located. Python bindings will not be generated")
endif()
\ No newline at end of file
......@@ -30,6 +30,47 @@ macro(ELSA_TEST NAME)
catch_discover_tests(test_${NAME} TEST_SPEC ${ELSA_JUNIT_ARGUMENTS})
endmacro(ELSA_TEST)
if (ELSA_BUILD_PYTHON_BINDINGS)
find_package(PythonLibs)
if(PYTHONLIBS_FOUND)
add_custom_target(pyelsa)
# macro for generation of the corresponding python module for the elsa target TARGET_NAME
# as a pre-build step the code for the python bindings will be generated and stored in BINDINGS_CODE_PATH
# the sources containing the public interface of TARGET_NAME should be specified as additional arguments
# you can omit a source file from the list to prevent the generation of bindings for that file
macro(GENERATE_BINDINGS TARGET_NAME BINDINGS_CODE_PATH HINTS_PATH)
if (TARGET pybind11_generator)
get_filename_component(CMAKE_CURRENT_LIST_DIR_LAST_PART ${CMAKE_CURRENT_LIST_DIR} NAME)
file(APPEND ${PROJECT_BINARY_DIR}/elsa/__init__.py "from .${CMAKE_CURRENT_LIST_DIR_LAST_PART} import *\n")
add_custom_command(OUTPUT ${BINDINGS_CODE_PATH}
COMMAND ${PROJECT_BINARY_DIR}/tools/bindings_generation/pybind11_generator
${ARGN}
--extra-arg=-I${PYTHON_INCLUDE_DIRS}
--extra-arg=-I${PYBIND11_INCLUDE_DIR}
--hints=${HINTS_PATH}
-o=${BINDINGS_CODE_PATH}
--name=${TARGET_NAME}
DEPENDS ${TARGET_NAME} pybind11_generator ${HINTS_PATH}
WORKING_DIRECTORY ${CMAKE_CURRENT_LIST_DIR}
COMMENT "Generating bindings code for ${TARGET_NAME}"
VERBATIM)
pybind11_add_module(py${TARGET_NAME} ${BINDINGS_CODE_PATH})
target_include_directories(py${TARGET_NAME} PUBLIC ${PROJECT_SOURCE_DIR}/elsa/bindings/hints)
target_link_libraries(py${TARGET_NAME} PUBLIC ${TARGET_NAME})
target_compile_features(py${TARGET_NAME} PUBLIC cxx_std_17)
add_dependencies(pyelsa py${TARGET_NAME})
endif()
endmacro()
else()
message(STATUS "Couldn't find Python.h. Python bindings will not be generated.")
endif()
endif()
# add the elsa modules
add_subdirectory(core)
add_subdirectory(logging)
......
......@@ -89,6 +89,28 @@ target_compile_features(${ELSA_MODULE_TARGET_NAME} PUBLIC cxx_std_17)
# set -fPIC
set_target_properties(${ELSA_MODULE_TARGET_NAME} PROPERTIES POSITION_INDEPENDENT_CODE ON)
if (ELSA_BUILD_PYTHON_BINDINGS)
set(BINDINGS_SOURCES ${MODULE_SOURCES}
Descriptors/DataDescriptor.h
Descriptors/DescriptorUtils.h
Descriptors/VolumeDescriptor.h
Descriptors/BlockDescriptor.h
Descriptors/IdenticalBlocksDescriptor.h
Descriptors/PartitionDescriptor.h
Descriptors/RandomBlocksDescriptor.h
Geometry.h)
list(FILTER BINDINGS_SOURCES EXCLUDE REGEX DataHandler.*.cpp)
list(FILTER BINDINGS_SOURCES EXCLUDE REGEX .*Descriptor.*.cpp)
list(FILTER BINDINGS_SOURCES EXCLUDE REGEX Geometry.cpp)
list(FILTER BINDINGS_SOURCES EXCLUDE REGEX StrongTypes.cpp)
GENERATE_BINDINGS(${ELSA_MODULE_TARGET_NAME}
${CMAKE_CURRENT_BINARY_DIR}/bind_${ELSA_MODULE_NAME}.cpp
bindings_hints.hpp
${BINDINGS_SOURCES})
endif()
# build the tests (if enabled)
if(ELSA_TESTING)
......
#include "DataContainer.h"
#include "Descriptors/VolumeDescriptor.h"
#include "LinearOperator.h"
#include "DescriptorUtils.h"
#include <pybind11/pybind11.h>
#include <pybind11/operators.h>
#include <functional>
namespace elsa
{
template <typename Class>
struct ClassHints {
};
namespace py = pybind11;
/// wrapper for variadic functions
template <typename T, std::size_t N, typename = std::make_index_sequence<N>>
struct invokeBestCommonVariadic;
template <typename T, std::size_t N, std::size_t... S>
struct invokeBestCommonVariadic<T, N, std::index_sequence<S...>> {
static auto exec(const py::args& args)
{
if (args.size() == N)
return elsa::bestCommon(args[S].template cast<T>()...);
if constexpr (N > 1) {
return invokeBestCommonVariadic<T, N - 1>::exec(args);
} else {
throw(std::logic_error("Unsupported number of variadic arguments"));
}
};
};
template <typename data_t>
LinearOperator<data_t> adjointHelper(const LinearOperator<data_t>& op)
{
return adjoint(op);
}
template <typename data_t>
LinearOperator<data_t> leafHelper(const LinearOperator<data_t>& op)
{
return leaf(op);
}
// define global variables and functions in the module hints
struct ModuleHints {
static void addCustomFunctions(py::module& m)
{
m.def("adjoint", &adjointHelper<float>)
.def("adjoint", &adjointHelper<double>)
.def("adjoint", &adjointHelper<std::complex<float>>)
.def("adjoint", &adjointHelper<std::complex<double>>);
m.def("leaf", &leafHelper<float>)
.def("leaf", &leafHelper<double>)
.def("leaf", &leafHelper<std::complex<float>>)
.def("leaf", &leafHelper<std::complex<double>>);
m.def("bestCommon", (std::unique_ptr<DataDescriptor>(*)(
const std::vector<const DataDescriptor*>&))(&bestCommon))
.def("bestCommon", &invokeBestCommonVariadic<const DataDescriptor&, 10>::exec);
}
};
template <typename data_t, typename type_, typename... options>
void addOperatorsDc(py::class_<type_, options...>& c)
{
c.def(
"__add__",
[](const DataContainer<data_t>& self, const DataContainer<data_t>& other) {
return DataContainer<data_t>(self + other);
},
py::return_value_policy::move)
.def(
"__mul__",
[](const DataContainer<data_t>& self, const DataContainer<data_t>& other) {
return DataContainer<data_t>(self * other);
},
py::return_value_policy::move)
.def(
"__sub__",
[](const DataContainer<data_t>& self, const DataContainer<data_t>& other) {
return DataContainer<data_t>(self - other);
},
py::return_value_policy::move)
.def(
"__truediv__",
[](const DataContainer<data_t>& self, const DataContainer<data_t>& other) {
return DataContainer<data_t>(self / other);
},
py::return_value_policy::move)
// TODO: make the generator automatically generate this __setitem__ function
.def("__setitem__", [](elsa::DataContainer<data_t>& dc, elsa::index_t i, data_t value) {
dc[i] = value;
});
}
template <typename data_t>
struct DataContainerHints : public ClassHints<elsa::DataContainer<data_t>> {
constexpr static std::tuple ignoreMethods = {
"operator()", "begin", "cbegin", "end", "cend", "rbegin", "crbegin", "rend", "crend"};
template <typename type_, typename... options>
static void addCustomMethods(py::class_<type_, options...>& c)
{
addOperatorsDc<data_t>(c);
}
template <typename type_, typename... options>
static void exposeBufferInfo(py::class_<type_, options...>& c)
{
c.def(py::init([](py::buffer b) {
py::buffer_info info = b.request();
if (info.format != py::format_descriptor<data_t>::format())
throw std::invalid_argument("Incompatible scalar types");
elsa::IndexVector_t coeffsPerDim(info.ndim);
ssize_t minStride = info.strides[0];
for (std::size_t i = 0; i < static_cast<std::size_t>(info.ndim); i++) {
if (info.strides[i] < minStride)
minStride = info.strides[i];
coeffsPerDim[static_cast<elsa::index_t>(i)] =
static_cast<elsa::index_t>(info.shape[i]);
}
if (static_cast<std::size_t>(minStride) / sizeof(data_t) != 1)
throw std::invalid_argument("Cannot convert strided buffer to DataContainer");
auto map = Eigen::Map<Eigen::Matrix<data_t, Eigen::Dynamic, 1>>(
static_cast<data_t*>(info.ptr), coeffsPerDim.prod());
elsa::VolumeDescriptor dd{coeffsPerDim};
return std::make_unique<elsa::DataContainer<data_t>>(dd, map);
})).def_buffer([](elsa::DataContainer<data_t>& m) {
std::vector<ssize_t> dims, strides;
auto coeffsPerDim = m.getDataDescriptor().getNumberOfCoefficientsPerDimension();
ssize_t combined = 1;
for (int i = 0; i < coeffsPerDim.size(); i++) {
dims.push_back(coeffsPerDim[i]);
strides.push_back(combined * static_cast<ssize_t>(sizeof(data_t)));
combined *= coeffsPerDim[i];
}
return py::buffer_info(
&m[0], sizeof(data_t), py::format_descriptor<data_t>::format(),
m.getDataDescriptor().getNumberOfDimensions(), coeffsPerDim, strides);
});
}
};
template <typename data_t>
struct LinearOperatorHints : public ClassHints<elsa::LinearOperator<data_t>> {
template <typename type_, typename... options>
static void addCustomMethods(py::class_<type_, options...>& c)
{
c.def(py::self + py::self).def(py::self * py::self);
}
};
template <typename data_t>
struct DataContainerComplexHints : public ClassHints<elsa::DataContainer<data_t>> {
constexpr static std::tuple ignoreMethods = {
"operator()", "begin", "cbegin", "end", "cend", "rbegin", "crbegin", "rend", "crend"};
template <typename type_, typename... options>
static void addCustomMethods(py::class_<type_, options...>& c)
{
addOperatorsDc<data_t>(c);
}
// TODO: pybind11 does not provide a default format_descriptor for complex types -> make a
// custom one for this to work
// CustomMethod<true, py::buffer_info, elsa::DataContainer<data_t>&> buffer =
// [](elsa::DataContainer<data_t>& m) {
// std::vector<ssize_t> dims, strides;
// auto coeffsPerDim = m.getDataDescriptor().getNumberOfCoefficientsPerDimension();
// ssize_t combined = 1;
// for (int i = 0; i < coeffsPerDim.size(); i++) {
// dims.push_back(coeffsPerDim[i]);
// strides.push_back(combined * static_cast<ssize_t>(sizeof(data_t)));
// combined *= coeffsPerDim[i];
// }
// return py::buffer_info(
// &m[0], /* Pointer to buffer */
// sizeof(data_t), /* Size of one scalar */
// py::format_descriptor<data_t>::format(),
// /* Python struct-style format descriptor
// */
// m.getDataDescriptor().getNumberOfDimensions(), /* Number of dimensions */
// coeffsPerDim, /* Buffer dimensions */
// strides);
// };
};
template struct DataContainerHints<float>;
template struct DataContainerComplexHints<std::complex<float>>;
template struct DataContainerHints<double>;
template struct DataContainerComplexHints<std::complex<double>>;
template struct DataContainerHints<index_t>;
template struct LinearOperatorHints<float>;
template struct LinearOperatorHints<double>;
template struct LinearOperatorHints<std::complex<float>>;
template struct LinearOperatorHints<std::complex<double>>;
} // namespace elsa
\ No newline at end of file
......@@ -61,6 +61,12 @@ if(ELSA_TESTING)
add_subdirectory(tests)
endif(ELSA_TESTING)
if (ELSA_BUILD_PYTHON_BINDINGS)
GENERATE_BINDINGS(${ELSA_MODULE_TARGET_NAME}
${CMAKE_CURRENT_BINARY_DIR}/bind_${ELSA_MODULE_NAME}.cpp
""
${MODULE_SOURCES})
endif()
# register the module
registerComponent(${ELSA_MODULE_NAME})
......
......@@ -43,6 +43,12 @@ if(ELSA_TESTING)
add_subdirectory(tests)
endif(ELSA_TESTING)
if (ELSA_BUILD_PYTHON_BINDINGS)
GENERATE_BINDINGS(${ELSA_MODULE_TARGET_NAME}
${CMAKE_CURRENT_BINARY_DIR}/bind_${ELSA_MODULE_NAME}.cpp
""
${MODULE_SOURCES} CircleTrajectoryGenerator.h)
endif()
# register the module
registerComponent(${ELSA_MODULE_NAME})
......
......@@ -37,6 +37,17 @@ target_compile_features(${ELSA_MODULE_TARGET_NAME} PUBLIC cxx_std_17)
# set -fPIC
set_target_properties(${ELSA_MODULE_TARGET_NAME} PROPERTIES POSITION_INDEPENDENT_CODE ON)
if (ELSA_BUILD_PYTHON_BINDINGS)
set(BINDINGS_SOURCES ${MODULE_SOURCES})
list(FILTER BINDINGS_SOURCES EXCLUDE REGEX ioUtils.cpp)
GENERATE_BINDINGS(${ELSA_MODULE_TARGET_NAME}
${CMAKE_CURRENT_BINARY_DIR}/bind_${ELSA_MODULE_NAME}.cpp
bindings_hints.hpp
${BINDINGS_SOURCES})
endif()
# build the tests (if enabled)
if(ELSA_TESTING)
......
#include "EDFHandler.h"
#include "MHDHandler.h"
#include <pybind11/pybind11.h>
#include <functional>
namespace elsa
{
template <typename Class>
struct ClassHints {
};
template <class Return, class... Args>
using CustomMethod = std::pair<bool, std::function<Return(Args...)>>;
namespace py = pybind11;
struct EDFHints : public ClassHints<EDF> {
constexpr static std::tuple ignoreMethods = {"read"};
template <typename type_, typename... options>
static void addCustomMethods(py::class_<type_, options...>& c)
{
c.def_static(
"readf", [](std::string filename) { return EDF::read<float>(filename); },
py::return_value_policy::move)
.def_static(
"readd", [](std::string filename) { return EDF::read<double>(filename); },
py::return_value_policy::move)
.def_static(
"readl", [](std::string filename) { return EDF::read<index_t>(filename); },
py::return_value_policy::move);
}
};
struct MHDHints : public ClassHints<MHD> {
constexpr static std::tuple ignoreMethods = {"read"};
template <typename type_, typename... options>
static void addCustomMethods(py::class_<type_, options...>& c)
{
c.def_static(
"readf", [](std::string filename) { return MHD::read<float>(filename); },
py::return_value_policy::move)
.def_static(
"readd", [](std::string filename) { return MHD::read<double>(filename); },
py::return_value_policy::move)
.def_static(
"readl", [](std::string filename) { return MHD::read<index_t>(filename); },
py::return_value_policy::move);
}
};
} // namespace elsa
\ No newline at end of file
......@@ -75,6 +75,8 @@ namespace elsa
/// default destructor
~BlockLinearOperator() override = default;
BlockLinearOperator& operator=(BlockLinearOperator&) = delete;
/// return the operator corresponding to the i-th block of the matrix
const LinearOperator<data_t>& getIthOperator(index_t i) const;
......
......@@ -52,6 +52,12 @@ if(ELSA_TESTING)
add_subdirectory(tests)
endif(ELSA_TESTING)
if (ELSA_BUILD_PYTHON_BINDINGS)
GENERATE_BINDINGS(${ELSA_MODULE_TARGET_NAME}
${CMAKE_CURRENT_BINARY_DIR}/bind_${ELSA_MODULE_NAME}.cpp
""
${MODULE_SOURCES})
endif()
# register the module
registerComponent(${ELSA_MODULE_NAME})
......
......@@ -47,6 +47,12 @@ if(ELSA_TESTING)
add_subdirectory(tests)
endif(ELSA_TESTING)
if (ELSA_BUILD_PYTHON_BINDINGS)
GENERATE_BINDINGS(${ELSA_MODULE_TARGET_NAME}
${CMAKE_CURRENT_BINARY_DIR}/bind_${ELSA_MODULE_NAME}.cpp
""
${MODULE_SOURCES})
endif()
# register the module
registerComponent(${ELSA_MODULE_NAME})
......
......@@ -58,6 +58,12 @@ if(ELSA_TESTING)
add_subdirectory(tests)
endif(ELSA_TESTING)
if (ELSA_BUILD_PYTHON_BINDINGS)
GENERATE_BINDINGS(${ELSA_MODULE_TARGET_NAME}
${CMAKE_CURRENT_BINARY_DIR}/bind_${ELSA_MODULE_NAME}.cpp
""
${MODULE_SOURCES})
endif()
# register the module
registerComponent(${ELSA_MODULE_NAME})
......
......@@ -55,7 +55,13 @@ if (CMAKE_CUDA_COMPILER)
add_subdirectory(tests)
endif(ELSA_TESTING)
if (ELSA_BUILD_PYTHON_BINDINGS)
GENERATE_BINDINGS(${ELSA_MODULE_TARGET_NAME}
${CMAKE_CURRENT_BINARY_DIR}/bind_${ELSA_MODULE_NAME}.cpp
""
${MODULE_SOURCES})
endif()
# register the module
registerComponent(${ELSA_MODULE_NAME})
......
......@@ -43,6 +43,12 @@ if(ELSA_TESTING)
add_subdirectory(tests)
endif(ELSA_TESTING)
if (ELSA_BUILD_PYTHON_BINDINGS)
GENERATE_BINDINGS(${ELSA_MODULE_TARGET_NAME}
${CMAKE_CURRENT_BINARY_DIR}/bind_${ELSA_MODULE_NAME}.cpp
""
${MODULE_SOURCES})
endif()
# register the module
registerComponent(${ELSA_MODULE_NAME})
......
Subproject commit d96c34516d276f4c33d25df23d5934af1c5b2406
cmake_minimum_required(VERSION 3.10)
find_package(LibClang)
if (LIBCLANG_FOUND)
add_executable(pybind11_generator Parser.cpp)
target_compile_options(pybind11_generator PUBLIC ${LibClang_Flags})
target_compile_features(pybind11_generator PUBLIC cxx_std_17)
target_link_libraries(pybind11_generator LLVM clang-cpp stdc++fs)
file(WRITE ${PROJECT_BINARY_DIR}/elsa/__init__.py "")
else()
message(WARNING "libclang-dev not found. Python bindings will not be generated.")
endif()
#pragma once
#include "Module.h"
#include <iostream>
#include <fstream>
#include <map>
#include <algorithm>
#include <assert.h>
// Determine wether to use <filesystem> or <experimental/filesystem>. Adapted from
// https://stackoverflow.com/questions/53365538/how-to-determine-whether-to-use-filesystem-or-experimental-filesystem,
// simplified by removing MSVC specific code, as this is expected to be run with clang
// We haven't checked which filesystem to include yet
#ifndef INCLUDE_STD_FILESYSTEM_EXPERIMENTAL
// Check for feature test macro for <filesystem>
#if defined(__cpp_lib_filesystem)
#define INCLUDE_STD_FILESYSTEM_EXPERIMENTAL 0
// Check for feature test macro for <experimental/filesystem>
#elif defined(__cpp_lib_experimental_filesystem)
#define INCLUDE_STD_FILESYSTEM_EXPERIMENTAL 1
// We can't check if headers exist...
// Let's assume experimental to be safe
#elif !defined(__has_include)
#define INCLUDE_STD_FILESYSTEM_EXPERIMENTAL 1
// Check if the header "<filesystem>" exists
#elif __has_include(<filesystem>)
#define INCLUDE_STD_FILESYSTEM_EXPERIMENTAL 0
// Check if the header "<filesystem>" exists
#elif __has_include(<experimental/filesystem>)
#define INCLUDE_STD_FILESYSTEM_EXPERIMENTAL 1
// Fail if neither header is available with a nice error message
#else
#error Could not find system header "<filesystem>" or "<experimental/filesystem>"
#endif
// We priously determined that we need the exprimental version
#if INCLUDE_STD_FILESYSTEM_EXPERIMENTAL
// Include it
#include <experimental/filesystem>
// We need the alias from std::experimental::filesystem to std::filesystem
namespace std
{
namespace filesystem = experimental::filesystem;
}
// We have a decent compiler and can use the normal version
#else
// Include it
#include <filesystem>
#endif
#endif // #ifndef INCLUDE_STD_FILESYSTEM_EXPERIMENTAL
class Generator
{
public:
static void generateBindingsForModule(const elsa::Module& m, std::string outputPath)
{
if (outputPath.empty())
outputPath = "bind_" + m.name + ".cpp";
std::filesystem::path p(outputPath);
p = p.parent_path();
p.append("__init__.py");
std::ofstream outputFile(outputPath);
std::ofstream initFile(p.c_str());
initFile << "from ." << m.pythonName << " import *\n";
outputFile << "#include <pybind11/pybind11.h>\n";
for (const auto& include : m.pybindIncludes)
outputFile << "#include <" << include << ">\n";
outputFile << "\n";
for (const auto& include : m.includes)
outputFile << "#include \"" << include.substr(m.path.size() + 1) << "\"\n";
if (!m.moduleHints.includePath.empty())
outputFile << "\n#include \""
<< m.moduleHints.includePath.substr(m.moduleHints.includePath.rfind("/") + 1)
<< "\"\n";
outputFile << "\n"
<< "namespace py = pybind11;\n\n"
<< "PYBIND11_MODULE(" << m.pythonName << ", m)\n"
<< "{\n";
for (const auto& tag : m.tags) {
if (m.classHints.find(tag->name) != m.classHints.end()) {
const auto& hints = m.classHints.at(tag->name);
const auto& rec = static_cast<const elsa::Module::Record&>(*tag);
generateBindingsForRecord(rec, outputFile, initFile, &hints);
} else {
if (dynamic_cast<const elsa::Module ::Record*>(tag.get())) {
const auto& rec = static_cast<const elsa::Module::Record&>(*tag);
generateBindingsForRecord(rec, outputFile, initFile);
} else if (dynamic_cast<const elsa::Module::Enum*>(tag.get())) {
const auto& e = static_cast<const elsa::Module::Enum&>(*tag);
generateBindingsForEnum(e, outputFile);
}
}
}
if (m.moduleHints.definesGlobalCustomFunctions)
outputFile << "\telsa::ModuleHints::addCustomFunctions(m);\n";
outputFile << "}\n";
}
static void generateBindingsForRecord(const elsa::Module::Record& r, std::ofstream& outputFile,
std::ofstream& initFile,
const elsa::Module::ClassHints* hints = nullptr)
{
std::string qualifiedName = r.name;
std::string additionalProps = "";
std::map<std::string, bool> hasOverloads;
for (const auto& [fId, f] : r.methods) {
if (hasOverloads.find(f.name) == hasOverloads.end()) {
hasOverloads.emplace(f.name, false);
} else {
hasOverloads[f.name] = true;
}
}
if (classSupportsBufferProtocol(r, hints))
additionalProps = ", py::buffer_protocol()";
auto pythonName = getPythonNameForTag(r.namespaceStrippedName);
// py::class_<Class, Base1, Base2,...>(m, Class[, py::buffer_protocol()])
outputFile << "\tpy::class_<" << r.name;
for (auto& base : r.bases)
outputFile << ", " << base;
outputFile << "> " << pythonName << "(m, \"" << pythonName << "\"" << additionalProps
<< ");\n\t" << pythonName;
// define constructors and other methods