// Hand-written pybind11 bindings for espp::SocketReactor.
//
// Why hand-written (see the note in autogenerate_bindings.py): litgen/srcmlcpp cannot parse
// SocketReactor (brace-init default members with parenthesized/cast expressions trip srcmlcpp's
// brace-init fixer), and its TCP callbacks take std::unique_ptr / TcpSocket& which
// litgen cannot bind. This shim exposes a clean, GIL-correct subset for Python: the reactor
// lifecycle and the UDP-receiver path (the TCP listener/stream paths remain C++-only for now).
//
// It is kept out of the generated pybind_espp.cpp so regeneration never clobbers it.
#include
#include
#include
#include
#include
#include
#include
#include
#include "dscp.hpp"
#include "qos_band.hpp"
#include "socket_reactor.hpp"
#include "udp_socket.hpp"
namespace py = pybind11;
using espp::SocketReactor;
namespace {
// Adapt a Python callable `cb(data: bytes, sender: Socket.Info) -> Optional[bytes]` into the C++
// receive callback. Like the rtps shim: capture the py::function in a shared_ptr so the reactor may
// copy the std::function off the GIL (only the shared_ptr refcount moves, which is GIL-free); the
// callable is invoked and finally destroyed under the GIL.
espp::Socket::receive_callback_fn wrap_receive_callback(const py::function &fn) {
if (!fn) {
return {};
}
// Own the py::function via a shared_ptr with a GIL-acquiring deleter: the last
// reference may be released on a non-Python thread (a reactor pool worker
// erasing an entry after remove()), and ~py::function must run under the GIL.
auto cb = std::shared_ptr(new py::function(fn), [](py::function *f) {
py::gil_scoped_acquire gil;
delete f;
});
return [cb](std::vector &data,
const espp::Socket::Info &sender) -> std::optional {
py::gil_scoped_acquire gil;
// The handler runs on a reactor pool worker; a Python exception (or a bad
// return type) must not propagate out - it would crash/stall the worker.
// Report it and degrade to "no response".
try {
py::object result =
(*cb)(py::bytes(reinterpret_cast(data.data()), data.size()), sender);
if (result.is_none()) {
return std::nullopt;
}
// Accept either bytes or str as the response payload.
std::string s = py::cast(result);
return std::vector(s.begin(), s.end());
} catch (py::error_already_set &e) {
// Reports the traceback via sys.unraisablehook and clears the error.
e.discard_as_unraisable("espp.SocketReactor receive callback");
return std::nullopt;
} catch (const std::exception &e) {
py::print("espp.SocketReactor receive callback error:", e.what());
return std::nullopt;
}
};
}
} // namespace
void py_init_socket_reactor(py::module &m) {
py::class_(
m, "SocketReactor", py::dynamic_attr(),
"A select()-based event loop that multiplexes many receiver sockets onto a thread pool, "
"instead of one thread per socket.")
// Construct with an owned thread pool of `worker_count` workers. (Sharing an external
// pool is available in C++ but not exposed here.)
.def(py::init(
[](std::size_t worker_count, bool auto_start, espp::Logger::Verbosity log_level) {
SocketReactor::Config config;
config.pool_config.worker_count = worker_count;
config.auto_start = auto_start;
config.log_level = log_level;
return std::make_unique(config);
}),
py::arg("worker_count") = 2, py::arg("auto_start") = true,
py::arg("log_level") = espp::Logger::Verbosity::WARN)
// Release the GIL around start()/stop(): stop() blocks waiting for
// in-flight handlers, and a pool worker running a Python receive callback
// needs the GIL - holding it here would deadlock.
.def("start", &SocketReactor::start, py::call_guard(),
"Start the select() loop (and owned pool).")
.def("stop", &SocketReactor::stop, py::call_guard(),
"Stop the loop and wait for in-flight handlers to finish.")
.def("is_running", &SocketReactor::is_running)
.def("num_registered", &SocketReactor::num_registered)
.def("remove",
static_cast(&SocketReactor::remove),
py::arg("id"), "Unregister a socket by the id returned from add_udp_receiver().")
.def(
"add_udp_receiver",
[](SocketReactor &self, espp::UdpSocket &socket, std::size_t port,
std::size_t buffer_size, const py::function &callback, espp::QosBand band,
std::optional dscp) -> SocketReactor::Id {
espp::UdpSocket::ReceiveConfig rc;
rc.port = port;
rc.buffer_size = buffer_size;
rc.on_receive_callback = wrap_receive_callback(callback);
rc.band = band;
rc.dscp = dscp;
return self.add_udp_receiver(socket, rc);
},
py::arg("socket"), py::arg("port"), py::arg("buffer_size"), py::arg("callback"),
py::arg("band") = espp::QosBand::Normal, py::arg("dscp") = std::optional{},
"Bind `socket` to `port` and receive on it via the reactor. `callback(data: bytes, "
"sender) -> Optional[bytes]`; a returned bytes is sent back to the sender. `band` "
"selects the espp.QosBand this socket's handlers are dispatched at; `dscp` (an "
"espp.Dscp, e.g. Dscp.Ef) optionally marks transmitted replies (IP_TOS, best-effort). "
"Returns a registration id (0 == INVALID_ID on failure).")
.def_property_readonly_static(
"INVALID_ID", [](py::object) { return SocketReactor::INVALID_ID; },
"The id value returned by add_* on failure.");
}