Search code examples
rustpyo3

How to change a value so that it doesn't have static lifetime in Rust


I have the following function that uses PyO3 to call a python function and get a result (in this case, an int that gets assigned to a i32):

fn run_python<'a, T: FromPyObject<'a> + Clone>(func_name: &str) -> Result<T, ()> {
    Python::with_gil(|py| {
        let pyapi = match py.import("pyapi") {
            Ok(v) => v,
            Err(e) => { e.print_and_set_sys_last_vars(py); return Err(()) },
        };

        let locals = [("pyapi", pyapi)].into_py_dict(py);
        let eval_result: PyResult<&PyAny> = py.eval("pyapi.{}(**kwargs)", None, Some(&locals));

        let wrapped_obj: &PyAny = match eval_result {
            Ok(v) => v,
            Err(e) => { e.print_and_set_sys_last_vars(py); return Err(()) },
        };

        let unwrapped_result: PyResult<T> = wrapped_obj.extract();

        match unwrapped_result {
            Ok(v) => return Ok(v.clone()),
            Err(e) => return Err(()),
        };
    })
}

When I try to compile, I get the following error:

error[E0495]: cannot infer an appropriate lifetime for lifetime parameter `'p` due to conflicting requirements
   --> src\bin\launch.rs:89:30
    |
89  |         let eval_result = py.eval("pyapi.{}(**kwargs)", None, Some(&locals));
    |                              ^^^^
    |
note: first, the lifetime cannot outlive the anonymous lifetime #1 defined on the body at 82:22...
   --> src\bin\launch.rs:82:22
    |
82  |       Python::with_gil(|py| {
    |  ______________________^
83  | |         let pyapi = match py.import("pyapi") {
84  | |             Ok(v) => v,
85  | |             Err(e) => { e.print_and_set_sys_last_vars(py); return Err(()) },
...   |
101 | |         };
102 | |     })
    | |_____^
note: ...so that the types are compatible
   --> src\bin\launch.rs:89:30
    |
89  |         let eval_result = py.eval("pyapi.{}(**kwargs)", None, Some(&locals));
    |                              ^^^^
    = note: expected `pyo3::Python<'_>`
               found `pyo3::Python<'_>`
note: but, the lifetime must be valid for the lifetime `'a` as defined on the function body at 81:15...
   --> src\bin\launch.rs:81:15
    |
81  | fn run_python<'a, T: FromPyObject<'a> + Clone>(func_name: &str) -> Result<T, ()> {
    |               ^^
note: ...so that the types are compatible
   --> src\bin\launch.rs:96:57
    |
96  |         let unwrapped_result: PyResult<T> = wrapped_obj.extract();
    |                                                         ^^^^^^^
    = note: expected `pyo3::FromPyObject<'_>`
               found `pyo3::FromPyObject<'a>`

I'm very new to Rust, and am probably doing something silly (and very likely could be an X/Y problem). How can I get a value out of py.eval that doesn't have a lifetime tied to the python interpreter (which is what I'm assuming is going on here)?


Solution

  • The fix that Ry suggests, seems to work well. Using for<'p> lets the compiler defer evaluation of the lifetime until it's needed when processing the code that calls .extract(). And the lifetime, 'p, doesn't need to be specified in the function's generic parameter list. Also, the Clone bound wasn't required.

    fn run_python<T>(func_name: &str) -> Result<T, ()>
    where
        T: for<'p> FromPyObject<'p>,
    {
        let guard = Python::acquire_gil();
        let py    = guard.python();    
        match || -> _ { // try...
            let pyapi  = py.import("pyapi")?; // throw...
            let locals = [("pyapi", pyapi)].into_py_dict(py);            
            py.eval(&format!("pyapi.{}(**kwargs)", func_name), 
                    None, 
                    Some(&locals))?.extract()
        }() { // catch...
            Err(e) => {
                // Error handling specific to Pyo3.
                e.print_and_set_sys_last_vars(py);
                // Return the error type spec'd in the fn signature.
                Err(())
            },
            Ok(obj) => Ok(obj),
        }    
    }
    

    The only limitation to this approach would be the type that .extract() converts to needs to not depend on the lifetime of the type it's converting from. For instance if run_python() could return list of strings, this is possible:

    let values: Vec<String> = run_python("make_string_list").unwrap(); // OK.
    

    But this would produce a lifetime related compiler error although .extract() is capable of producing this type under the right condtions:

    let values: Vec<&str> = run_python("make_string_list").unwrap(); // Error.
    

    If run_python() needed to be able to produce lifetime dependent values, then a solution could be for the caller to grab the GIL, and pass in a Python instance. The function could look like this:

    fn run_python<'p, T>(func_name: &str, py: Python<'p>) -> PyResult<T>
    where
        T: FromPyObject<'p>,
    {
        let pyapi  = py.import("pyapi")?;
        let locals = [("pyapi", pyapi)].into_py_dict(py);            
        py.eval(&format!("pyapi.{}(**kwargs)", func_name), 
                None, 
                Some(&locals))?.extract()
    }
    

    I started writing another answer before I realized the suggestion to use for<> was the best alternative. I had assumed that the return value had some dependency on the GIL, but .extract() returns types that don't depend on the GIL.

    In the previous answer, I suggested ways to deal with Python objects that need to be held beyond GIL lifetimes. This involved converting GIL-Dependent types to GIL-Independent types using .to_object(py), and back again using methods like .cast_as::<PyType>(py) when they're needed.