0

I have the following working implementation for reading bytes from a datachannel and returning the bytes as a list in the python interop:

    fn read<'a>(&self, py: Python<'a>) -> PyResult<&'a PyAny> {
        let mut dc = self.datachannel.clone();
        let mut buf = vec![0u8; 2048 as usize];
        
        pyo3_asyncio::tokio::future_into_py(py, async move {
            let n = dc.read(&mut buf).await.unwrap();
            assert!(n < 2048);
            Ok(buf[..n].to_vec())
        })
    }

However, I would like this function to return the raw bytes as PyBytes instead. My initial thought was to do as the following

    fn read<'a>(&self, py: Python<'a>) -> PyResult<&'a PyAny> {
        let mut dc = self.datachannel.clone();
        let mut buf = vec![0u8; 2048 as usize];
        
        pyo3_asyncio::tokio::future_into_py(py, async move {
            let n = dc.read(&mut buf).await.unwrap();
            assert!(n < 2048);
            let py_bytes = PyBytes::new(py, &buf[..n]);
            Ok(py_bytes.into())
        })
    }

but I am getting the compile error:

37  |           pyo3_asyncio::tokio::future_into_py(py, async move {
    |  _________-----------------------------------_____^
    | |         |
    | |         required by a bound introduced by this call
38  | |             let n = dc.read(&mut buf).await.unwrap();
39  | |             assert!(n < 2048);
40  | |             let py_bytes = PyBytes::new(py, &buf[..n]);
41  | |             Ok(py_bytes.into())
42  | |         })
    | |_________^ `*mut pyo3::Python<'static>` cannot be shared between threads safely

How do I make the read function return PyBytes?

Kevin
  • 3,096
  • 2
  • 8
  • 37
  • I'm no expert, but the [py03-asyncio documentation](https://docs.rs/pyo3-asyncio/latest/pyo3_asyncio/index.html) always uses `Python::with_gil` to construct the return value. It doesn't try to use the original `py`. EDIT: probably because async tasks need to be cooperative and not hold the GIL the whole time the task is executing. – kmdreko Aug 13 '23 at 22:16
  • I tried using the python GIL with `Ok(Python::with_gil(|py| PyBytes::new(py, &buf[..n])))` but I am getting instead that the lifetime may not live long enough since the lifetime of py does not match the lifetime of the return value from `PyBytes::new`. – Kevin Aug 13 '23 at 22:25

1 Answers1

1

Keeping a handle to py means holding the global interpreter lock (GIL) which you should avoid with asynchronous tasks or else they lose their cooperative nature.

Instead you should use Python::with_gil to reacquire the GIL to construct the result. To return a PyBytes from this, you must un-link it from the new py reference which you can do by turning it into a PyObject.

Give this a try:

use pyo3::ToPyObject;

fn read<'a>(&self, py: Python<'a>) -> PyResult<&'a PyAny> {
    let mut dc = self.datachannel.clone();
    let mut buf = vec![0u8; 2048 as usize];
    
    pyo3_asyncio::tokio::future_into_py(py, async move {
        let n = dc.read(&mut buf).await.unwrap();
        assert!(n < 2048);
        Python::with_gil(|py| Ok(PyBytes::new(py, &buf[..n]).to_object(py)))
    })
}
kmdreko
  • 42,554
  • 6
  • 57
  • 106
  • Did you mean "concurrent nature" instead of "cooperative nature". I don't see how the cooperativeness i.e. the need to explicitly `await` rather than preemptive scheduling gets lost by holding a GIL. Maybe I've mixed up some terms? – cafce25 Aug 14 '23 at 11:21
  • @cafce25 I worded it that way because holding the GIL would not be "cooperating" with the rest of the Python system (in the traditional sense of the word). But you're right in the technical sense since "cooperative multitasking" usually just requires yielding. – kmdreko Aug 14 '23 at 15:07