From c5ba1f0632a2a457dd516a2d0a26835baea66b9d Mon Sep 17 00:00:00 2001 From: David Hewitt <1939362+davidhewitt@users.noreply.github.com> Date: Thu, 22 Sep 2022 11:14:06 +0200 Subject: [PATCH] pyclass: better error and explanation why lifetimes are disallowed (#2633) * pyclass: better error and explanation why lifetimes are disallowed * extend detail on lifetimes --- guide/src/class.md | 24 ++++++++++++++++++++++-- pyo3-macros-backend/src/pyclass.rs | 12 +++++++++++- tests/ui/reject_generics.rs | 5 +++++ tests/ui/reject_generics.stderr | 8 +++++++- 4 files changed, 45 insertions(+), 4 deletions(-) diff --git a/guide/src/class.md b/guide/src/class.md index 04decec6..19f5eb88 100644 --- a/guide/src/class.md +++ b/guide/src/class.md @@ -50,10 +50,28 @@ enum MyEnum { } ``` -Because Python objects are freely shared between threads by the Python interpreter, all types annotated with `#[pyclass]` must implement `Send` (unless annotated with [`#[pyclass(unsendable)]`](#customizing-the-class)). - The above example generates implementations for [`PyTypeInfo`] and [`PyClass`] for `MyClass` and `MyEnum`. To see these generated implementations, refer to the [implementation details](#implementation-details) at the end of this chapter. +### Restrictions + +To integrate Rust types with Python, PyO3 needs to place some restrictions on the types which can be annotated with `#[pyclass]`. In particular, they must have no lifetime parameters, no generic parameters, and must implement `Send`. The reason for each of these is explained below. + +#### No lifetime parameters + +Rust lifetimes are used by the Rust compiler to reason about a program's memory safety. They are a compile-time only concept; there is no way to access Rust lifetimes at runtime from a dynamic language like Python. + +As soon as Rust data is exposed to Python, there is no guarantee which the Rust compiler can make on how long the data will live. Python is a reference-counted language and those references can be held for an arbitrarily long time which is untraceable by the Rust compiler. The only possible way to express this correctly is to require that any `#[pyclass]` does not borrow data for any lifetime shorter than the `'static` lifetime, i.e. the `#[pyclass]` cannot have any lifetime parameters. + +When you need to share ownership of data between Python and Rust, instead of using borrowed references with lifetimes consider using reference-counted smart pointers such as [`Arc`] or [`Py`]. + +#### No generic parameters + +A Rust `struct Foo` with a generic parameter `T` generates new compiled implementations each time it is used with a different concrete type for `T`. These new implementations are generated by the compiler at each usage site. This is incompatible with wrapping `Foo` in Python, where there needs to be a single compiled implementation of `Foo` which is integrated with the Python interpreter. + +#### Must be send + +Because Python objects are freely shared between threads by the Python interpreter, there is no guarantee which thread will eventually drop the object. Therefore all types annotated with `#[pyclass]` must implement `Send` (unless annotated with [`#[pyclass(unsendable)]`](#customizing-the-class)). + ## Constructor By default it is not possible to create an instance of a custom class from Python code. @@ -996,12 +1014,14 @@ impl pyo3::impl_::pyclass::PyClassImpl for MyClass { [`GILGuard`]: {{#PYO3_DOCS_URL}}/pyo3/struct.GILGuard.html [`PyTypeInfo`]: {{#PYO3_DOCS_URL}}/pyo3/type_object/trait.PyTypeInfo.html +[`Py`]: {{#PYO3_DOCS_URL}}/pyo3/struct.Py.html [`PyCell`]: {{#PYO3_DOCS_URL}}/pyo3/pycell/struct.PyCell.html [`PyClass`]: {{#PYO3_DOCS_URL}}/pyo3/pyclass/trait.PyClass.html [`PyRef`]: {{#PYO3_DOCS_URL}}/pyo3/pycell/struct.PyRef.html [`PyRefMut`]: {{#PYO3_DOCS_URL}}/pyo3/pycell/struct.PyRefMut.html [`PyClassInitializer`]: {{#PYO3_DOCS_URL}}/pyo3/pyclass_init/struct.PyClassInitializer.html +[`Arc`]: https://doc.rust-lang.org/std/sync/struct.Arc.html [`RefCell`]: https://doc.rust-lang.org/std/cell/struct.RefCell.html [classattr]: https://docs.python.org/3/tutorial/classes.html#class-and-instance-variables diff --git a/pyo3-macros-backend/src/pyclass.rs b/pyo3-macros-backend/src/pyclass.rs index 9a800ee5..d318b5a1 100644 --- a/pyo3-macros-backend/src/pyclass.rs +++ b/pyo3-macros-backend/src/pyclass.rs @@ -197,9 +197,19 @@ pub fn build_py_class( ); let krate = get_pyo3_crate(&args.options.krate); + if let Some(lt) = class.generics.lifetimes().next() { + bail_spanned!( + lt.span() => + "#[pyclass] cannot have lifetime parameters. \ + For an explanation, see https://pyo3.rs/latest/class.html#no-lifetime-parameters" + ); + } + ensure_spanned!( class.generics.params.is_empty(), - class.generics.span() => "#[pyclass] cannot have generic parameters" + class.generics.span() => + "#[pyclass] cannot have generic parameters. \ + For an explanation, see https://pyo3.rs/latest/class.html#no-generic-parameters" ); let field_options = match &mut class.fields { diff --git a/tests/ui/reject_generics.rs b/tests/ui/reject_generics.rs index b400cd2b..55142ee0 100644 --- a/tests/ui/reject_generics.rs +++ b/tests/ui/reject_generics.rs @@ -5,4 +5,9 @@ struct ClassWithGenerics { a: A, } +#[pyclass] +struct ClassWithLifetimes<'a> { + a: &'a str, +} + fn main() {} diff --git a/tests/ui/reject_generics.stderr b/tests/ui/reject_generics.stderr index 0956c735..2285b927 100644 --- a/tests/ui/reject_generics.stderr +++ b/tests/ui/reject_generics.stderr @@ -1,5 +1,11 @@ -error: #[pyclass] cannot have generic parameters +error: #[pyclass] cannot have generic parameters. For an explanation, see https://pyo3.rs/latest/class.html#no-generic-parameters --> tests/ui/reject_generics.rs:4:25 | 4 | struct ClassWithGenerics { | ^ + +error: #[pyclass] cannot have lifetime parameters. For an explanation, see https://pyo3.rs/latest/class.html#no-lifetime-parameters + --> tests/ui/reject_generics.rs:9:27 + | +9 | struct ClassWithLifetimes<'a> { + | ^^