Write code that can be both async and synchronous without duplicating it.
This macro set is intended as an interim solution for the problem space that will eventually be covered by the Rust Keyword Generic Initiative.
As of this writing (September 2023), the official status of that project is listed as still in the "Experimental" stage, so deployment in the language is still likely many months away if not longer.
So ... what do we do now if we need async-generic code? We build our own, using proc macros. And that's exactly what this crate is.
I'll happily mark this crate as deprecated when keyword generics land officially in the language. Until then, hopefully it solves some problems for you, too!
IMPORTANT: This crate is quite simple, so I expect there will be few releases beyond the 1.0.0 release. If you encounter issues, pelase do file them here; I use this crate routinely in other projects and will be watching, even if I don't update it regularly.
The async_generic
crate introduces a single proc macro also named async_generic
which can be applied as an attribute to any function (either inside a struct or not).
The macro outputs two versions of the function, one synchronous and one that's async. The functions are identical to each other, except as follows:
- When writing the async flavor of the function, the macro inserts the
async
modifier for you and renames the function (to avoid a name collision) by adding an_async
suffix to the existing function name. - The attribute macro may contain an
async_signature
argument. If that exists, the async function's argument parameters are replaced. (See example below.) - You can write
if _sync
orif _async
blocks inside this block. The contents of these blocks will only appear in the corresponding sync or async flavors of the functions. You may specify anelse
clause, which will only appear in the opposite flavor of the function. You may not combine_sync
or_async
with any other expression. (These aren't really variables in the function scope, and they will cause "undefined identifier" errors if you try that.)
A simple example:
use async_generic::async_generic;
#[async_generic]
fn do_stuff() -> String {
// Also: async fn do_stuff_async() -> String {...}
if _async {
my_async_stuff().await
} else {
"not async".to_owned()
}
}
async fn my_async_stuff() -> String {
"async".to_owned()
}
#[async_std::main]
async fn main() {
println!("sync => {}", do_stuff());
println!("async => {}", do_stuff_async().await);
}
An example with different function arguments in the sync and async flavors:
use async_generic::async_generic;
#[async_generic(async_signature(thing: &AsyncThing))]
fn do_stuff(thing: &SyncThing) -> String {
// Also: async fn do_stuff_async(thing: &AsyncThing) -> String {...}
if _async {
thing.do_stuff().await
} else {
thing.do_stuff()
}
}
struct SyncThing {}
impl SyncThing {
fn do_stuff(&self) -> String {
"sync".to_owned()
}
}
struct AsyncThing {}
impl AsyncThing {
async fn do_stuff(&self) -> String {
"async".to_owned()
}
}
#[async_std::main]
async fn main() {
let st = SyncThing {};
let at = AsyncThing {};
println!("sync => {}", do_stuff(&st));
println!("async => {}", do_stuff_async(&at).await);
}
This crate is loosely derived from the excellent work of the maybe-async
crate, but is intended to solve a subtly different problem.
Use maybe-async
when you know at compile-time whether each crate in your dependency tree will be used in an async or synchronous fashion. In that model, you can't have both at once.
Use async-generic
when you wish to have both async and synchronous versions of an API at the same time and want to reuse most of the implementation.