312 lines
11 KiB
C++
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
|