Hello! This is my first post here. I am relatively new to rust but not exactly a novice, and I have been programming for decades in languages like C and C++ and, more recently, go. I did one non-trivial project in rust about two years ago and and back in rust for a while for a new project.
I have read several posts about async traits and Send including the blog post about the stabilization of async traits in rust 1.75 (I think). I've seen lots of discussions about issues similar to this, but I think my question is slightly different. If this is a duplicate, please accept my apologies -- I really did search around before posting.
My specific use case is that I have rust API that, among other things, communicates with a server using HTTP calls. There is trait object -- let's call it Communication -- that has a method that gets text from a URL with a GET call -- call it url_get
. I have an implementation of the trait that uses the reqwest
crate. Now I need to create two wrappers around this rust API: a C wrapper that runs single-threaded, and a Wasm wrapper that is async but uses fetch via web-sys and js-sys. Although the Wasm code is async, it is necessarily single-threaded, and the Wasm implementation of the url_get function awaits on non-Send futures and therefore returns a non-Send future itself. (For the C wrapper, I'm using a blocking client using block_on in a tokio runtime, and that works fine, so no further need to discuss. Yes, there's a lot of code duplication, but I don't think I'm doing it wrong.)
My "object" has dispatch functions that delegate certain operations to the communications trait. As it stands right now, I have to basically implement everything twice. I have one variant of Communications declared with #[async_trait(?Send)]
(I believe that, since these are trait objects, I still can't use the stabilized async trait native capability...) and another with #[async_trait]
, and then I have two different dispatchers. The caller has to know which one to call to avoid having anything in the function calling chain contaminate the Send-ness of the futures.
Here's the problem. Let's say I have a method in my API that does something like get a value using HTTP, do something with the value, and return a Result<String, Box<dyn Error + Sync + Send>>
. I basically have to implement it twice: once that dispatches to the Send version of the trait and once that dispatches to the local version. As a relative newcomer here, I spent several hours trying to figure out ways of encapsulating the non-Send part so I could force the non-Send parts to run on the current thread, similar to tokio::LocalSet, but inside an async function that actually runs in Web Assembly, which is already single-threaded. In the end, all my solutions were akin to putting a sheet of glass over the windshield to keep the rain off -- they just pushed the problem up or down a level. At least doing this has I believe solidified my understanding of a future being non-Send if a non-Send value is captured or crosses an await point.
I've created an example below (in the playground -- I hope that I have created this post correctly) that passes and has comments explaining the issue.
Here are my questions:
- Is my analysis about what's going on here correct? As far as I can see, there's no way to cheat on this -- I am simply forced to have both Send and non-Send versions of the trait, and at this time, I have not found a way to make the trait-variant and async-trait crates play nicely together.
- Have I overlooked a solution? Is there something perhaps Wasm specific that would allow me to somehow grab the values inside some kind of inner guaranteed single-threaded environment so that the non-Send-ness off JsFuture et al don't "contaminate" the Send-ness of my trait implementation?
- Is there a standard crate that can help with the duplication? I believe I can create my own custom proc macro for this, though before I go down that path, I might see whether there are other reasons to deviate from this pattern...
Finally, I have a general rule about trouble-shooting these kinds of things. People have a habit of going down a path, hitting a wall, backing up one step, and formulating a question from where they were when they get stuck. Sometimes there is an answer, but often, the problem is that they took a wrong turn six steps back. So a more general question is whether I've simply gone the wrong way here. Am I unwittingly trying to use rust in a way that doesn't fit with rust? Is my idea of encapsulating the environment-specific concern to a trait object the right way to go here? If this were go code, I'd write an interface, and it would "just work" because I'm not actually simultaneously accessing non-thread-safe data across multiple threads, but that's why I'm in rust -- I want the safety guarantees. Anyway, I feel like I'm eventually going to bump into this issue whether I'm using a trait object or not. No matter what, a specific action in my API will necessarily have to await on something non-Send somewhere in the course of doing its work in Wasm, but I will also want to be able to take full advantage of async with tokio in the regular environment.
I'd be eager to get any insight, even if it's just, "Go read this -- someone has already solved this (or explained how it will be solved in the future or why it's fundamentally not solvable)" or "you're going about this all wrong and making a classic newbie mistake". (Even people who have been coding for 40 years like me can make newbie mistakes in new languages!)
use async_trait::async_trait;
use std::future::Future;
use std::marker::PhantomData;
// needs_send requires a future with Send. This is like tokio::spawn.
async fn needs_send<F, T>(future: F) -> T
where
F: Future<Output = T> + Send + 'static,
F::Output: Send + 'static,
{
future.await
}
struct Answer {
a: i32,
}
// NotSend isn't Send because it contains a raw pointer.
#[derive(Default)]
struct NotSend {
_z: PhantomData<*const u8>,
}
impl NotSend {
async fn f(&self) -> Answer {
Answer { a: 1 }
}
}
// SendDemo can be sent to needs_sync, but the implementation of f()
// can't await on any futures that are not Send.
#[async_trait]
trait SendDemo {
async fn f(&self) -> Answer;
}
struct SendStruct {}
#[async_trait]
impl SendDemo for SendStruct {
async fn f(&self) -> Answer {
Answer { a: 0 }
}
}
// LocalDemo can't be sent to needs_sync because its async functions
// return futures without Send. This makes it able to await on
// non-Send futures in its implementations. We need to duplicate
// SendDemo here because `trait_variant` and `async_trait` don't work
// together.
#[async_trait(? Send)]
trait LocalDemo {
async fn f(&self) -> Answer;
}
struct LocalStruct {}
#[async_trait(? Send)]
impl LocalDemo for LocalStruct {
async fn f(&self) -> Answer {
// The fact that this function awaits on a non-Send future
// prevents the Future it returns from being Send. If we don't
// include `(?Send)` in `async_trait`, this won't compile.
let x: NotSend = Default::default();
x.f().await
}
}
// Demo must contain either a SendDemo or a LocalDemo.
enum Demo {
Send(Box<dyn SendDemo + Sync + Send>),
Local(Box<dyn LocalDemo + Sync + Send>),
}
struct Thing {
x: Demo,
}
impl Thing {
// Problem: These two functions have almost identical code. We
// want to support an API that may sometimes run in a
// single-threaded environment, like wasm, that awaits on futures
// that are not Send in its implementation, and we also want to
// support the API in normal environments that may run multiple
// copies concurrently. How can we do that without having a pair
// of dispatchers?
async fn run_send(&self) -> Answer {
let Demo::Send(x) = &self.x else {
panic!("not send");
};
// This future is Send because x is a SendDemo whose async
// functions return Futures with Send. It is possible to pass
// the future returned by run_send to something that requires
// Send, such as tokio::spawn.
x.f().await
}
async fn run_local(&self) -> Answer {
let Demo::Local(x) = &self.x else {
panic!("not local");
};
// This future is not Send because x is a LocalDemo whose
// async functions return Futures without Send. It is possible
// to pass the future returned by run_local to something that
// requires Send, such as tokio::spawn because the body of the
// function ultimately awaits on a future that is not send
// inside LocalDemo's implementation of f.
x.f().await
}
}
pub async fn f() {
let th = Thing {
x: Demo::Send(Box::new(SendStruct {})),
};
assert_eq!(th.run_send().await.a, 0);
assert_eq!(
needs_send(async move {
// We can do this because th.run_send() returns a future that is Send.
th.run_send().await
})
.await
.a,
0
);
let th = Thing {
x: Demo::Local(Box::new(LocalStruct {})),
};
// We can do this in the current thread, but we can't do it in a
// different thread because run_local()'s future is not Send.
// error: needs_send(th.run_local()).await;
assert_eq!(th.run_local().await.a, 1);
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_f() {
f().await;
}
}
Output:
running 1 test
test tests::test_f ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Errors:
Compiling playground v0.0.1 (/playground)
Finished `test` profile [unoptimized + debuginfo] target(s) in 1.38s
Running unittests src/lib.rs (target/debug/deps/playground-e8628c74cdc4bf1a)
Doc-tests playground