Hi all,
my company has developed a library for physical quantities in rust, which we intend to make open source soon. In order to give the community something of value we would like to improve the code quality before doing so.
The library implements its quantities as shown here for area:
...
#[pyclass(eq, eq_int)]
#[derive(Copy, Clone, PartialEq)]
pub enum AreaUnit {
mmsq,
...
}
...
impl_quantity!(Area, AreaUnit, AreaUnit::mmsq);
impl_div_with_self_to_f64!(Area);
impl_sqrt!(Area, Distance);
impl_mul!(Area, Stress, Force);
impl_mul_with_self!(Area, AreaOfMoment);
impl_div!(Area, Distance, Distance);
impl_div!(Area, Volume, InverseDistance);
This hides all implementation of the actual operations in the macros which we work well with. Since we also want to be able to make quantities available as Python objects we implement a pyO3 wrapper struct which implements the allowed division operations like this:
...
#[pymethods]
impl Area {
fn __truediv__(lhs: PyRef<Self>, rhs: Py<PyAny>) -> PyResult<PyObject> {
let py = lhs.py();
let rhs_ref = rhs.bind(py);
if let Ok(rhs_value) = extract_f64(rhs_ref) {
return Ok(Py::new(
py,
Area {
value: lhs.clone().into_raw() / rhs_value,
},
)?
.into_py(py));
} else if let Ok(rhs_distance) = rhs_ref.extract::<PyRef<Distance>>() {
let distance_value = lhs.clone().into_raw() / rhs_distance.clone().into_raw();
return Ok(Py::new(py, Distance::from_raw(distance_value))?.into_py(py));
} else if let Ok(rhs_area) = rhs_ref.extract::<PyRef<Area>>() {
return Ok((lhs.clone().into_raw() / rhs_area.clone().into_raw()).into_py(py));
}
Err(PyValueError::new_err(
"rhs must be a floating-point number, Distance, or Area",
))
}
}
...
Since the allowed operations between quantities grow super linear with respect to the implemented quantities we have a lot more wrapper code then actual implementation, we would like to also implement the python operators via macros.
However, we do not have a good idea on how to do that since a function like truediv would have to be extended in each macro call. To complicate things further PyO3 only allows one #[pymethods] block per struct.
The only viable option that we currently see is to handle the problem by generating the code prior to build by running a script. Do you have any other ideas that might be less hacky?
All the best
Henrik