Performance improvements, better readme and complete python bindings
🧪 Test / test (push) Failing after 28m30s
🧪 Test / test (push) Failing after 28m30s
This commit is contained in:
@@ -0,0 +1,311 @@
|
||||
#pragma once
|
||||
// Auto-binding helpers for KPN++ Python bindings.
|
||||
//
|
||||
// Usage in your binding .cpp:
|
||||
//
|
||||
// #define KPN_BUILD_PYTHON
|
||||
// #include <kpn/python/auto_bind.hpp>
|
||||
//
|
||||
// int produce() { return 42; }
|
||||
// int double_it(int x) { return x * 2; }
|
||||
// void print_it(int x) { std::cout << x << '\n'; }
|
||||
//
|
||||
// using MyNodes = kpn::python::NodeRegistry<
|
||||
// kpn::python::Entry<produce, "produce">,
|
||||
// kpn::python::Entry<double_it, "double_it">,
|
||||
// kpn::python::Entry<print_it, "print_it">
|
||||
// >;
|
||||
//
|
||||
// NB_MODULE(my_kpn, m) {
|
||||
// kpn::python::bind_network<MyNodes>(m); // KPN_BIND_PYTHON behaviour
|
||||
// kpn::python::bind_debug<MyNodes>(m); // KPN_PYTHON_DEBUG behaviour
|
||||
// }
|
||||
//
|
||||
// bind_network registers:
|
||||
// - Network class (PyNetwork<auto-deduced-variant>) with auto-registered converters
|
||||
// - make_<name>(capacity=5) factory for each entry
|
||||
// - <Name>Node class for each entry
|
||||
//
|
||||
// bind_debug additionally registers each raw C++ function as a free Python
|
||||
// callable (e.g. double_it(5) → 10) so node logic can be tested without a network.
|
||||
//
|
||||
// To support a custom type T, specialise kpn::PythonConverter<T> before calling
|
||||
// bind_network:
|
||||
//
|
||||
// namespace kpn {
|
||||
// template<> struct PythonConverter<MyVec3> {
|
||||
// static constexpr const char* type_name = "vec3"; // optional friendly name
|
||||
// static nb::object to_python(const MyVec3& v) { ... }
|
||||
// static MyVec3 from_python(nb::object o) { ... }
|
||||
// };
|
||||
// } // namespace kpn
|
||||
|
||||
#include "../variant_node.hpp"
|
||||
#include "../traits.hpp"
|
||||
#include "bindings.hpp"
|
||||
|
||||
#ifdef KPN_BUILD_PYTHON
|
||||
#include <nanobind/nanobind.h>
|
||||
#include <nanobind/stl/shared_ptr.h>
|
||||
#include <nanobind/stl/string.h>
|
||||
#include <nanobind/stl/vector.h>
|
||||
|
||||
#include <cctype>
|
||||
#include <string>
|
||||
#include <tuple>
|
||||
#include <type_traits>
|
||||
|
||||
// ── PythonConverter specialisations for built-in nanobind-castable types ─────
|
||||
// These live in kpn:: to match the primary template in variant_node.hpp.
|
||||
|
||||
namespace kpn {
|
||||
|
||||
template<> struct PythonConverter<int> {
|
||||
static constexpr const char* type_name = "int";
|
||||
static nanobind::object to_python(const int& v) { return nanobind::cast(v); }
|
||||
static int from_python(nanobind::object o) { return nanobind::cast<int>(std::move(o)); }
|
||||
};
|
||||
|
||||
template<> struct PythonConverter<float> {
|
||||
static constexpr const char* type_name = "float";
|
||||
static nanobind::object to_python(const float& v) { return nanobind::cast(v); }
|
||||
static float from_python(nanobind::object o) { return nanobind::cast<float>(std::move(o)); }
|
||||
};
|
||||
|
||||
template<> struct PythonConverter<double> {
|
||||
static constexpr const char* type_name = "double";
|
||||
static nanobind::object to_python(const double& v) { return nanobind::cast(v); }
|
||||
static double from_python(nanobind::object o) { return nanobind::cast<double>(std::move(o)); }
|
||||
};
|
||||
|
||||
template<> struct PythonConverter<bool> {
|
||||
static constexpr const char* type_name = "bool";
|
||||
static nanobind::object to_python(const bool& v) { return nanobind::cast(v); }
|
||||
static bool from_python(nanobind::object o) { return nanobind::cast<bool>(std::move(o)); }
|
||||
};
|
||||
|
||||
template<> struct PythonConverter<std::string> {
|
||||
static constexpr const char* type_name = "str";
|
||||
static nanobind::object to_python(const std::string& v) { return nanobind::cast(v); }
|
||||
static std::string from_python(nanobind::object o) {
|
||||
return nanobind::cast<std::string>(std::move(o));
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace kpn
|
||||
|
||||
namespace kpn::python {
|
||||
namespace nb = nanobind;
|
||||
|
||||
// ── Entry<Func, Name> ─────────────────────────────────────────────────────────
|
||||
// Compile-time descriptor for one bindable node function.
|
||||
|
||||
template<auto Func, fixed_string Name>
|
||||
struct Entry {
|
||||
static constexpr auto func = Func;
|
||||
static constexpr auto name = Name;
|
||||
};
|
||||
|
||||
// ── NodeRegistry<Es...> ───────────────────────────────────────────────────────
|
||||
|
||||
template<typename... Es>
|
||||
struct NodeRegistry {
|
||||
using entries_tuple = std::tuple<Es...>;
|
||||
static constexpr std::size_t size = sizeof...(Es);
|
||||
};
|
||||
|
||||
// ── Internal TMP ──────────────────────────────────────────────────────────────
|
||||
|
||||
namespace detail {
|
||||
|
||||
// All non-void port types for a single function (args + normalised returns).
|
||||
template<auto Func>
|
||||
struct entry_port_types {
|
||||
using args = args_t<decltype(Func)>;
|
||||
using ret = normalised_return_t<return_t<decltype(Func)>>;
|
||||
using type = decltype(std::tuple_cat(std::declval<args>(), std::declval<ret>()));
|
||||
};
|
||||
|
||||
// Flat tuple of all types across all entries (may contain duplicates).
|
||||
template<typename... Es>
|
||||
struct all_types_flat {
|
||||
using type = decltype(std::tuple_cat(
|
||||
std::declval<typename entry_port_types<Es::func>::type>()...));
|
||||
};
|
||||
|
||||
template<typename Registry>
|
||||
struct registry_flat_types;
|
||||
|
||||
template<typename... Es>
|
||||
struct registry_flat_types<NodeRegistry<Es...>> {
|
||||
using type = typename all_types_flat<Es...>::type;
|
||||
};
|
||||
|
||||
// Unpack a tuple into unique_types_t (which takes a pack, not a tuple).
|
||||
// unique_types_t<T> takes Ts... not std::tuple<Ts...>, so we need this bridge.
|
||||
template<typename Tuple>
|
||||
struct unpack_unique;
|
||||
|
||||
template<typename... Ts>
|
||||
struct unpack_unique<std::tuple<Ts...>> {
|
||||
using type = kpn::detail::unique_types_t<Ts...>;
|
||||
};
|
||||
|
||||
// SFINAE: does PythonConverter<T> have a 'type_name' member?
|
||||
template<typename Conv, typename = void>
|
||||
struct has_type_name : std::false_type {};
|
||||
|
||||
template<typename Conv>
|
||||
struct has_type_name<Conv, std::void_t<decltype(Conv::type_name)>> : std::true_type {};
|
||||
|
||||
inline std::string make_class_name(std::string_view snake) {
|
||||
std::string result(snake);
|
||||
if (!result.empty()) result[0] = static_cast<char>(std::toupper(result[0]));
|
||||
result += "Node";
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace detail
|
||||
|
||||
// ── registry_variant_t<Registry> ─────────────────────────────────────────────
|
||||
// Deduces std::variant<UniqueTypes...> from all port types across the registry.
|
||||
|
||||
template<typename Registry>
|
||||
using registry_variant_t = typename kpn::detail::tuple_to_variant<
|
||||
typename detail::unpack_unique<
|
||||
typename detail::registry_flat_types<Registry>::type>::type
|
||||
>::type;
|
||||
|
||||
// ── Converter registration ────────────────────────────────────────────────────
|
||||
|
||||
template<typename T, typename Variant>
|
||||
void register_one_type(PyNetwork<Variant>& net) {
|
||||
const char* friendly = nullptr;
|
||||
if constexpr (detail::has_type_name<PythonConverter<T>>::value)
|
||||
friendly = PythonConverter<T>::type_name;
|
||||
|
||||
net.template register_full_type<T>(
|
||||
[](const T& v) -> nb::object { return PythonConverter<T>::to_python(v); },
|
||||
[](nb::object o) -> T { return PythonConverter<T>::from_python(std::move(o)); },
|
||||
friendly);
|
||||
}
|
||||
|
||||
template<typename Variant, typename... Ts>
|
||||
void register_types_impl(PyNetwork<Variant>& net, std::tuple<Ts...>*) {
|
||||
(register_one_type<Ts>(net), ...);
|
||||
}
|
||||
|
||||
template<typename Registry, typename Variant>
|
||||
void register_all_converters(PyNetwork<Variant>& net) {
|
||||
using Flat = typename detail::registry_flat_types<Registry>::type;
|
||||
using Unique = typename detail::unpack_unique<Flat>::type;
|
||||
register_types_impl(net, static_cast<Unique*>(nullptr));
|
||||
}
|
||||
|
||||
// ── Per-entry class + factory registration ───────────────────────────────────
|
||||
|
||||
namespace detail {
|
||||
|
||||
template<typename E, typename Variant>
|
||||
void register_one_entry(nb::module_& m) {
|
||||
using Wrapper = VariantNodeWrapper<E::func, Variant>;
|
||||
|
||||
auto class_name = make_class_name(E::name.view());
|
||||
auto make_name = "make_" + std::string(E::name.view());
|
||||
|
||||
nb::class_<Wrapper, IVariantNode<Variant>>(m, class_name.c_str())
|
||||
.def("__init__", [](Wrapper* self, std::size_t cap) {
|
||||
new (self) Wrapper(cap);
|
||||
}, nb::arg("capacity") = 5);
|
||||
|
||||
m.def(make_name.c_str(),
|
||||
[](std::size_t cap) -> std::shared_ptr<IVariantNode<Variant>> {
|
||||
return std::make_shared<Wrapper>(cap);
|
||||
},
|
||||
nb::arg("capacity") = 5);
|
||||
}
|
||||
|
||||
template<typename Variant, typename... Es>
|
||||
void register_entries_impl(nb::module_& m, std::tuple<Es...>*) {
|
||||
(register_one_entry<Es, Variant>(m), ...);
|
||||
}
|
||||
|
||||
} // namespace detail
|
||||
|
||||
// ── bind_network<Registry> ────────────────────────────────────────────────────
|
||||
// Registers:
|
||||
// - INode — base class (opaque Python handle)
|
||||
// - Network — PyNetwork with auto-registered converters
|
||||
// - <Name>Node — VariantNodeWrapper for each entry
|
||||
// - make_<name>() — factory returning shared_ptr<INode>
|
||||
|
||||
template<typename Registry>
|
||||
void bind_network(nb::module_& m) {
|
||||
using Variant = registry_variant_t<Registry>;
|
||||
using Net = PyNetwork<Variant>;
|
||||
using Entries = typename Registry::entries_tuple;
|
||||
|
||||
nb::class_<IVariantNode<Variant>>(m, "INode");
|
||||
|
||||
nb::class_<Net>(m, "Network")
|
||||
.def("__init__", [](Net* self) {
|
||||
new (self) Net();
|
||||
register_all_converters<Registry>(*self);
|
||||
})
|
||||
// add(name, c++_node)
|
||||
.def("add", [](Net& self, std::string name,
|
||||
std::shared_ptr<IVariantNode<Variant>> node) {
|
||||
self.add(std::move(name), std::move(node));
|
||||
}, nb::arg("name"), nb::arg("node"))
|
||||
// add_node(name, callable, inputs=[...], outputs=[...], capacity=5)
|
||||
.def("add_node", &Net::add_node_python,
|
||||
nb::arg("name"),
|
||||
nb::arg("callable"),
|
||||
nb::arg("inputs") = std::vector<std::string>{},
|
||||
nb::arg("outputs") = std::vector<std::string>{},
|
||||
nb::arg("capacity") = std::size_t(5))
|
||||
.def("connect", &Net::connect,
|
||||
nb::arg("src"), nb::arg("out_idx"),
|
||||
nb::arg("dst"), nb::arg("in_idx"))
|
||||
.def("build", &Net::build)
|
||||
.def("start", &Net::start)
|
||||
.def("stop", &Net::stop)
|
||||
.def("read", &Net::read,
|
||||
nb::arg("node"), nb::arg("out_idx") = std::size_t(0))
|
||||
.def("write", &Net::write,
|
||||
nb::arg("node"), nb::arg("in_idx"), nb::arg("value"))
|
||||
;
|
||||
|
||||
detail::register_entries_impl<Variant>(m, static_cast<Entries*>(nullptr));
|
||||
}
|
||||
|
||||
// ── bind_debug<Registry> ─────────────────────────────────────────────────────
|
||||
// Exposes each node's raw C++ function as a free Python callable so node logic
|
||||
// can be unit-tested without constructing a network.
|
||||
//
|
||||
// Example: assert kpn.double_it(5) == 10
|
||||
|
||||
namespace detail {
|
||||
|
||||
template<typename E>
|
||||
void bind_one_debug(nb::module_& m) {
|
||||
auto name_str = std::string(E::name.view());
|
||||
m.def(name_str.c_str(), E::func);
|
||||
}
|
||||
|
||||
template<typename... Es>
|
||||
void bind_debug_impl(nb::module_& m, std::tuple<Es...>*) {
|
||||
(bind_one_debug<Es>(m), ...);
|
||||
}
|
||||
|
||||
} // namespace detail
|
||||
|
||||
template<typename Registry>
|
||||
void bind_debug(nb::module_& m) {
|
||||
using Entries = typename Registry::entries_tuple;
|
||||
detail::bind_debug_impl(m, static_cast<Entries*>(nullptr));
|
||||
}
|
||||
|
||||
} // namespace kpn::python
|
||||
|
||||
#endif // KPN_BUILD_PYTHON
|
||||
Reference in New Issue
Block a user