KPN/include/kpn/python/auto_bind.hpp
2026-05-12 21:23:33 +02:00

312 lines
11 KiB
C++

#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