diff options
| author | 魏曹先生 <1992414357@qq.com> | 2026-06-27 17:46:29 +0800 |
|---|---|---|
| committer | 魏曹先生 <1992414357@qq.com> | 2026-06-27 17:46:29 +0800 |
| commit | 4f7bd4f6fa5d27cfe703c54aa029a321f40d19fb (patch) | |
| tree | 97c31ddbb905d8f0e70418b0970ee6420aeac611 | |
| parent | 6cadc8d39adebbc263e8576c389b5ef491f10ace (diff) | |
feat(macros): add async mutable resource injection for `#[chain]`
Support `&mut T` resource parameters in async chain functions by using
an extract-store pattern that avoids holding mutable borrows across
await
points. Remove the previous compile-time rejection of this combination.
| -rw-r--r-- | CHANGELOG.md | 16 | ||||
| -rw-r--r-- | examples/example-async-support/src/main.rs | 3 | ||||
| -rw-r--r-- | mingling/src/example_docs.rs | 3 | ||||
| -rw-r--r-- | mingling_core/src/asset/global_resource.rs | 38 | ||||
| -rw-r--r-- | mingling_macros/src/chain.rs | 29 | ||||
| -rw-r--r-- | mingling_macros/src/lib.rs | 18 | ||||
| -rw-r--r-- | mingling_macros/src/res_injection.rs | 73 |
7 files changed, 149 insertions, 31 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 60b8f2c..7cc4e6f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -243,6 +243,22 @@ fn comp_my_entry(ctx: &ShellContext, res: &mut ResA) -> Suggest { For mutable resources (`&mut T`), both macros use `Program::modify_res` (with constraint `Return: Default`) instead of `#[chain]`'s dedicated `__modify_res_and_return_route` (with constraint `Return: Into<ChainProcess>`), because the return types of help/completion are `()` and `Suggest` respectively. +12. **\[macros\]** Added async mutable resource injection support for `#[chain]`. Previously, async chain functions could only use `&T` (immutable) resource injection; `&mut T` was rejected with a compile-time error. Now, async chain functions support `&mut T` resource injection by using an extract‑store pattern: each mutable resource is cloned out of the global store before the body executes (via `__extract_res_mut`), bound as `&mut` within a scoped block, and written back after the block completes (via `__store_res`). This avoids holding a mutable borrow across `.await` points while still providing a natural `&mut T` syntax. + +```rust +use mingling::macros::{chain, pack, gen_program}; + +pack!(MyOutput = String); + +#[chain] +async fn greet(prev: HelloEntry, ec: &mut ResExitCode) -> Next { + let name = prev.first().cloned().unwrap_or_else(|| "World".to_string()); + ec.exit_code = 42; + some_async_fn(&name).await; + MyOutput::new(name) +} +``` + #### **BREAKING CHANGES** (API CHANGES): --- diff --git a/examples/example-async-support/src/main.rs b/examples/example-async-support/src/main.rs index 920b3dd..847bfea 100644 --- a/examples/example-async-support/src/main.rs +++ b/examples/example-async-support/src/main.rs @@ -9,8 +9,7 @@ //! //! However, you will lose some capabilities: //! -//! 1. `&mut` resource injection is not available in async chain functions -//! 2. The program will not be able to use panic unwind functionality +//! 1. The program will not be able to use panic unwind functionality //! //! Run: //! ```bash diff --git a/mingling/src/example_docs.rs b/mingling/src/example_docs.rs index c492a11..5208077 100644 --- a/mingling/src/example_docs.rs +++ b/mingling/src/example_docs.rs @@ -128,8 +128,7 @@ pub mod example_argument_parse {} /// /// However, you will lose some capabilities: /// -/// 1. `&mut` resource injection is not available in async chain functions -/// 2. The program will not be able to use panic unwind functionality +/// 1. The program will not be able to use panic unwind functionality /// /// Run: /// ```bash diff --git a/mingling_core/src/asset/global_resource.rs b/mingling_core/src/asset/global_resource.rs index 83a779d..651655a 100644 --- a/mingling_core/src/asset/global_resource.rs +++ b/mingling_core/src/asset/global_resource.rs @@ -74,6 +74,44 @@ where } } + /// Internal syntax for the `&mut MyResource` syntax of async #[chain], do not use directly. + /// + /// Extracts a mutable resource from the global store (clone-out), returning an + /// owned value. The caller must call [`__store_res`] to write back modifications. + #[doc(hidden)] + #[must_use] + pub fn __extract_res_mut<Res: 'static + Default + ResourceMarker + Send + Sync>( + &self, + ) -> Res { + let Ok(mut guard) = self.resources.lock() else { + return Res::res_default(); + }; + if let Some(arc_res) = guard + .get_mut(&TypeId::of::<Res>()) + .and_then(|a| a.downcast_mut::<Arc<Res>>()) + { + match Arc::try_unwrap(std::mem::take(arc_res)) { + Ok(val) => val, + Err(arc) => (*arc).res_clone(), + } + } else { + Res::res_default() + } + } + + /// Internal syntax for the `&mut MyResource` syntax of async #[chain], do not use directly. + /// + /// Stores a modified resource value back into the global store. + #[doc(hidden)] + pub fn __store_res<Res: 'static + Send + Sync + ResourceMarker>( + &self, + val: Res, + ) { + if let Ok(mut guard) = self.resources.lock() { + guard.insert(TypeId::of::<Res>(), Box::new(Arc::new(val))); + } + } + /// Get an resources by type, returning `Res` if present #[must_use] pub fn res<Res: 'static + Send + Sync>(&self) -> Option<GlobalResource<Res>> { diff --git a/mingling_macros/src/chain.rs b/mingling_macros/src/chain.rs index 6191d39..5f72422 100644 --- a/mingling_macros/src/chain.rs +++ b/mingling_macros/src/chain.rs @@ -2,7 +2,7 @@ use crate::res_injection::{ ResourceInjection, extract_args_info, generate_immut_resource_bindings, - wrap_body_with_mut_resources, + wrap_body_with_mut_resources, wrap_body_with_mut_resources_async, }; use proc_macro::TokenStream; use quote::{ToTokens, quote}; @@ -93,7 +93,11 @@ fn generate_proc_fn( fn_body_stmts }; - let wrapped_body = wrap_body_with_mut_resources(body_stmts, &mut_resources, program_type); + let wrapped_body = if is_async_fn && !mut_resources.is_empty() { + wrap_body_with_mut_resources_async(body_stmts, &mut_resources, program_type) + } else { + wrap_body_with_mut_resources(body_stmts, &mut_resources, program_type) + }; // When the function returns `()`, wrap the result with ResultEmpty let call_or_wrapped = if is_unit_return { @@ -252,19 +256,6 @@ fn reject_async(sig: &Signature) -> Result<(), proc_macro2::TokenStream> { Ok(()) } -/// Ensures no `&mut` resource injection is used in async functions. -#[cfg(feature = "async")] -fn reject_mut_in_async(resources: &[ResourceInjection]) -> Result<(), proc_macro2::TokenStream> { - if let Some(mut_res) = resources.iter().find(|r| r.is_mut) { - return Err(syn::Error::new( - mut_res.var_name.span(), - "Cannot use `&mut` resource injection in async chain function.", - ) - .to_compile_error()); - } - Ok(()) -} - pub fn chain_attr(attr: TokenStream, item: TokenStream) -> TokenStream { // Reject non-empty attribute arguments; #[chain] must be bare if !attr.is_empty() { @@ -304,14 +295,6 @@ pub fn chain_attr(attr: TokenStream, item: TokenStream) -> TokenStream { Err(e) => return e.to_compile_error().into(), }; - // Reject `&mut` in async chains - #[cfg(feature = "async")] - if is_async_fn { - if let Err(err) = reject_mut_in_async(&resources) { - return err.into(); - } - } - // Prepare building blocks let sig = &input_fn.sig; let inputs = &sig.inputs; diff --git a/mingling_macros/src/lib.rs b/mingling_macros/src/lib.rs index 21273ba..1b0cbb5 100644 --- a/mingling_macros/src/lib.rs +++ b/mingling_macros/src/lib.rs @@ -858,8 +858,6 @@ pub fn r_println(input: TokenStream) -> TokenStream { /// - The first parameter (previous type) must be taken **by move**, not by reference. /// - Resource injection parameters **must** be references (`&T` or `&mut T`), /// owned values are not allowed. -/// - When the `async` feature is enabled, `&mut T` cannot be used in async -/// chain functions (only `&T` is supported for async). /// /// # Sync Example /// @@ -924,6 +922,22 @@ pub fn r_println(input: TokenStream) -> TokenStream { /// } /// ``` /// +/// # Async Example with Mutable Resource Injection +/// +/// ```rust,ignore +/// use mingling::macros::{chain, pack, gen_program}; +/// +/// pack!(MyOutput = String); +/// +/// #[chain] +/// async fn greet(prev: HelloEntry, ec: &mut ResExitCode) -> Next { +/// let name = prev.first().cloned().unwrap_or_else(|| "World".to_string()); +/// ec.exit_code = 42; +/// some_async_fn(&name).await; +/// MyOutput::new(name) +/// } +/// ``` +/// /// # Requirements /// /// - The function must have at least **one** parameter (the previous type in the chain). diff --git a/mingling_macros/src/res_injection.rs b/mingling_macros/src/res_injection.rs index 4f0be88..f2952cc 100644 --- a/mingling_macros/src/res_injection.rs +++ b/mingling_macros/src/res_injection.rs @@ -158,9 +158,19 @@ pub(crate) fn generate_immut_resource_bindings<'a>( .collect() } +/// Generates a unique binding name for a mutable resource variable. +fn mut_res_binding_name(var_name: &Ident) -> Ident { + syn::Ident::new( + &format!("__{}_binding", var_name), + var_name.span(), + ) +} + /// Wraps the function body in nested `__modify_res_and_return_route` closures for -/// each mutable resource parameter. The innermost closure gets the original body, -/// and each mutable parameter wraps outward from last to first. +/// each mutable resource parameter (sync version). +/// +/// The innermost closure gets the original body, and each mutable parameter wraps +/// outward from last to first. pub(crate) fn wrap_body_with_mut_resources( fn_body_stmts: &[syn::Stmt], mut_resources: &[&ResourceInjection], @@ -182,3 +192,62 @@ pub(crate) fn wrap_body_with_mut_resources( wrapped } + +/// Generates code for mutable resource injection in async `#[chain]` functions. +/// +/// Instead of wrapping in a closure (which causes lifetime issues with async), +/// this generates a three-part pattern: +/// +/// 1. **Extract** each mutable resource from the global store into an owned local. +/// 2. **Body block** — the user's body with `&mut` references scoped to a block. +/// 3. **Store back** each modified resource after the body block completes. +/// +/// This avoids holding a `&mut` reference across `.await` points — the borrow +/// ends when the body block closes. +pub(crate) fn wrap_body_with_mut_resources_async( + fn_body_stmts: &[syn::Stmt], + mut_resources: &[&ResourceInjection], + program_type: &proc_macro2::TokenStream, +) -> proc_macro2::TokenStream { + // 1. Generate extract statements and body-block `let` bindings + let mut extract_stmts = Vec::new(); + let mut body_lets = Vec::new(); + + for res in mut_resources { + let var_name = &res.var_name; + let inner_type = &res.inner_type; + let binding_name = mut_res_binding_name(var_name); + + extract_stmts.push(quote! { + let mut #binding_name = ::mingling::this::<#program_type>() + .__extract_res_mut::<#inner_type>(); + }); + + body_lets.push(quote! { + let #var_name: &mut #inner_type = &mut #binding_name; + }); + } + + // 2. Store-back statements (reverse order is fine, resources are independent) + let mut store_stmts = Vec::new(); + for res in mut_resources { + let var_name = &res.var_name; + let inner_type = &res.inner_type; + let binding_name = mut_res_binding_name(var_name); + + store_stmts.push(quote! { + ::mingling::this::<#program_type>() + .__store_res::<#inner_type>(#binding_name); + }); + } + + quote! { + #(#extract_stmts)* + let __chain_result = { + #(#body_lets)* + #(#fn_body_stmts)* + }; + #(#store_stmts)* + __chain_result + } +} |
