KPN/python/kpn_python.cpp
2026-05-08 17:48:16 +02:00

165 lines
7.6 KiB
C++

#define KPN_BUILD_PYTHON
#include <kpn/python/bindings.hpp>
#include <nanobind/nanobind.h>
#include <nanobind/stl/shared_ptr.h>
#include <nanobind/stl/string.h>
#include <nanobind/stl/vector.h>
#include <iostream>
#include <map>
namespace nb = nanobind;
using namespace kpn;
using namespace kpn::python;
// ── Node functions for the hello-pipeline examples ────────────────────────────
static int produce() { return 42; }
static int double_it(int x) { return x * 2; }
static void print_it(int x) { std::cout << "result: " << x << '\n'; }
// ── Variant type ──────────────────────────────────────────────────────────────
// Deduplicated port types across all registered node functions.
// produce: () → int | double_it: int → int | print_it: int → void
// Unique types: int
using KpnVariant = std::variant<int>;
using Net = PyNetwork<KpnVariant>;
using ProduceNode = VariantNodeWrapper<produce, KpnVariant>;
using DoubleItNode= VariantNodeWrapper<double_it, KpnVariant>;
using PrintItNode = VariantNodeWrapper<print_it, KpnVariant>;
// ── Converter helpers for int ─────────────────────────────────────────────────
static nb::object int_to_py(const KpnVariant& v) {
return nb::int_(std::get<int>(v));
}
static KpnVariant int_from_py(nb::object o) {
return KpnVariant{ nb::cast<int>(o) };
}
// ── Type name resolver (extensible) ──────────────────────────────────────────
static std::type_index resolve_type(const std::string& name) {
if (name == "int") return std::type_index(typeid(int));
throw std::runtime_error("unknown type name '" + name +
"' — only 'int' is registered in this module");
}
// ── Ensure converters are registered on a Net instance ───────────────────────
static void ensure_converters(Net& net) {
net.register_type<int>(
[](const int& v) -> nb::object { return nb::int_(v); },
[](nb::object o) -> int { return nb::cast<int>(o); }
);
net.register_tap_factory<int>();
}
NB_MODULE(kpn_python, m) {
m.doc() = "KPN++ Python bindings — Kahn Process Network library";
// ── IVariantNode base ─────────────────────────────────────────────────────
nb::class_<IVariantNode<KpnVariant>>(m, "INode");
// ── Concrete C++ node wrappers ─────────────────────────────────────────────
// Factories return shared_ptr so Python can pass them to net.add().
nb::class_<ProduceNode, IVariantNode<KpnVariant>>(m, "ProduceNode")
.def("__init__", [](ProduceNode* self, std::size_t cap) {
new (self) ProduceNode(cap);
}, nb::arg("capacity") = 5);
nb::class_<DoubleItNode, IVariantNode<KpnVariant>>(m, "DoubleItNode")
.def("__init__", [](DoubleItNode* self, std::size_t cap) {
new (self) DoubleItNode(cap);
}, nb::arg("capacity") = 5);
nb::class_<PrintItNode, IVariantNode<KpnVariant>>(m, "PrintItNode")
.def("__init__", [](PrintItNode* self, std::size_t cap) {
new (self) PrintItNode(cap);
}, nb::arg("capacity") = 5);
// Expose factory functions that return shared_ptr — these are what net.add() accepts.
m.def("make_produce", [](std::size_t cap) -> std::shared_ptr<IVariantNode<KpnVariant>> {
return std::make_shared<ProduceNode>(cap);
}, nb::arg("capacity") = 5);
m.def("make_double_it", [](std::size_t cap) -> std::shared_ptr<IVariantNode<KpnVariant>> {
return std::make_shared<DoubleItNode>(cap);
}, nb::arg("capacity") = 5);
m.def("make_print_it", [](std::size_t cap) -> std::shared_ptr<IVariantNode<KpnVariant>> {
return std::make_shared<PrintItNode>(cap);
}, nb::arg("capacity") = 5);
// ── Network ───────────────────────────────────────────────────────────────
nb::class_<Net>(m, "Network")
.def(nb::init<>())
// add(name, c++_node) — for pre-constructed C++ nodes
.def("add", [](Net& self, std::string name,
std::shared_ptr<IVariantNode<KpnVariant>> node) {
self.add(std::move(name), std::move(node));
}, nb::arg("name"), nb::arg("node"))
// add_node(name, callable, inputs=[type_names], outputs=[type_names])
// Creates a pure Python processing node.
.def("add_node", [](Net& self,
std::string name,
nb::object callable,
std::vector<std::string> in_names,
std::vector<std::string> out_names,
std::size_t capacity)
{
std::vector<std::type_index> in_types, out_types;
for (auto& s : in_names) in_types.push_back(resolve_type(s));
for (auto& s : out_names) out_types.push_back(resolve_type(s));
std::map<std::type_index, std::function<nb::object(const KpnVariant&)>> to_py;
std::map<std::type_index, std::function<KpnVariant(nb::object)>> from_py;
std::map<std::type_index, PyNode<KpnVariant>::ChannelFactory> ch_factories;
auto int_idx = std::type_index(typeid(int));
to_py[int_idx] = int_to_py;
from_py[int_idx] = int_from_py;
ch_factories[int_idx] = [](std::size_t cap) -> std::shared_ptr<IVariantChannel<KpnVariant>> {
auto ch = std::make_shared<Channel<int>>(cap);
return std::make_shared<VariantChannel<int, KpnVariant>>(std::move(ch));
};
auto node = std::make_shared<PyNode<KpnVariant>>(
std::move(callable),
std::move(in_types),
std::move(out_types),
std::move(to_py),
std::move(from_py),
std::move(ch_factories),
capacity
);
self.add(std::move(name), std::move(node));
},
nb::arg("name"), nb::arg("callable"),
nb::arg("inputs") = std::vector<std::string>{},
nb::arg("outputs") = std::vector<std::string>{},
nb::arg("capacity") = 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)
// read(node, out_idx=0) — blocking pop from a C++ node's output port into Python
.def("read", [](Net& self, const std::string& node, std::size_t out_idx) {
ensure_converters(self);
return self.read(node, out_idx);
}, nb::arg("node"), nb::arg("out_idx") = 0)
// write(node, in_idx, value) — push a Python value into a node's input port
.def("write", [](Net& self, const std::string& node,
std::size_t in_idx, nb::object value) {
ensure_converters(self);
self.write(node, in_idx, std::move(value));
}, nb::arg("node"), nb::arg("in_idx"), nb::arg("value"));
}