Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Procedural macro for boilerplate #12

Closed
marioortizmanero opened this issue Jan 29, 2022 · 10 comments
Closed

Procedural macro for boilerplate #12

marioortizmanero opened this issue Jan 29, 2022 · 10 comments
Labels
enhancement New feature or request good first issue Good for newcomers

Comments

@marioortizmanero
Copy link
Contributor

marioortizmanero commented Jan 29, 2022

It'd be pretty nice to have a procedural macro that adds the boilerplate automatically. Instead of:

use async_ffi::{FfiFuture, FutureExt};

#[no_mangle]
pub extern "C" fn work(arg: u32) -> FfiFuture<u32> {
    async move {
        let ret = do_some_io(arg).await;
        do_some_sleep(42).await;
        ret
    }
    .into_ffi()
}

It would look like:

use async_ffi::async_ffi;

#[no_mangle]
#[async_ffi]
pub async extern "C" fn work(arg: u32) -> u32 {
    let ret = do_some_io(arg).await;
    do_some_sleep(42).await;
    ret
}

Notes:

  • I would personally give it the same name as the crate, async_ffi.
  • async goes before extern "C" according to Rust's syntax. Adding it after extern "C" shouldn't work.
  • It adds the FfiFuture<> to the return type
  • It adds the async move { }.into_ffi() automatically
  • No need to import FutureExt because the macro calls it with FutureExt::into_ffi(async move {}) instead.
  • It could be customized to use LocalFfiFuture or such variants. For example #[async_ffi(local)].

Having implemented a couple of them I would say it'd be relatively simple. I don't currently have time to implement it myself, but I'm opening this issue in case there are any takers, and to start some discussion and ideas.

@oxalica oxalica added enhancement New feature or request good first issue Good for newcomers labels Jan 30, 2022
@jon-chuang
Copy link

jon-chuang commented Feb 12, 2022

No need to import FutureExt because the macro calls it with FutureExt::into_ffi(async move {}) instead.

macro does not mean you don't need to import. proc macro only transforms AST.

I guess you are saying that the macro is importing it for you.

@marioortizmanero
Copy link
Contributor Author

macro does not mean you don't need to import. proc macro only transforms AST.

I guess you are saying that the macro is importing it for you.

What I meant is that with the proc macro you can remove the use async_ffi::FutureExt; line, because you aren't using it directly. If you use the trait like future.into_ffi() manually, you obviously have to import FutureExt. But if you use async_ffi::FutureExt::into_ffi(future), you don't need to import it anywhere, and it's done internally in the macro.

@oxalica
Copy link
Owner

oxalica commented Sep 18, 2022

After a quick glance of async_trait which provides a similar wrap-async-fn-in-fox proc macro. It's way more complex than I thought.
Specifically, we need,

  1. Add lifetime restrictions on result lifetime ('static for FfiFuture, a new parameter for BorrowingFfiFuture) for generic parameters. Or you'll get a compile error.
  2. Elaborate elided lifetimes, both in &[mut] T form and '_ form, then do 1.
  3. Elaborate lifetimes for parameter-position and return-position impl Trait, then do 1.
  4. Handle drop-order stuff of unused or _-binded parameters for async {}, which is by default different than async fn. Eg, Make closure capture independent of 2018 vs 2021 edition dtolnay/async-trait#203
  5. Special cases of 1 and 4 for receivers, aka. self parameters.

Maybe it's easier to just fork-and-edit async_trait but that would be hard to maintain for us. 🤔

@yavko
Copy link
Contributor

yavko commented Jan 30, 2023

Async-std's macro for this seems dead simple, maybe it could be copied over? https://github.com/async-rs/async-attributes/blob/master/src/lib.rs

@yavko
Copy link
Contributor

yavko commented Jan 30, 2023

Hello I just wrote a macro for this, its a bit different but its pretty much the same as what you would have in async-ffi. For anyone that wants it, here. I'll make a pr for this tmrw

#[proc_macro_attribute]
pub fn async_fn(_attr: TokenStream, item: TokenStream) -> TokenStream {
    use syn::ReturnType;
    let input = syn::parse_macro_input!(item as syn::ItemFn);

    let ret_raw = &input.sig.output;
    let ret = match ret_raw {
        ReturnType::Default => quote!(-> BoxFuture<'static, ()>),
        ReturnType::Type(_, t) => {
            let ty = *t.clone();
            quote!(-> BoxFuture<'static, #ty>)
        }
    };
    let inputs = &input.sig.inputs;
    let name = &input.sig.ident;
    let body = &input.block;
    let attrs = &input.attrs;
    let vis = &input.vis;

    if name == "main" {
        return TokenStream::from(quote_spanned! { name.span() =>
            compile_error!("the main function cannot be tagged with #[async_fn]"),
        });
    }

    if input.sig.asyncness.is_none() {
        return TokenStream::from(quote_spanned! { input.span() =>
            compile_error!("the async keyword is missing from the function declaration"),
        });
    }

    let result = quote! {
        #(#attrs)*
        #vis fn #name(#inputs) #ret {
            use std::future::IntoFuture;
            let future = async move {
                #body
            };
            BoxFuture::new(future.into_future())
        }

    };

    result.into()
}

@oxalica
Copy link
Owner

oxalica commented Jan 30, 2023

Async-std's macro for this seems dead simple, maybe it could be copied over? https://github.com/async-rs/async-attributes/blob/master/src/lib.rs

That is simple because fn main has no parameters, thus it must returns a type has 'static lifetime.

Parameters which may include lifetimes and/or have Drop impls, are complicated. For example,

#[somemacro]
async fn foo(foo: Foo<'_>, bar: &Bar) -> Baz { ... }

should become,

fn foo<'a>(foo: Foo<'_>, bar: &'a Bar) -> impl Future<Output = Baz> + 'a { // <- All lifetimes, including elided ones.
    async move {
        let foo = foo; // <- `async fn`'s behavior is to keep unused parameters in the returned Future.
        let bar = bar;
        ...
    }
}

Technically the drop-order stuff doesn't really matter. Unlike async_trait, we don't have to align with behaviors of primitive async fn. An documentation should be enough. I'll re-look into and try to impl it in this week.

I'll make a pr for this tmrw

Thanks, but I think it's better to do it myself, since proc-macros should be in a new crate.

@yavko
Copy link
Contributor

yavko commented Jan 30, 2023

Ah ok

@oxalica
Copy link
Owner

oxalica commented Feb 17, 2023

The proc-macro helper is available in 0.4.1 now. See docs.

@oxalica oxalica closed this as completed Feb 17, 2023
@yavko
Copy link
Contributor

yavko commented Feb 17, 2023

Looks great thanks!

@marioortizmanero
Copy link
Contributor Author

Awesome! Great work :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request good first issue Good for newcomers
Projects
None yet
Development

No branches or pull requests

4 participants