[ Web Proxy ]
URL:
Viewing: https://raw.githubusercontent.com/esp-cpp/espp/main/lib/python_bindings/socket_reactor_bindings.cpp [Back]  [Original]

// 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.");
}

Web Proxy Viewer  |  New URL  |  Original Page