1

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)?

Eli Stevens
  • 1,447
  • 1
  • 12
  • 21
  • If you're new to Rust, I might propose that interfacing with Python is not a great project for learning. The borrow checker is a beast, and trying to learn it while understanding the complexities of any (let alone Rust's) FFI is going to be incredibly complicated. – Silvio Mayolo Aug 06 '21 at 19:46
  • 4
    Normally I’d try this first and answer, but it’s some time to set up. Does `run_python FromPyObject<'a> + Clone>` work? Possibly `run_python FromPyObject<'a> + Clone + 'static>`? (And maybe you wanted `+ 'static` instead of `+ Clone` to begin with?) – Ry- Aug 06 '21 at 20:53
  • @Ry- Yep, the first suggestion did the trick. Would you like to make an answer so that I can accept it? Bonus points if there's a link to docs that explain what's going on in more detail than https://doc.rust-lang.org/nomicon/hrtb.html . Thanks! – Eli Stevens Aug 06 '21 at 23:30
  • If Ry wants to post an answer, I can remove mine. I had written an answer with another type of fix, until I realized the `for<'b>` approach is likely the best option, so I edited it. – Todd Aug 08 '21 at 06:16

1 Answers1

1

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.

Todd
  • 4,669
  • 1
  • 22
  • 30
  • Thanks for providing an answer (I don't get the impression that Ry is too worried about the points). I'm trying to understand why the `match` you have is idiomatic; what's the reason to have the `let pyapi` and `let locals` inside the match vs. outside? My guess is that it's due to the `?` on `py.import` needing to flow into the match, and that's supposed to be cleaner than returning early? – Eli Stevens Aug 10 '21 at 04:34
  • 1
    The match statement is probably more "elegant" (if it could be called that) than idiomatic. What's going on there is the `match` argument is actually a closure, and the parenthesis immediately after the last right-curly-brace invoke it. So the let bindings are local to the closure. The idea here was to capture any errors in the closure block with the `?` convention, and handle in the match body below the closure. I haven't seen any other cases of this monstrosity (?) - I stumbled on it when I was agonizing over the lack of try/catch semantics like other OO languages. @EliStevens – Todd Aug 10 '21 at 05:49