#proc-macro #async #async-api #macro #maybe #future #api-version

macro maybe-async-cfg

A procedure macro to unify sync and async implementations depending on the features of your crate

6 releases

0.2.4 Jun 14, 2024
0.2.3 Aug 3, 2022
0.2.1 Jul 18, 2022
0.2.0 Jun 24, 2022
0.1.1 Jun 21, 2022

#82 in Procedural macros

Download history 3122/week @ 2024-06-15 4403/week @ 2024-06-22 3501/week @ 2024-06-29 3713/week @ 2024-07-06 2058/week @ 2024-07-13 3336/week @ 2024-07-20 2591/week @ 2024-07-27 4364/week @ 2024-08-03 4951/week @ 2024-08-10 4460/week @ 2024-08-17 2016/week @ 2024-08-24 3310/week @ 2024-08-31 2801/week @ 2024-09-07 3834/week @ 2024-09-14 2521/week @ 2024-09-21 2051/week @ 2024-09-28

11,699 downloads per month
Used in 27 crates (26 directly)

MIT license

145KB
2.5K SLoC

Maybe-Async-Cfg Procedure Macro

Why bother writing similar code twice for blocking and async code?

Build Status MIT licensed Latest Version maybe-async

When implementing both sync and async versions of API in a crate, most API of the two version are almost the same except for some async/await keyword.

maybe-async-cfg help unifying async and sync implementation by procedural macro.

  • Write async code with normal async, await, and let maybe_async_cfg handles those async and await when you need a blocking code.
  • Add maybe attributes and specify feature-based conditions under which sync or async code should be generated.
  • Use only_if (or remove_if) to keep code in specified version if necessary.

The maybe procedural macro can be applied to the following codes:

  • use declaration
  • trait declaration
  • trait implementation
  • function definition
  • struct and enum definition
  • modules

RECOMMENDATION: Enable resolver ver2 in your crate, which is introduced in Rust 1.51. If not, two crates in dependency with conflict version (one async and another blocking) can fail compilation.

Motivation

The async/await language feature alters the async world of rust. Comparing with the map/and_then style, now the async code really resembles sync version code.

In many crates, the async and sync version of crates shares the same API, but the minor difference that all async code must be awaited prevent the unification of async and sync code. In other words, we are forced to write an async and an sync implementation respectively.

Macros in Detail

To use maybe-async-cfg, we must know which block of codes is only used on sync implementation, and which on async. These two versions of the implementation should share the same function signatures except for async/await keywords.

Use maybe macro for code that is the same in both async and sync versions except for async/await keywords. Specify in the macro parameters the conditions (based on features) under which async and/or sync versions of the code should appear.

  • attribute macro maybe

    Offers a unified way to provide sync and async conversion on demand depending on features, enabled for your crate, with async first policy.

    [dependencies]
    maybe_async_cfg = "0.2"
    
    [features]
    use_sync = []
    use_async = []
    

    In this and all the following examples, we use two features. But you can use any conditions that are convenient for you, for example, replacing feature="use_sync" with not(feature="use_async") everywhere. Feel free, maybe-async-cfg does not analyze the conditions in any way, just substituting them as is.

    Add the maybe attribute before all the items that need to be changed in different versions of the code (sync or async).

    Want to keep async code? Specify the async parameter with the condition (based on features) when your code should be async.

    Wanna convert async code to sync? Specify the sync parameter with the condition when the sync code should be generated.

    #[maybe_async_cfg::maybe(
        idents(Foo),
        sync(feature="use_sync"),
        async(feature="use_async")
    )]
    struct Struct {
        f: Foo,
    }
    

    After conversion:

    #[cfg(feature="use_sync")]
    struct StructSync {
        f: FooSync,
    }
    #[cfg(feature="use_async")]
    struct StructAsync {
        f: FooAsync,
    }
    
  • procedural macro content

    The content macro allows you to specify common parameters for many maybe macros. Use the internal default attribute with the required parameters inside the content macro.

    maybe_async_cfg::content!{
    #![maybe_async_cfg::default(
        idents(Foo, Bar),
    )]
    
    #[maybe_async_cfg::maybe(
        sync(feature="use_sync"), 
        async(feature="use_async")
    )]
    struct Struct {
        f: Foo,
    }
    
    #[maybe_async_cfg::maybe(
        sync(feature="use_sync"), 
        async(feature="use_async")
    )]
    async fn func(b: Bar) {
        todo!()
    }
    } // content!
    

    After conversion:

    #[cfg(feature="use_sync")]
    struct StructSync {
        f: FooSync,
    }
    #[cfg(feature="use_async")]
    struct StructAsync {
        f: FooAsync,
    }
    
    #[cfg(feature="use_sync")]
    fn func_sync(b: BarSync) {
        todo!()
    }
    #[cfg(feature="use_async")]
    async fn func_async(b: BarAsync) {
        todo!()
    }
    

Doctests

When writing doctests, you can mark them as applicable only in the corresponding code version. To do this, specify only_if(VARIANT_KEY) in the doctest attributes. Then in all other versions of the code, this doctest will be replaced with an empty string.

#[maybe_async_cfg::maybe(
    idents(Foo),
    sync(feature="use_sync"),
    async(feature="use_async")
)]
/// This is a structure. 
/// ```rust, only_if(sync)
/// let s = StructSync{ f: FooSync::new() };
/// ```
/// ```rust, only_if(async)
/// let s = StructAsync{ f: FooAsync::new().await };
/// ```
struct Struct {
    f: Foo,
}

After conversion:

#[cfg(feature="use_sync")]
/// This is a structure. 
/// ```rust, only_if(sync)
/// let s = StructSync{ f: FooSync::new() };
/// ```
///
struct StructSync {
f: FooSync,
}
#[cfg(feature="use_async")]
/// This is a structure. 
///
/// ```rust, only_if(async)
/// let s = StructAsync{ f: FooAsync::new().await };
/// ```
struct StructAsync {
    f: FooAsync,
}

Examples

rust client for services

When implementing rust client for any services, like awz3. The higher level API of async and sync version is almost the same, such as creating or deleting a bucket, retrieving an object and etc.

The example service_client is a proof of concept that maybe_async_cfg can actually free us from writing almost the same code for sync and async. We can toggle between a sync AWZ3 client and async one by is_sync feature gate when we add maybe-async-cfg to dependency.

Acknowledgements

This crate is a redesigned fork of these wonderful crates:

Thanks!

License

MIT

Dependencies

~1.3–1.7MB
~41K SLoC