Multiple applicable items in scope

I am making a crate which needs to support both the tokio and async-std runtime. To do this I use the correct dependency matching the used features flag.

#[allow(unused_imports)]
#[cfg(feature = "tokio")]
use tokio::io::{AsyncReadExt, AsyncWriteExt};

#[allow(unused_imports)]
#[cfg(feature = "async-std")]
use async_std::io::{ReadExt, WriteExt};

Originally I had this code:

self.rt.block_on(async {
     writer
        .write_all(data_to_write)
        .await
        .map_err(ureq::Error::Io)?;

        writer.flush().await.map_err(ureq::Error::Io)?;

        Ok(())
})

This compiles fine using cargo build, but using cargo clippy --all-features --all-targets, which needs to work for the CI pipeline I get the following errors:

error[E0034]: multiple applicable items in scope
   --> crates/arti-ureq/src/lib.rs:250:18
    |
250 |                 .write_all(data_to_write)
    |                  ^^^^^^^^^ multiple `write_all` found
    |
    = note: candidate #1 is defined in an impl of the trait `async_std::io::WriteExt` for the type `T`
    = note: candidate #2 is defined in an impl of the trait `tokio::io::AsyncWriteExt` for the type `W`
help: disambiguate the method for candidate #1
    |
249 |             async_std::io::WriteExt::write_all(&mut writer, data_to_write)
    |
help: disambiguate the method for candidate #2
    |
249 |             tokio::io::AsyncWriteExt::write_all(&mut writer, data_to_write)
    |

I currently solved it like this:

        #[allow(unreachable_code)]
        self.rt.block_on(async {

            #[cfg(feature = "tokio")] {
                tokio::io::AsyncWriteExt::write_all(&mut *writer, data_to_write)
                    .await
                    .map_err(ureq::Error::Io)?;
                tokio::io::AsyncWriteExt::flush(&mut *writer).await.map_err(ureq::Error::Io)?;

                return Ok(());
            }

            #[cfg(feature = "async-std")] {
                async_std::io::WriteExt::write_all(&mut *writer, data_to_write)
                    .await
                    .map_err(ureq::Error::Io)?;
                async_std::io::WriteExt::flush(&mut *writer).await.map_err(ureq::Error::Io)?;
                
                return Ok(());
            }

            panic!("Either `tokio` or `async-std` feature must be enabled on arti_ureq.");
        })

Is this correct, or is there a better way?

For this method I now have a block of duplicate code:

    // Read data from arti stream to ureq buffer.
    fn await_input(&mut self, _timeout: NextTimeout) -> Result<bool, ureq::Error> {
        let mut reader = self.r.lock().expect("lock poisoned");

        let buffers = self.buffer.input_append_buf();
        #[allow(unreachable_code)]
        let size = self.rt.block_on(async {
            let mut temp_buf = vec![0; buffers.len()];

            #[cfg(feature = "tokio")]
            {
                let read_result = tokio::io::AsyncReadExt::read(&mut *reader, &mut temp_buf).await;

                return match read_result {
                    Ok(size) if size > 0 => {
                        buffers[..size].copy_from_slice(&temp_buf[..size]);
                        Ok(size)
                    }
                    Ok(_) => Ok(0),
                    Err(e) => Err(ureq::Error::Io(e)),
                };
            }

            #[cfg(feature = "async-std")]
            {
                let read_result = async_std::io::ReadExt::read(&mut *reader, &mut temp_buf).await;

                return match read_result {
                    Ok(size) if size > 0 => {
                        buffers[..size].copy_from_slice(&temp_buf[..size]);
                        Ok(size)
                    }
                    Ok(_) => Ok(0),
                    Err(e) => Err(ureq::Error::Io(e)),
                };
            }

            panic!("Either `tokio` or `async-std` feature must be enabled on arti_ureq.");
        })?;
        self.buffer.input_appended(size);

        Ok(size > 0)
    }

(the return statement).

Is there a way to resolve this as well?

cargo features are additive, ideally you should not use them for mutually exclusive configurations. however, if there's no easier alternative, at least you should explicitly check the invalid feature combinations and emit more helpful diagnostics for the user. e.g.:

#[cfg(all(feature = "tokio", feature = "async-std"))]
compile_error!("conflicting runtime enabled");

if the features are not mutually exclusive, you should rethink how you design the library. for example, instead of calling the runtime internally to do the IO, you can return a data type that encapsulate the operation, but let the user decide how to "run" it. in the case of async, it's usually done in the form of combinators of Futures. this is similar to the principle of policy based design in C++.

for your example specifically, what's the type of writer or reader?

for example, if it is either some Read type of tokio, or some Read type of async-std, then it should be clear, the feature "tokio" and "async-std" is mutually exclusive.

however, if it is some (generic) wrapper type of your own, that is compatible with both tokio and async-std, and it's up to the user to decide how to use, then it's reasonabe to say the features are not mutually exclusive. then your wrapper type probably should implement your own poll_read()/poll_write() method based on Future, and your function should use it directly, instead of using the AsyncReadExt convenient methods.

struct MyReader {
    //...
}
impl MyReader {
    fn poll_read(&mut self, cx: &mut Context<'_>) -> Poll<> {
        todo!()
    }
}

// make it compatible to `tokio`
#[cfg!(feature = "tokio")]
impl tokio::AsyncRead for MyReader {
    //...
}

// make it compatible with `async-std`
#[cfg(feature = "async-std")]
impl async_std::Read for MyReader {
    //...
}

I think another possibility is, you can use tokio-util, which has a compatibility layer between tokio::AsyncRead and futures_io::AsyncRead (which is what async-std is actually using).