From 049922dc4f26b0b3123a7824a72b4c109bebd86a Mon Sep 17 00:00:00 2001 From: Taiki Endo Date: Wed, 7 Aug 2019 06:29:46 +0900 Subject: [PATCH] Rewrite join! and try_join! as procedural macros --- .travis.yml | 1 + Cargo.toml | 1 + futures-join-macro/Cargo.toml | 24 ++++ futures-join-macro/LICENSE-APACHE | 1 + futures-join-macro/LICENSE-MIT | 1 + futures-join-macro/src/lib.rs | 176 +++++++++++++++++++++++ futures-util/Cargo.toml | 2 + futures-util/src/async_await/join.rs | 133 ----------------- futures-util/src/async_await/join_mod.rs | 85 +++++++++++ futures-util/src/async_await/mod.rs | 6 +- futures/Cargo.toml | 2 +- futures/src/lib.rs | 36 ++++- futures/tests/async_await_macros.rs | 12 +- 13 files changed, 330 insertions(+), 150 deletions(-) create mode 100644 futures-join-macro/Cargo.toml create mode 120000 futures-join-macro/LICENSE-APACHE create mode 120000 futures-join-macro/LICENSE-MIT create mode 100644 futures-join-macro/src/lib.rs delete mode 100644 futures-util/src/async_await/join.rs create mode 100644 futures-util/src/async_await/join_mod.rs diff --git a/.travis.yml b/.travis.yml index bf3e675560..d8d58aead0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -140,6 +140,7 @@ matrix: - cargo check --manifest-path futures-util/Cargo.toml --features io - cargo check --manifest-path futures-util/Cargo.toml --features channel - cargo check --manifest-path futures-util/Cargo.toml --features nightly,async-await + - cargo check --manifest-path futures-util/Cargo.toml --features nightly,join-macro - cargo check --manifest-path futures-util/Cargo.toml --features nightly,select-macro - cargo check --manifest-path futures-util/Cargo.toml --features compat - cargo check --manifest-path futures-util/Cargo.toml --features io-compat diff --git a/Cargo.toml b/Cargo.toml index 430205d8ca..9dc5a75ca9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,7 @@ members = [ "futures-channel", "futures-executor", "futures-io", + "futures-join-macro", "futures-select-macro", "futures-sink", "futures-util", diff --git a/futures-join-macro/Cargo.toml b/futures-join-macro/Cargo.toml new file mode 100644 index 0000000000..2574625779 --- /dev/null +++ b/futures-join-macro/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "futures-join-macro-preview" +edition = "2018" +version = "0.3.0-alpha.17" +authors = ["Taiki Endo "] +license = "MIT OR Apache-2.0" +repository = "https://github.com/rust-lang-nursery/futures-rs" +homepage = "https://rust-lang-nursery.github.io/futures-rs" +documentation = "https://rust-lang-nursery.github.io/futures-api-docs/0.3.0-alpha.17/futures_join_macro" +description = """ +Definition of the `join!` macro and the `try_join!` macro. +""" + +[lib] +name = "futures_join_macro" +proc-macro = true + +[features] + +[dependencies] +proc-macro2 = "0.4" +proc-macro-hack = "0.5.3" +quote = "0.6" +syn = { version = "0.15.25", features = ["full"] } diff --git a/futures-join-macro/LICENSE-APACHE b/futures-join-macro/LICENSE-APACHE new file mode 120000 index 0000000000..965b606f33 --- /dev/null +++ b/futures-join-macro/LICENSE-APACHE @@ -0,0 +1 @@ +../LICENSE-APACHE \ No newline at end of file diff --git a/futures-join-macro/LICENSE-MIT b/futures-join-macro/LICENSE-MIT new file mode 120000 index 0000000000..76219eb72e --- /dev/null +++ b/futures-join-macro/LICENSE-MIT @@ -0,0 +1 @@ +../LICENSE-MIT \ No newline at end of file diff --git a/futures-join-macro/src/lib.rs b/futures-join-macro/src/lib.rs new file mode 100644 index 0000000000..a17b1bd8be --- /dev/null +++ b/futures-join-macro/src/lib.rs @@ -0,0 +1,176 @@ +//! The futures-rs `join! macro implementation. + +#![recursion_limit = "128"] +#![warn(rust_2018_idioms, unreachable_pub)] +// It cannot be included in the published code because this lints have false positives in the minimum required version. +#![cfg_attr(test, warn(single_use_lifetimes))] +#![warn(clippy::all)] + +extern crate proc_macro; + +use proc_macro::TokenStream; +use proc_macro2::{Span, TokenStream as TokenStream2}; +use proc_macro_hack::proc_macro_hack; +use quote::quote; +use syn::parse::{Parse, ParseStream}; +use syn::{parenthesized, parse_quote, Expr, Ident, Token}; + +mod kw { + syn::custom_keyword!(futures_crate_path); +} + +#[derive(Default)] +struct Join { + futures_crate_path: Option, + fut_exprs: Vec, +} + +impl Parse for Join { + fn parse(input: ParseStream<'_>) -> syn::Result { + let mut join = Join::default(); + + // When `futures_crate_path(::path::to::futures::lib)` is provided, + // it sets the path through which futures library functions will be + // accessed. + if input.peek(kw::futures_crate_path) { + input.parse::()?; + let content; + parenthesized!(content in input); + join.futures_crate_path = Some(content.parse()?); + } + + while !input.is_empty() { + join.fut_exprs.push(input.parse::()?); + + if !input.is_empty() { + input.parse::()?; + } + } + + Ok(join) + } +} + +fn bind_futures( + futures_crate: &syn::Path, + fut_exprs: Vec, + span: Span, +) -> (Vec, Vec) { + let mut future_let_bindings = Vec::with_capacity(fut_exprs.len()); + let future_names: Vec<_> = fut_exprs + .into_iter() + .enumerate() + .map(|(i, expr)| { + let name = Ident::new(&format!("_fut{}", i), span); + future_let_bindings.push(quote! { + // Move future into a local so that it is pinned in one place and + // is no longer accessible by the end user. + let mut #name = #futures_crate::future::maybe_done(#expr); + }); + name + }) + .collect(); + + (future_let_bindings, future_names) +} + +/// The `join!` macro. +#[proc_macro_hack] +pub fn join(input: TokenStream) -> TokenStream { + let parsed = syn::parse_macro_input!(input as Join); + + let futures_crate = parsed + .futures_crate_path + .unwrap_or_else(|| parse_quote!(::futures_util)); + + // should be def_site, but that's unstable + let span = Span::call_site(); + + let (future_let_bindings, future_names) = bind_futures(&futures_crate, parsed.fut_exprs, span); + + let poll_futures = future_names.iter().map(|fut| { + quote! { + __all_done &= #futures_crate::core_reexport::future::Future::poll( + unsafe { #futures_crate::core_reexport::pin::Pin::new_unchecked(&mut #fut) }, __cx).is_ready(); + } + }); + let take_outputs = future_names.iter().map(|fut| { + quote! { + unsafe { #futures_crate::core_reexport::pin::Pin::new_unchecked(&mut #fut) }.take_output().unwrap(), + } + }); + + TokenStream::from(quote! { { + #( #future_let_bindings )* + + #futures_crate::future::poll_fn(move |__cx: &mut #futures_crate::task::Context<'_>| { + let mut __all_done = true; + #( #poll_futures )* + if __all_done { + #futures_crate::core_reexport::task::Poll::Ready(( + #( #take_outputs )* + )) + } else { + #futures_crate::core_reexport::task::Poll::Pending + } + }).await + } }) +} + +/// The `try_join!` macro. +#[proc_macro_hack] +pub fn try_join(input: TokenStream) -> TokenStream { + let parsed = syn::parse_macro_input!(input as Join); + + let futures_crate = parsed + .futures_crate_path + .unwrap_or_else(|| parse_quote!(::futures_util)); + + // should be def_site, but that's unstable + let span = Span::call_site(); + + let (future_let_bindings, future_names) = bind_futures(&futures_crate, parsed.fut_exprs, span); + + let poll_futures = future_names.iter().map(|fut| { + quote! { + if #futures_crate::core_reexport::future::Future::poll( + unsafe { #futures_crate::core_reexport::pin::Pin::new_unchecked(&mut #fut) }, __cx).is_pending() + { + __all_done = false; + } else if unsafe { #futures_crate::core_reexport::pin::Pin::new_unchecked(&mut #fut) }.output_mut().unwrap().is_err() { + // `.err().unwrap()` rather than `.unwrap_err()` so that we don't introduce + // a `T: Debug` bound. + return #futures_crate::core_reexport::task::Poll::Ready( + #futures_crate::core_reexport::result::Result::Err( + unsafe { #futures_crate::core_reexport::pin::Pin::new_unchecked(&mut #fut) }.take_output().unwrap().err().unwrap() + ) + ); + } + } + }); + let take_outputs = future_names.iter().map(|fut| { + quote! { + // `.ok().unwrap()` rather than `.unwrap()` so that we don't introduce + // an `E: Debug` bound. + unsafe { #futures_crate::core_reexport::pin::Pin::new_unchecked(&mut #fut) }.take_output().unwrap().ok().unwrap(), + } + }); + + TokenStream::from(quote! { { + #( #future_let_bindings )* + + #futures_crate::future::poll_fn(move |__cx: &mut #futures_crate::task::Context<'_>| { + let mut __all_done = true; + #( #poll_futures )* + if __all_done { + #futures_crate::core_reexport::task::Poll::Ready( + #futures_crate::core_reexport::result::Result::Ok(( + #( #take_outputs )* + )) + ) + } else { + #futures_crate::core_reexport::task::Poll::Pending + } + }).await + } }) +} diff --git a/futures-util/Cargo.toml b/futures-util/Cargo.toml index a28bc5c718..59618db47b 100644 --- a/futures-util/Cargo.toml +++ b/futures-util/Cargo.toml @@ -27,6 +27,7 @@ cfg-target-has-atomic = ["futures-core-preview/cfg-target-has-atomic"] sink = ["futures-sink-preview"] io = ["std", "futures-io-preview", "memchr"] channel = ["std", "futures-channel-preview"] +join-macro = ["async-await", "futures-join-macro-preview", "proc-macro-hack", "proc-macro-nested"] select-macro = ["async-await", "futures-select-macro-preview", "proc-macro-hack", "proc-macro-nested", "rand"] [dependencies] @@ -34,6 +35,7 @@ futures-core-preview = { path = "../futures-core", version = "=0.3.0-alpha.17", futures-channel-preview = { path = "../futures-channel", version = "=0.3.0-alpha.17", default-features = false, features = ["std"], optional = true } futures-io-preview = { path = "../futures-io", version = "=0.3.0-alpha.17", default-features = false, features = ["std"], optional = true } futures-sink-preview = { path = "../futures-sink", version = "=0.3.0-alpha.17", default-features = false, optional = true } +futures-join-macro-preview = { path = "../futures-join-macro", version = "=0.3.0-alpha.17", default-features = false, optional = true } futures-select-macro-preview = { path = "../futures-select-macro", version = "=0.3.0-alpha.17", default-features = false, optional = true } proc-macro-hack = { version = "0.5", optional = true } proc-macro-nested = { version = "0.1.2", optional = true } diff --git a/futures-util/src/async_await/join.rs b/futures-util/src/async_await/join.rs deleted file mode 100644 index 83ed0d7219..0000000000 --- a/futures-util/src/async_await/join.rs +++ /dev/null @@ -1,133 +0,0 @@ -//! The `join` macro. - -/// Polls multiple futures simultaneously, returning a tuple -/// of all results once complete. -/// -/// While `join!(a, b)` is similar to `(a.await, b.await)`, -/// `join!` polls both futures concurrently and therefore is more efficent. -/// -/// This macro is only usable inside of async functions, closures, and blocks. -/// It is also gated behind the `async-await` feature of this library, which is -/// _not_ activated by default. -/// -/// # Examples -/// -/// ``` -/// #![feature(async_await)] -/// # futures::executor::block_on(async { -/// use futures::{join, future}; -/// -/// let a = future::ready(1); -/// let b = future::ready(2); -/// -/// assert_eq!(join!(a, b), (1, 2)); -/// # }); -/// ``` -#[macro_export] -macro_rules! join { - ($($fut:ident),* $(,)?) => { { - $( - // Move future into a local so that it is pinned in one place and - // is no longer accessible by the end user. - let mut $fut = $crate::future::maybe_done($fut); - )* - $crate::future::poll_fn(move |cx| { - let mut all_done = true; - $( - all_done &= $crate::core_reexport::future::Future::poll( - unsafe { $crate::core_reexport::pin::Pin::new_unchecked(&mut $fut) }, cx).is_ready(); - )* - if all_done { - $crate::core_reexport::task::Poll::Ready(($( - unsafe { $crate::core_reexport::pin::Pin::new_unchecked(&mut $fut) }.take_output().unwrap(), - )*)) - } else { - $crate::core_reexport::task::Poll::Pending - } - }).await - } } -} - -/// Polls multiple futures simultaneously, resolving to a [`Result`] containing -/// either a tuple of the successful outputs or an error. -/// -/// `try_join!` is similar to [`join!`], but completes immediately if any of -/// the futures return an error. -/// -/// This macro is only usable inside of async functions, closures, and blocks. -/// It is also gated behind the `async-await` feature of this library, which is -/// _not_ activated by default. -/// -/// # Examples -/// -/// When used on multiple futures that return `Ok`, `try_join!` will return -/// `Ok` of a tuple of the values: -/// -/// ``` -/// #![feature(async_await)] -/// # futures::executor::block_on(async { -/// use futures::{try_join, future}; -/// -/// let a = future::ready(Ok::(1)); -/// let b = future::ready(Ok::(2)); -/// -/// assert_eq!(try_join!(a, b), Ok((1, 2))); -/// # }); -/// ``` -/// -/// If one of the futures resolves to an error, `try_join!` will return -/// that error: -/// -/// ``` -/// #![feature(async_await)] -/// # futures::executor::block_on(async { -/// use futures::{try_join, future}; -/// -/// let a = future::ready(Ok::(1)); -/// let b = future::ready(Err::(2)); -/// -/// assert_eq!(try_join!(a, b), Err(2)); -/// # }); -/// ``` -#[macro_export] -macro_rules! try_join { - ($($fut:ident),* $(,)?) => { { - $( - // Move future into a local so that it is pinned in one place and - // is no longer accessible by the end user. - let mut $fut = $crate::future::maybe_done($fut); - )* - - let res: $crate::core_reexport::result::Result<_, _> = $crate::future::poll_fn(move |cx| { - let mut all_done = true; - $( - if $crate::core_reexport::future::Future::poll( - unsafe { $crate::core_reexport::pin::Pin::new_unchecked(&mut $fut) }, cx).is_pending() - { - all_done = false; - } else if unsafe { $crate::core_reexport::pin::Pin::new_unchecked(&mut $fut) }.output_mut().unwrap().is_err() { - // `.err().unwrap()` rather than `.unwrap_err()` so that we don't introduce - // a `T: Debug` bound. - return $crate::core_reexport::task::Poll::Ready( - $crate::core_reexport::result::Result::Err( - unsafe { $crate::core_reexport::pin::Pin::new_unchecked(&mut $fut) }.take_output().unwrap().err().unwrap() - ) - ); - } - )* - if all_done { - $crate::core_reexport::task::Poll::Ready( - $crate::core_reexport::result::Result::Ok(($( - // `.ok().unwrap()` rather than `.unwrap()` so that we don't introduce - // an `E: Debug` bound. - unsafe { $crate::core_reexport::pin::Pin::new_unchecked(&mut $fut) }.take_output().unwrap().ok().unwrap(), - )*)) - ) - } else { - $crate::core_reexport::task::Poll::Pending - } - }).await; - - res - } } -} diff --git a/futures-util/src/async_await/join_mod.rs b/futures-util/src/async_await/join_mod.rs new file mode 100644 index 0000000000..61cb058590 --- /dev/null +++ b/futures-util/src/async_await/join_mod.rs @@ -0,0 +1,85 @@ +//! The `join` macro. + +use proc_macro_hack::proc_macro_hack; + +#[doc(hidden)] +#[macro_export] +macro_rules! document_join_macro { + ($join:item $try_join:item) => { + /// Polls multiple futures simultaneously, returning a tuple + /// of all results once complete. + /// + /// While `join!(a, b)` is similar to `(a.await, b.await)`, + /// `join!` polls both futures concurrently and therefore is more efficent. + /// + /// This macro is only usable inside of async functions, closures, and blocks. + /// It is also gated behind the `async-await` feature of this library, which is + /// _not_ activated by default. + /// + /// # Examples + /// + /// ``` + /// #![feature(async_await)] + /// # futures::executor::block_on(async { + /// use futures::{join, future}; + /// + /// let a = future::ready(1); + /// let b = future::ready(2); + /// + /// assert_eq!(join!(a, b), (1, 2)); + /// # }); + /// ``` + $join + + /// Polls multiple futures simultaneously, resolving to a [`Result`] containing + /// either a tuple of the successful outputs or an error. + /// + /// `try_join!` is similar to [`join!`], but completes immediately if any of + /// the futures return an error. + /// + /// This macro is only usable inside of async functions, closures, and blocks. + /// It is also gated behind the `async-await` feature of this library, which is + /// _not_ activated by default. + /// + /// # Examples + /// + /// When used on multiple futures that return `Ok`, `try_join!` will return + /// `Ok` of a tuple of the values: + /// + /// ``` + /// #![feature(async_await)] + /// # futures::executor::block_on(async { + /// use futures::{try_join, future}; + /// + /// let a = future::ready(Ok::(1)); + /// let b = future::ready(Ok::(2)); + /// + /// assert_eq!(try_join!(a, b), Ok((1, 2))); + /// # }); + /// ``` + /// + /// If one of the futures resolves to an error, `try_join!` will return + /// that error: + /// + /// ``` + /// #![feature(async_await)] + /// # futures::executor::block_on(async { + /// use futures::{try_join, future}; + /// + /// let a = future::ready(Ok::(1)); + /// let b = future::ready(Err::(2)); + /// + /// assert_eq!(try_join!(a, b), Err(2)); + /// # }); + /// ``` + $try_join + } +} + +document_join_macro! { + #[proc_macro_hack(support_nested)] + pub use futures_join_macro::join; + + #[proc_macro_hack(support_nested)] + pub use futures_join_macro::try_join; +} diff --git a/futures-util/src/async_await/mod.rs b/futures-util/src/async_await/mod.rs index 80cd5b6348..ef5052f757 100644 --- a/futures-util/src/async_await/mod.rs +++ b/futures-util/src/async_await/mod.rs @@ -20,8 +20,10 @@ mod pending; pub use self::pending::*; // Primary export is a macro -#[macro_use] -mod join; +#[cfg(feature = "join-macro")] +mod join_mod; +#[cfg(feature = "join-macro")] +pub use self::join_mod::*; // Primary export is a macro #[cfg(feature = "select-macro")] diff --git a/futures/Cargo.toml b/futures/Cargo.toml index b598bfb6fc..f23fbc7099 100644 --- a/futures/Cargo.toml +++ b/futures/Cargo.toml @@ -40,7 +40,7 @@ default = ["std"] std = ["alloc", "futures-core-preview/std", "futures-executor-preview/std", "futures-io-preview/std", "futures-sink-preview/std", "futures-util-preview/std", "futures-util-preview/io", "futures-util-preview/channel"] alloc = ["futures-core-preview/alloc", "futures-sink-preview/alloc", "futures-channel-preview/alloc", "futures-util-preview/alloc"] nightly = ["futures-core-preview/nightly", "futures-channel-preview/nightly", "futures-util-preview/nightly"] -async-await = ["futures-util-preview/async-await", "futures-util-preview/select-macro"] +async-await = ["futures-util-preview/async-await", "futures-util-preview/join-macro", "futures-util-preview/select-macro"] compat = ["std", "futures-util-preview/compat"] io-compat = ["compat", "futures-util-preview/io-compat"] cfg-target-has-atomic = ["futures-core-preview/cfg-target-has-atomic", "futures-channel-preview/cfg-target-has-atomic", "futures-util-preview/cfg-target-has-atomic"] diff --git a/futures/src/lib.rs b/futures/src/lib.rs index 862253aaca..e6da5b58ab 100644 --- a/futures/src/lib.rs +++ b/futures/src/lib.rs @@ -68,10 +68,7 @@ compile_error!("The `cfg-target-has-atomic` feature requires the `nightly` featu pub use futures_core::ready; // Readiness propagation pub use futures_util::pin_mut; #[cfg(feature = "async-await")] -pub use futures_util::{ - // Async-await - join, try_join, pending, poll, -}; +pub use futures_util::{pending, poll}; // Async-await #[cfg_attr( feature = "cfg-target-has-atomic", @@ -516,7 +513,7 @@ pub mod never { pub use futures_core::never::Never; } -// `select!` re-export -------------------------------------- +// proc-macro re-export -------------------------------------- #[cfg(feature = "async-await")] #[doc(hidden)] @@ -524,16 +521,41 @@ pub use futures_util::rand_reexport; #[cfg(feature = "async-await")] #[doc(hidden)] -pub mod inner_select { +pub mod inner_macro { + pub use futures_util::join; + pub use futures_util::try_join; pub use futures_util::select; } +#[cfg(feature = "async-await")] +futures_util::document_join_macro! { + #[macro_export] + macro_rules! join { // replace `::futures_util` with `::futures` as the crate path + ($($tokens:tt)*) => { + $crate::inner_macro::join! { + futures_crate_path ( ::futures ) + $( $tokens )* + } + } + } + + #[macro_export] + macro_rules! try_join { // replace `::futures_util` with `::futures` as the crate path + ($($tokens:tt)*) => { + $crate::inner_macro::try_join! { + futures_crate_path ( ::futures ) + $( $tokens )* + } + } + } +} + #[cfg(feature = "async-await")] futures_util::document_select_macro! { #[macro_export] macro_rules! select { // replace `::futures_util` with `::futures` as the crate path ($($tokens:tt)*) => { - $crate::inner_select::select! { + $crate::inner_macro::select! { futures_crate_path ( ::futures ) $( $tokens )* } diff --git a/futures/tests/async_await_macros.rs b/futures/tests/async_await_macros.rs index 1604644f2a..e21a382c8a 100644 --- a/futures/tests/async_await_macros.rs +++ b/futures/tests/async_await_macros.rs @@ -194,21 +194,19 @@ fn try_join_size() { assert_eq!(::std::mem::size_of_val(&fut), 32); } - #[test] fn join_doesnt_require_unpin() { let _ = async { - let x = async {}; - let y = async {}; - join!(x, y) + join!(async {}, async {}) }; } #[test] fn try_join_doesnt_require_unpin() { let _ = async { - let x = async { Ok::<(), ()>(()) }; - let y = async { Ok::<(), ()>(()) }; - try_join!(x, y) + try_join!( + async { Ok::<(), ()>(()) }, + async { Ok::<(), ()>(()) }, + ) }; }