Search code examples
pythonrustpyo3

Async function with self-reference in Pyo3


I have the following example:

use pyo3::prelude::*;
use std::collections::{HashMap, HashSet};
use std::sync::RwLock;

#[pyclass]
struct Rustex {
    map: RwLock<HashMap<String, String>>,
    contexts: RwLock<HashSet<String>>,
}

#[pymethods]
impl Rustex {
    #[new]
    fn new() -> Self {
        Rustex {
            map: RwLock::new(HashMap::new()),
            contexts: RwLock::new(HashSet::new()),
        }
    }

    fn acquire_mutex<'a>(
        &mut self,
        py: Python<'a>,
        mutex_name: String,
        context: String,
    ) -> PyResult<&'a PyAny> {
        pyo3_asyncio::async_std::future_into_py(py, async move {
            let mut map = self.map.write().unwrap();
            Ok(Python::with_gil(|py| py.None()))
        })
    }
}

#[pymodule]
fn rustex(_py: Python, m: &PyModule) -> PyResult<()> {
    m.add_class::<Rustex>()?;

    Ok(())
}

When compiled, the error says:

error[E0759]: `self` has an anonymous lifetime `'_` but it needs to satisfy a `'static` lifetime requirement
   --> src/main.rs:27:64
    |
22  |           &mut self,
    |           --------- this data with an anonymous lifetime `'_`...
...
27  |           pyo3_asyncio::async_std::future_into_py(py, async move {
    |  ________________________________________________________________^
28  | |             let mut map = self.map.write().unwrap();
29  | |             Ok(Python::with_gil(|py| py.None()))
30  | |         })
    | |_________^ ...is used here...
    |
note: ...and is required to live as long as `'static` here
   --> src/main.rs:27:9
    |
27  |         pyo3_asyncio::async_std::future_into_py(py, async move {
    |         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
note: `'static` lifetime requirement introduced by this bound
   --> /Users/shep/.cargo/registry/src/github.com-1ecc6299db9ec823/pyo3-asyncio-0.16.0/src/async_std.rs:318:46
    |
318 |     F: Future<Output = PyResult<T>> + Send + 'static,
    |                                              ^^^^^^^

If I make self 'static then gil isn't happy:

error[E0597]: `_ref` does not live long enough
  --> src/main.rs:11:1
   |
11 | #[pymethods]
   | ^^^^^^^^^^^-
   | |          |
   | |          `_ref` dropped here while still borrowed
   | borrowed value does not live long enough
   | argument requires that `_ref` is borrowed for `'static`
   |
   = note: this error originates in the attribute macro `pymethods` (in Nightly builds, run with -Z macro-backtrace for more info)

error[E0597]: `gil` does not live long enough
  --> src/main.rs:11:1
   |
11 | #[pymethods]
   | ^^^^^^^^^^^-
   | |          |
   | |          `gil` dropped here while still borrowed
   | borrowed value does not live long enough
   | argument requires that `gil` is borrowed for `'static`
   |
   = note: this error originates in the attribute macro `pymethods` (in Nightly builds, run with -Z macro-backtrace for more info)

Is there a way to either

  • make the self reference not 'static but still available in the async
  • make gil live long enough

I am using the following crates:

pyo3 = { version = "0.16.5", features = ["extension-module"] }
pyo3-asyncio = { version = "0.16", features = ["attributes", "async-std-runtime"] }
async-std = "1.9"

Solution

  • As correctly pointed out by @ShepMaster, the solution here is to Arc the self-reference in order to allow for multiple ownership. Therefore Rustex is now treated as a 'core'-object and any references go via the Arc. Therefore the structure becomes:

    #[derive(Default)]
    struct RustexCore {
        map: RwLock<HashMap<String, String>>,
        contexts: RwLock<HashSet<String>>,
    }
    
    #[pyclass]
    #[derive(Default)]
    struct Rustex(Arc<RustexCore>);
    

    Now any function will access one of the references and get access to the object by cloning the reference:

    #[pymethods]
    impl Rustex {
    
        fn acquire_mutex<'a>(
            &self,
            py: Python<'a>,
            mutex_name: String,
            context: String
        ) -> PyResult<&'a PyAny> {
            let core = Arc::clone(&self.0);
    

    From there on this core can be used also in the async function. The full code is:

    use pyo3::prelude::*;
    use std::{
        collections::{HashMap, HashSet},
        sync::{Arc, RwLock},
    };
    
    #[derive(Default)]
    struct RustexCore {
        map: RwLock<HashMap<String, String>>,
        contexts: RwLock<HashSet<String>>,
    }
    
    #[pyclass]
    #[derive(Default)]
    struct Rustex(Arc<RustexCore>);
    
    #[pymethods]
    impl Rustex {
        #[new]
        fn new() -> Self {
            Self::default()
        }
    
        fn acquire_mutex<'a>(
            &self,
            py: Python<'a>,
            mutex_name: String,
            context: String,
        ) -> PyResult<&'a PyAny> {
            let core = Arc::clone(&self.0);
            pyo3_asyncio::async_std::future_into_py(py, async move {
                let mut map = core.map.write().unwrap();
                Ok(Python::with_gil(|py| py.None()))
            })
        }
    }
    
    #[pymodule]
    fn rustex(_py: Python, m: &PyModule) -> PyResult<()> {
        m.add_class::<Rustex>()?;
    
        Ok(())
    }