1

Say I have the following C++ Date class. Using pybind11 I can easily wrap it to be used in Python:

/* date.hpp */ class Date { public: int year, month, day; Date(int year, int month, int day); Date add_days(int N) const; // Returns new date with N days added bool is_weekend() const; // Returns true if date is Sat or Sun }; 
/* date_bindings.cpp */ #include <pybind11/pybind11.h> #include <date.hpp> namespace py = pybind11; PYBIND11_MODULE(date_py, m) { py::class_<Date>(m, "Date") .def(py::init<int, int, int>(), py::arg("year"), py::arg("month"), py::arg("day")) .def("add_days", &Date::add_days, py::arg("N")) .def("is_weekend", &Date::is_weekend) .def_readwrite("year", &Date::year) .def_readwrite("month", &Date::month) .def_readwrite("day", &Date::day); } 

Now, I want to do the same in Rust. So I implemented my Date class and then used PyO3 to wrap it. See code below.

I've noticed that the PyO3 bindings contain a lot of boilerplate, and hence they are much longer compared to pybind11:

  • 14 lines with pybind11
  • 54 lines with PyO3

Am I using PyO3 correctly? Are there ways to shorten the code to achieve a similar level of "conciseness" as is the case for pybind11?

edit: If there is a way to create bindings without the DatePy helper struct, I would actually prefer that.

/* date.rs */ #[derive(Debug)] pub struct Date { pub year: u16, pub month: u8, pub day: u8 } impl Date { pub const fn add_days(&self, N: u8) -> Self { // Simplified logic for demonstration purposes Date{year: self.year, month: self.month, day: self.day + N} } pub const fn is_weekend(&self) -> bool { // ... true } } 
/* date_bindings.rs */ use pyo3::prelude::*; use my_library::Date; #[pyclass(name = "Date")] struct DatePy { _date: Date } #[pymethods] impl DatePy { #[new] fn new(year: u16, month: u8, day: u8) -> Self { DatePy { _date: Date{year, month, day} } } fn add_days(&self, N: u8) -> PyResult<Self> { Ok(DatePy{_date: self._date.add_days(N)}) } fn is_weekend(&self) -> PyResult<bool> { Ok(self._date.is_weekend()) } #[getter] fn get_year(&self) -> PyResult<u16> { Ok(self._date.year) } #[getter] fn get_month(&self) -> PyResult<u8> { Ok(self._date.month) } #[getter] fn get_day(&self) -> PyResult<u8> { Ok(self._date.day) } #[setter] fn set_year(&mut self, year: u16) -> PyResult<()> { self._date.year = year; Ok(()) } #[setter] fn set_month(&mut self, month: u8) -> PyResult<()> { self._date.month = month; Ok(()) } #[setter] fn set_day(&mut self, day: u8) -> PyResult<()> { self._date.day = day; Ok(()) } } #[pymodule] fn date_py(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::<DatePy>()?; Ok(()) } 
8
  • Assuming you do not want to give up the DatePy -> Date wrapping, I would say "judicious use of macros to generate the getters and setters". Commented Jul 11, 2024 at 8:10
  • @Botje I only created DatePy as a helper struct. If there is a way to create bindings without it, I am not opposed to it. Commented Jul 11, 2024 at 8:13
  • You can transfer the #[pyclass] annotation to Date and put #[pyo3(get, set)] annotations on the member fields Commented Jul 11, 2024 at 8:20
  • @Botje If I add the #[pyclass] annotation to my original Date class, then that means my core rust class & Python bindings will be merged into one? Is there no way to keep them independent & simply expose the class to Python? Similar to the pybind example? Commented Jul 11, 2024 at 8:27
  • I don't know how exactly the C++ works, but your snippet doesn't seem to have a dedicated PyDate either, so why do you want one for Rust but not for C++? Commented Jul 11, 2024 at 8:41

0

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.