Hello, I'm experimenting with porting some synchronous network server code to Tokio. I have defined a trait Server
as follows:
#[async_trait]
pub trait Server: Sync + 'static {
async fn handler<R, W>(&self, reader: R, writer: W) -> Result<(), Box<dyn std::error::Error>>
where
R: AsyncRead + Unpin + Send,
W: AsyncWrite + Unpin + Send;
/* more methods follow */
I want other methods of the trait spawning a task that calls the handler
, such as this:
async fn other_function(&self) -> Result<(), std::io::Error> {
/* ... */
tokio::task::spawn(async move {
if let Err(err) = self.handler(reader, writer).await {
eprintln!("Error in handler: {}", err);
}
});
}
This gives an error[E0759]: `self` has lifetime `'life0` but it needs to satisfy a `'static` lifetime requirement
.
I fixed this by passing an Arc<Self>
(instead of &self
) to that other_function
.
Question 1:
Is using Arc<Self>
the best way to solve that problem?
My complete code is as follows:
use async_trait::async_trait;
use std::fs;
use std::os::unix::fs::FileTypeExt as _;
use std::path::Path;
use std::sync::Arc;
use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt};
#[async_trait]
pub trait Server: Sync + 'static {
async fn handler<R, W>(&self, reader: R, writer: W) -> Result<(), Box<dyn std::error::Error>>
where
R: AsyncRead + Unpin + Send,
W: AsyncWrite + Unpin + Send;
async fn run_local<P>(self: Arc<Self>, path: P) -> Result<(), std::io::Error>
where
P: AsRef<Path> + Send,
{
if let Ok(meta) = fs::metadata(&path) {
if meta.file_type().is_socket() {
fs::remove_file(&path)?;
}
}
let listener = tokio::net::UnixListener::bind(path)?;
loop {
let (conn, _addr) = listener.accept().await?;
let (reader, writer) = conn.into_split();
let this = self.clone();
tokio::task::spawn(async move {
if let Err(err) = this.handler(reader, writer).await {
eprintln!("Error in handler: {}", err);
}
});
}
}
async fn run_network<A>(self: Arc<Self>, addrs: A) -> Result<(), std::io::Error>
where
A: tokio::net::ToSocketAddrs + Send,
{
let listener = tokio::net::TcpListener::bind(addrs).await?;
loop {
let (conn, _addr) = listener.accept().await?;
let (reader, writer) = conn.into_split();
let this = self.clone();
tokio::task::spawn(async move {
if let Err(err) = this.handler(reader, writer).await {
eprintln!("Error in handler: {}", err);
}
});
}
}
}
struct SaySomething {
greeting: String,
}
#[async_trait]
impl Server for SaySomething {
async fn handler<R, W>(
&self,
_reader: R,
mut writer: W,
) -> Result<(), Box<dyn std::error::Error>>
where
R: AsyncRead + Unpin + Send,
W: AsyncWrite + Unpin + Send,
{
writer.write_all(self.greeting.as_ref()).await?;
Ok(())
}
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let server = Arc::new(SaySomething {
greeting: String::from("Hi there!\n"),
});
let task1 = tokio::task::spawn(server.clone().run_local("socket"));
let task2 = tokio::task::spawn(server.clone().run_network("[::]:1234"));
let (result1, result2) = tokio::join!(task1, task2);
result1??;
result2??;
Ok(())
}
Also note the various Sync
and Send
requirements, particularly R: AsyncRead + Unpin + Send
and W: AsyncWrite + Unpin + Send
.
Question 2:
Why do I need to make the reader and writer to be Send
in the signature of the handler
method? Do futures always have to be Send
able? Or is this some requirement of async_trait
?
I get the following error if I remove the + Send
:
error: future cannot be sent between threads safely
--> src/main.rs:67:5
|
67 | / {
68 | | writer.write_all(self.greeting.as_ref()).await?;
69 | | Ok(())
70 | | }
| |_____^ future created by async block is not `Send`
|
note: captured value is not `Send`
--> src/main.rs:61:9
|
61 | _reader: R,
| ^^^^^^^ has type `R` which is not `Send`
= note: required for the cast to the object type `dyn Future<Output = Result<(), Box<(dyn std::error::Error + 'static)>>> + Send`
help: consider further restricting type parameter `R`
|
59 | async fn handler, R: std::marker::Send
| ~~~~~~~~~~~~~~~~~~~~~~
Learning Rust has been quite a challenge for me. Moving to async Rust seems to make things even more complex, and I wonder why. I think the combination of strict lifetime management plus futures might be inherently complex. Perhaps that's the price to pay when you want your code to be abstract, concurrent, and yet efficient. Or am I overcomplicating things?