From 114df1d3fbd07c66d15a6f099c12c3b5d5a8a2e6 Mon Sep 17 00:00:00 2001 From: tison Date: Thu, 11 Jun 2026 09:24:28 +0800 Subject: [PATCH 1/5] refactor: break down fastrace-macros Signed-off-by: tison --- fastrace-macro/src/args.rs | 135 ++++++++++ fastrace-macro/src/impls.rs | 381 ++++++++++++++++++++++++++++ fastrace-macro/src/lib.rs | 489 +++--------------------------------- 3 files changed, 549 insertions(+), 456 deletions(-) create mode 100644 fastrace-macro/src/args.rs create mode 100644 fastrace-macro/src/impls.rs diff --git a/fastrace-macro/src/args.rs b/fastrace-macro/src/args.rs new file mode 100644 index 0000000..64b75f3 --- /dev/null +++ b/fastrace-macro/src/args.rs @@ -0,0 +1,135 @@ +// Copyright 2024 FastLabs Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::collections::HashSet; +use std::hash::Hash; +use std::hash::Hasher; + +use proc_macro2::Span; +use syn::Error; +use syn::LitBool; +use syn::LitStr; +use syn::Path; +use syn::Result; +use syn::Token; +use syn::braced; +use syn::parse::Parse; +use syn::parse::ParseStream; +use syn::parse_quote; +use syn::spanned::Spanned; + +#[cfg_attr(not(feature = "enable"), allow(dead_code))] +pub struct Property { + pub key: LitStr, + pub value: LitStr, +} + +impl PartialEq for Property { + fn eq(&self, other: &Self) -> bool { + PartialEq::eq(&self.key, &other.key) + } +} + +impl Eq for Property {} + +impl Hash for Property { + fn hash(&self, state: &mut H) { + self.key.hash(state); + } +} + +impl Parse for Property { + fn parse(input: ParseStream) -> Result { + let key: LitStr = input.parse()?; + input.parse::()?; + let value: LitStr = input.parse()?; + Ok(Property { key, value }) + } +} + +#[cfg_attr(not(feature = "enable"), allow(dead_code))] +pub struct Args { + pub name: Option, + pub short_name: bool, + pub enter_on_poll: bool, + pub properties: HashSet, + pub crate_path: Path, +} + +impl Parse for Args { + fn parse(input: ParseStream) -> Result { + let mut name = None; + let mut short_name = false; + let mut enter_on_poll = false; + let mut properties = HashSet::new(); + let mut crate_path = parse_quote!(::fastrace); + let mut seen = HashSet::new(); + + while !input.is_empty() { + let key: Path = input.parse()?; + let key = key + .get_ident() + .ok_or_else(|| Error::new(key.span(), "expected identifier"))?; + if seen.contains(key) { + return Err(Error::new(key.span(), "duplicate argument")); + } + seen.insert(key.clone()); + input.parse::()?; + match key.to_string().as_str() { + "name" => { + let parsed_name: LitStr = input.parse()?; + name = Some(parsed_name); + } + "short_name" => { + let parsed_short_name: LitBool = input.parse()?; + short_name = parsed_short_name.value; + } + "enter_on_poll" => { + let parsed_enter_on_poll: LitBool = input.parse()?; + enter_on_poll = parsed_enter_on_poll.value; + } + "properties" => { + let content; + let _brace_token = braced!(content in input); + let property_list = content.parse_terminated(Property::parse, Token![,])?; + for property in property_list { + if properties.contains(&property) { + return Err(Error::new( + Span::call_site(), + format!("duplicate property key: {}", property.key.value()), + )); + } + properties.insert(property); + } + } + "crate" => { + let parsed_crate_path: Path = input.parse()?; + crate_path = parsed_crate_path; + } + _ => return Err(Error::new(Span::call_site(), "unexpected identifier")), + } + if !input.is_empty() { + let _ = input.parse::(); + } + } + + Ok(Args { + name, + short_name, + enter_on_poll, + properties, + crate_path, + }) + } +} diff --git a/fastrace-macro/src/impls.rs b/fastrace-macro/src/impls.rs new file mode 100644 index 0000000..3c095dd --- /dev/null +++ b/fastrace-macro/src/impls.rs @@ -0,0 +1,381 @@ +// Copyright 2024 FastLabs Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use proc_macro2::Ident; +use proc_macro2::Span; +use quote::ToTokens; +use quote::quote; +use quote::quote_spanned; +use syn::Block; +use syn::Error; +use syn::Expr; +use syn::ExprAsync; +use syn::ExprCall; +use syn::Generics; +use syn::Item; +use syn::ItemFn; +use syn::LitStr; +use syn::Path; +use syn::Result; +use syn::ReturnType; +use syn::Signature; +use syn::Stmt; +use syn::Token; +use syn::Type; +use syn::TypeInfer; +use syn::parse_quote; +use syn::punctuated::Punctuated; +use syn::spanned::Spanned; +use syn::visit_mut; +use syn::visit_mut::VisitMut; + +use crate::args::Args; + +pub(crate) fn gen_trace(args: Args, input: ItemFn) -> Result { + let func_name = &input.sig.ident; + + // Check for async_trait-like patterns in the block, and instrument + // the future instead of the wrapper. + let func_body = if let Some(internal_fun) = + get_async_trait_info(&input.block, input.sig.asyncness.is_some()) + { + match internal_fun.kind { + // async-trait <= 0.1.43 + AsyncTraitKind::Function => { + unimplemented!( + "Please upgrade the crate `async-trait` to a version higher than 0.1.44" + ) + } + // async-trait >= 0.1.44 + AsyncTraitKind::Async(async_expr) => { + let instrumented_block = + gen_block(func_name, &async_expr.block, true, false, &args, None)?; + let async_attrs = &async_expr.attrs; + quote! { + Box::pin(#(#async_attrs) * #instrumented_block) + } + } + } + } else { + let output_ty = match input.sig.output { + ReturnType::Type(_, ref ty) => (**ty).clone(), + ReturnType::Default => parse_quote! { () }, + }; + gen_block( + func_name, + &input.block, + input.sig.asyncness.is_some(), + input.sig.asyncness.is_some(), + &args, + Some(output_ty), + )? + }; + + let ItemFn { + attrs, vis, sig, .. + } = input; + + let Signature { + output: return_type, + inputs: params, + unsafety, + constness, + abi, + ident, + asyncness, + generics: + Generics { + params: gen_params, + where_clause, + .. + }, + .. + } = sig; + + let fn_span = ident.span(); + Ok(quote_spanned!(fn_span=> + #(#attrs) * + #vis #constness #unsafety #asyncness #abi fn #ident<#gen_params>(#params) #return_type + #where_clause + { + #func_body + } + )) +} + +fn gen_name(func_name: &Ident, args: &Args) -> Result { + let crate_path = &args.crate_path; + match &args.name { + Some(name) if name.value().is_empty() => { + Err(Error::new(Span::call_site(), "`name` can not be empty")) + } + Some(_) if args.short_name => Err(Error::new( + Span::call_site(), + "`name` and `short_name` can not be used together", + )), + Some(name) => Ok(name.into_token_stream()), + None if args.short_name => { + Ok(LitStr::new(&func_name.to_string(), func_name.span()).into_token_stream()) + } + None => Ok(quote!(#crate_path::func_path!())), + } +} + +fn gen_properties(args: &Args) -> Result { + if args.properties.is_empty() { + return Ok(quote!()); + } + + if args.enter_on_poll { + return Err(Error::new( + Span::call_site(), + "`enter_on_poll` can not be used with `properties`", + )); + } + + let properties = args.properties.iter().map(|p| { + let k = &p.key; + let v = &p.value; + quote!( + (std::borrow::Cow::from(#k), match format_args!(#v) { + __f => if let Some(__s) = __f.as_str() { + std::borrow::Cow::from(__s) + } else { + std::borrow::Cow::from(std::string::ToString::to_string(&__f)) + } + }) + ) + }); + let properties = Punctuated::<_, Token![,]>::from_iter(properties); + Ok(quote!( + .with_properties(|| [ #properties ]) + )) +} + +/// Instrument a block +fn gen_block( + func_name: &Ident, + block: &Block, + async_context: bool, + async_keyword: bool, + args: &Args, + output_ty: Option, +) -> Result { + let name = gen_name(func_name, args)?; + let properties = gen_properties(args)?; + let crate_path = &args.crate_path; + let output_ty_hint = output_ty + .map(erase_impl_trait) + .unwrap_or_else(|| parse_quote! { _ }); + + // Generate the instrumented function body. + // If the function is an `async fn`, this will wrap it in an async block. + // Otherwise, this will enter the span and then perform the rest of the body. + if async_context { + let block = if args.enter_on_poll { + quote!( + #crate_path::future::FutureExt::enter_on_poll( + async move { #block }, + #name + ) + ) + } else { + quote!( + { + let __span__ = #crate_path::Span::enter_with_local_parent( #name ) #properties; + #crate_path::future::FutureExt::in_span( + async move { + let __ret__: #output_ty_hint = #block; + #[allow(unreachable_code)] + __ret__ + }, + __span__, + ) + } + ) + }; + + if async_keyword { + Ok(quote!( + #block.await + )) + } else { + Ok(block) + } + } else { + if args.enter_on_poll { + Err(Error::new( + Span::call_site(), + "`enter_on_poll` can not be applied on non-async function", + )) + } else { + Ok(quote!( + let __guard__ = #crate_path::local::LocalSpan::enter_with_local_parent( #name ) #properties; + #block + )) + } + } +} + +enum AsyncTraitKind<'a> { + // old construction. Contains the function + Function, + // new construction. Contains a reference to the async block + Async(&'a ExprAsync), +} + +struct AsyncTraitInfo<'a> { + // statement that must be patched + _source_stmt: &'a Stmt, + kind: AsyncTraitKind<'a>, +} + +// Get the AST of the inner function we need to hook, if it was generated +// by async-trait. +// When we are given a function annotated by async-trait, that function +// is only a placeholder that returns a pinned future containing the +// user logic, and it is that pinned future that needs to be instrumented. +// Were we to instrument its parent, we would only collect information +// regarding the allocation of that future, and not its own span of execution. +// Depending on the version of async-trait, we inspect the block of the function +// to find if it matches the pattern +// `async fn foo<...>(...) {...}; Box::pin(foo<...>(...))` (<=0.1.43), or if +// it matches `Box::pin(async move { ... }) (>=0.1.44). We the return the +// statement that must be instrumented, along with some other information. +// 'gen_body' will then be able to use that information to instrument the +// proper function/future. +// (this follows the approach suggested in +// https://github.com/dtolnay/async-trait/issues/45#issuecomment-571245673) +fn get_async_trait_info(block: &Block, block_is_async: bool) -> Option> { + // are we in an async context? If yes, this isn't an async_trait-like pattern + if block_is_async { + return None; + } + + // list of async functions declared inside the block + let inside_funs = block.stmts.iter().filter_map(|stmt| { + if let Stmt::Item(Item::Fn(fun)) = &stmt { + // If the function is async, this is a candidate + if fun.sig.asyncness.is_some() { + return Some((stmt, fun)); + } + } + None + }); + + // last expression of the block (it determines the return value + // of the block, so that if we are working on a function whose + // `trait` or `impl` declaration is annotated by async_trait, + // this is quite likely the point where the future is pinned) + let (last_expr_stmt, last_expr) = block.stmts.iter().rev().find_map(|stmt| { + if let Stmt::Expr(expr, None) = stmt { + Some((stmt, expr)) + } else { + None + } + })?; + + // is the last expression a function call? + let (outside_func, outside_args) = match last_expr { + Expr::Call(ExprCall { func, args, .. }) => (func, args), + _ => return None, + }; + + // is it a call to `Box::pin()`? + let path = match outside_func.as_ref() { + Expr::Path(path) => &path.path, + _ => return None, + }; + if !path_to_string(path).ends_with("Box::pin") { + return None; + } + + // Does the call take an argument? If it doesn't, + // it's not going to compile anyway, but that's no reason + // to (try to) perform an out-of-bounds access + if outside_args.is_empty() { + return None; + } + + // Is the argument to Box::pin an async block that + // captures its arguments? + if let Expr::Async(async_expr) = &outside_args[0] { + // check that the move 'keyword' is present + async_expr.capture?; + + return Some(AsyncTraitInfo { + _source_stmt: last_expr_stmt, + kind: AsyncTraitKind::Async(async_expr), + }); + } + + // Is the argument to Box::pin a function call itself? + let func = match &outside_args[0] { + Expr::Call(ExprCall { func, .. }) => func, + _ => return None, + }; + + // "stringify" the path of the function called + let func_name = match **func { + Expr::Path(ref func_path) => path_to_string(&func_path.path), + _ => return None, + }; + + // Was that function defined inside the current block? + // If so, retrieve the statement where it was declared and the function itself + let (stmt_func_declaration, _) = inside_funs + .into_iter() + .find(|(_, fun)| fun.sig.ident == func_name)?; + + Some(AsyncTraitInfo { + _source_stmt: stmt_func_declaration, + kind: AsyncTraitKind::Function, + }) +} + +// Return a path as a String +fn path_to_string(path: &Path) -> String { + use std::fmt::Write; + // some heuristic to prevent too many allocations + let mut res = String::with_capacity(path.segments.len() * 5); + for i in 0..path.segments.len() { + write!(res, "{}", path.segments[i].ident).expect("writing to a String should never fail"); + if i < path.segments.len() - 1 { + res.push_str("::"); + } + } + res +} + +/// Replaces any `impl Trait` with `_` so it can be used as the type in +/// a `let` statement's LHS. +struct ImplTraitEraser; + +impl VisitMut for ImplTraitEraser { + fn visit_type_mut(&mut self, t: &mut Type) { + if let Type::ImplTrait(..) = t { + *t = TypeInfer { + underscore_token: Token![_](t.span()), + } + .into(); + } else { + visit_mut::visit_type_mut(self, t); + } + } +} + +fn erase_impl_trait(mut ty: Type) -> Type { + ImplTraitEraser.visit_type_mut(&mut ty); + ty +} diff --git a/fastrace-macro/src/lib.rs b/fastrace-macro/src/lib.rs index c0e43ac..dfa9ddc 100644 --- a/fastrace-macro/src/lib.rs +++ b/fastrace-macro/src/lib.rs @@ -1,24 +1,26 @@ -// Copyright 2020 TiKV Project Authors. Licensed under Apache-2.0. - -//! An attribute macro designed to eliminate boilerplate code for [`fastrace`](https://crates.io/crates/fastrace). +// Copyright 2024 FastLabs Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! An attribute macro designed to eliminate boilerplate code for [`fastrace`]. +//! +//! [`fastrace`]: https://crates.io/crates/fastrace #![recursion_limit = "256"] -#![cfg_attr(not(feature = "enable"), allow(dead_code))] -#![cfg_attr(not(feature = "enable"), allow(unreachable_code))] - -use std::collections::HashSet; - -use proc_macro2::Span; -use quote::ToTokens; -use quote::quote; -use quote::quote_spanned; -use syn::parse::Parse; -use syn::parse::ParseStream; -use syn::punctuated::Punctuated; -use syn::spanned::Spanned; -use syn::*; -use crate::visit_mut::VisitMut; +mod args; +#[cfg(feature = "enable")] +mod impls; /// An attribute macro designed to eliminate boilerplate code. /// @@ -118,448 +120,23 @@ pub fn trace( ) -> proc_macro::TokenStream { #[cfg(not(feature = "enable"))] { - parse_macro_input!(args as Args); - return item; - } + use syn::parse_macro_input; - let args = parse_macro_input!(args as Args); - let input = parse_macro_input!(item as ItemFn); - match gen_trace(args, input) { - Ok(tokens) => tokens.into(), - Err(err) => err.to_compile_error().into(), + // simply check the attributes + parse_macro_input!(args as args::Args); + item } -} -fn gen_trace(args: Args, input: ItemFn) -> Result { - let func_name = &input.sig.ident; - - // Check for async_trait-like patterns in the block, and instrument - // the future instead of the wrapper. - let func_body = if let Some(internal_fun) = - get_async_trait_info(&input.block, input.sig.asyncness.is_some()) + #[cfg(feature = "enable")] { - match internal_fun.kind { - // async-trait <= 0.1.43 - AsyncTraitKind::Function => { - unimplemented!( - "Please upgrade the crate `async-trait` to a version higher than 0.1.44" - ) - } - // async-trait >= 0.1.44 - AsyncTraitKind::Async(async_expr) => { - let instrumented_block = - gen_block(func_name, &async_expr.block, true, false, &args, None)?; - let async_attrs = &async_expr.attrs; - quote! { - Box::pin(#(#async_attrs) * #instrumented_block) - } - } - } - } else { - let output_ty = match input.sig.output { - ReturnType::Type(_, ref ty) => (**ty).clone(), - ReturnType::Default => parse_quote! { () }, - }; - gen_block( - func_name, - &input.block, - input.sig.asyncness.is_some(), - input.sig.asyncness.is_some(), - &args, - Some(output_ty), - )? - }; - - let ItemFn { - attrs, vis, sig, .. - } = input; - - let Signature { - output: return_type, - inputs: params, - unsafety, - constness, - abi, - ident, - asyncness, - generics: - Generics { - params: gen_params, - where_clause, - .. - }, - .. - } = sig; - - let fn_span = ident.span(); - Ok(quote_spanned!(fn_span=> - #(#attrs) * - #vis #constness #unsafety #asyncness #abi fn #ident<#gen_params>(#params) #return_type - #where_clause - { - #func_body - } - )) -} - -struct Args { - name: Option, - short_name: bool, - enter_on_poll: bool, - properties: Vec<(LitStr, LitStr)>, - crate_path: Path, -} - -impl Default for Args { - fn default() -> Self { - Self { - name: None, - short_name: false, - enter_on_poll: false, - properties: Vec::new(), - crate_path: parse_quote!(::fastrace), - } - } -} - -struct Property { - key: LitStr, - value: LitStr, -} - -impl Parse for Property { - fn parse(input: ParseStream) -> Result { - let key: LitStr = input.parse()?; - input.parse::()?; - let value: LitStr = input.parse()?; - Ok(Property { key, value }) - } -} - -impl Parse for Args { - fn parse(input: ParseStream) -> Result { - let mut name = None; - let mut short_name = false; - let mut enter_on_poll = false; - let mut properties = Vec::new(); - let mut crate_path = parse_quote!(::fastrace); - let mut seen = HashSet::new(); - - while !input.is_empty() { - let key: Path = input.parse()?; - let key = key - .get_ident() - .ok_or_else(|| Error::new(key.span(), "expected identifier"))?; - if seen.contains(key) { - return Err(Error::new(key.span(), "duplicate argument")); - } - seen.insert(key.clone()); - input.parse::()?; - match key.to_string().as_str() { - "name" => { - let parsed_name: LitStr = input.parse()?; - name = Some(parsed_name); - } - "short_name" => { - let parsed_short_name: LitBool = input.parse()?; - short_name = parsed_short_name.value; - } - "enter_on_poll" => { - let parsed_enter_on_poll: LitBool = input.parse()?; - enter_on_poll = parsed_enter_on_poll.value; - } - "properties" => { - let content; - let _brace_token = braced!(content in input); - let property_list = content.parse_terminated(Property::parse, Token![,])?; - for property in property_list { - if properties.iter().any(|(k, _)| k == &property.key) { - return Err(Error::new(Span::call_site(), "duplicate property key")); - } - properties.push((property.key, property.value)); - } - } - "crate" => { - let parsed_crate_path: Path = input.parse()?; - crate_path = parsed_crate_path; - } - _ => return Err(Error::new(Span::call_site(), "unexpected identifier")), - } - if !input.is_empty() { - let _ = input.parse::(); - } - } - - Ok(Args { - name, - short_name, - enter_on_poll, - properties, - crate_path, - }) - } -} - -fn gen_name(func_name: &Ident, args: &Args) -> Result { - let crate_path = &args.crate_path; - match &args.name { - Some(name) if name.value().is_empty() => { - Err(Error::new(Span::call_site(), "`name` can not be empty")) - } - Some(_) if args.short_name => Err(Error::new( - Span::call_site(), - "`name` and `short_name` can not be used together", - )), - Some(name) => Ok(name.into_token_stream()), - None if args.short_name => { - Ok(LitStr::new(&func_name.to_string(), func_name.span()).into_token_stream()) - } - None => Ok(quote!(#crate_path::func_path!())), - } -} - -fn gen_properties(args: &Args) -> Result { - if args.properties.is_empty() { - return Ok(quote!()); - } - - if args.enter_on_poll { - return Err(Error::new( - Span::call_site(), - "`enter_on_poll` can not be used with `properties`", - )); - } - - let properties = args.properties.iter().map(|(k, v)| { - quote!( - (std::borrow::Cow::from(#k), match format_args!(#v) { - __f => if let Some(__s) = __f.as_str() { - std::borrow::Cow::from(__s) - } else { - std::borrow::Cow::from(std::string::ToString::to_string(&__f)) - } - }) - ) - }); - let properties = Punctuated::<_, Token![,]>::from_iter(properties); - Ok(quote!( - .with_properties(|| [ #properties ]) - )) -} - -/// Instrument a block -fn gen_block( - func_name: &Ident, - block: &Block, - async_context: bool, - async_keyword: bool, - args: &Args, - output_ty: Option, -) -> Result { - let name = gen_name(func_name, args)?; - let properties = gen_properties(args)?; - let crate_path = &args.crate_path; - let output_ty_hint = output_ty - .map(erase_impl_trait) - .unwrap_or_else(|| parse_quote! { _ }); - - // Generate the instrumented function body. - // If the function is an `async fn`, this will wrap it in an async block. - // Otherwise, this will enter the span and then perform the rest of the body. - if async_context { - let block = if args.enter_on_poll { - quote!( - #crate_path::future::FutureExt::enter_on_poll( - async move { #block }, - #name - ) - ) - } else { - quote!( - { - let __span__ = #crate_path::Span::enter_with_local_parent( #name ) #properties; - #crate_path::future::FutureExt::in_span( - async move { - let __ret__: #output_ty_hint = #block; - #[allow(unreachable_code)] - __ret__ - }, - __span__, - ) - } - ) - }; - - if async_keyword { - Ok(quote!( - #block.await - )) - } else { - Ok(block) - } - } else { - if args.enter_on_poll { - Err(Error::new( - Span::call_site(), - "`enter_on_poll` can not be applied on non-async function", - )) - } else { - Ok(quote!( - let __guard__ = #crate_path::local::LocalSpan::enter_with_local_parent( #name ) #properties; - #block - )) - } - } -} - -enum AsyncTraitKind<'a> { - // old construction. Contains the function - Function, - // new construction. Contains a reference to the async block - Async(&'a ExprAsync), -} - -struct AsyncTraitInfo<'a> { - // statement that must be patched - _source_stmt: &'a Stmt, - kind: AsyncTraitKind<'a>, -} - -// Get the AST of the inner function we need to hook, if it was generated -// by async-trait. -// When we are given a function annotated by async-trait, that function -// is only a placeholder that returns a pinned future containing the -// user logic, and it is that pinned future that needs to be instrumented. -// Were we to instrument its parent, we would only collect information -// regarding the allocation of that future, and not its own span of execution. -// Depending on the version of async-trait, we inspect the block of the function -// to find if it matches the pattern -// `async fn foo<...>(...) {...}; Box::pin(foo<...>(...))` (<=0.1.43), or if -// it matches `Box::pin(async move { ... }) (>=0.1.44). We the return the -// statement that must be instrumented, along with some other information. -// 'gen_body' will then be able to use that information to instrument the -// proper function/future. -// (this follows the approach suggested in -// https://github.com/dtolnay/async-trait/issues/45#issuecomment-571245673) -fn get_async_trait_info(block: &Block, block_is_async: bool) -> Option> { - // are we in an async context? If yes, this isn't an async_trait-like pattern - if block_is_async { - return None; - } - - // list of async functions declared inside the block - let inside_funs = block.stmts.iter().filter_map(|stmt| { - if let Stmt::Item(Item::Fn(fun)) = &stmt { - // If the function is async, this is a candidate - if fun.sig.asyncness.is_some() { - return Some((stmt, fun)); - } + use syn::ItemFn; + use syn::parse_macro_input; + + let args = parse_macro_input!(args as args::Args); + let input = parse_macro_input!(item as ItemFn); + match impls::gen_trace(args, input) { + Ok(tokens) => tokens.into(), + Err(err) => err.to_compile_error().into(), } - None - }); - - // last expression of the block (it determines the return value - // of the block, so that if we are working on a function whose - // `trait` or `impl` declaration is annotated by async_trait, - // this is quite likely the point where the future is pinned) - let (last_expr_stmt, last_expr) = block.stmts.iter().rev().find_map(|stmt| { - if let Stmt::Expr(expr, None) = stmt { - Some((stmt, expr)) - } else { - None - } - })?; - - // is the last expression a function call? - let (outside_func, outside_args) = match last_expr { - Expr::Call(ExprCall { func, args, .. }) => (func, args), - _ => return None, - }; - - // is it a call to `Box::pin()`? - let path = match outside_func.as_ref() { - Expr::Path(path) => &path.path, - _ => return None, - }; - if !path_to_string(path).ends_with("Box::pin") { - return None; } - - // Does the call take an argument? If it doesn't, - // it's not going to compile anyway, but that's no reason - // to (try to) perform an out-of-bounds access - if outside_args.is_empty() { - return None; - } - - // Is the argument to Box::pin an async block that - // captures its arguments? - if let Expr::Async(async_expr) = &outside_args[0] { - // check that the move 'keyword' is present - async_expr.capture?; - - return Some(AsyncTraitInfo { - _source_stmt: last_expr_stmt, - kind: AsyncTraitKind::Async(async_expr), - }); - } - - // Is the argument to Box::pin a function call itself? - let func = match &outside_args[0] { - Expr::Call(ExprCall { func, .. }) => func, - _ => return None, - }; - - // "stringify" the path of the function called - let func_name = match **func { - Expr::Path(ref func_path) => path_to_string(&func_path.path), - _ => return None, - }; - - // Was that function defined inside the current block? - // If so, retrieve the statement where it was declared and the function itself - let (stmt_func_declaration, _) = inside_funs - .into_iter() - .find(|(_, fun)| fun.sig.ident == func_name)?; - - Some(AsyncTraitInfo { - _source_stmt: stmt_func_declaration, - kind: AsyncTraitKind::Function, - }) -} - -// Return a path as a String -fn path_to_string(path: &Path) -> String { - use std::fmt::Write; - // some heuristic to prevent too many allocations - let mut res = String::with_capacity(path.segments.len() * 5); - for i in 0..path.segments.len() { - write!(res, "{}", path.segments[i].ident).expect("writing to a String should never fail"); - if i < path.segments.len() - 1 { - res.push_str("::"); - } - } - res -} - -/// Replaces any `impl Trait` with `_` so it can be used as the type in -/// a `let` statement's LHS. -struct ImplTraitEraser; - -impl VisitMut for ImplTraitEraser { - fn visit_type_mut(&mut self, t: &mut Type) { - if let Type::ImplTrait(..) = t { - *t = TypeInfer { - underscore_token: Token![_](t.span()), - } - .into(); - } else { - visit_mut::visit_type_mut(self, t); - } - } -} - -fn erase_impl_trait(mut ty: Type) -> Type { - ImplTraitEraser.visit_type_mut(&mut ty); - ty } From 017dafb39a56fc06bd7199974325894bc21a674f Mon Sep 17 00:00:00 2001 From: tison Date: Thu, 11 Jun 2026 14:11:09 +0800 Subject: [PATCH 2/5] trybuild tests Signed-off-by: tison --- .../tests/ui/err/has-duplicated-properties.rs | 6 +++++ .../ui/err/has-duplicated-properties.stderr | 7 ++++++ .../tests/ui/err/has-unknown-argument.rs | 6 +++++ .../tests/ui/err/has-unknown-argument.stderr | 7 ++++++ .../macros/tests/ui/err/name-is-not-string.rs | 6 +++++ .../tests/ui/err/name-is-not-string.stderr | 5 ++++ .../tests/ui/err/properties-missing-braces.rs | 6 +++++ .../ui/err/properties-missing-braces.stderr | 5 ++++ .../ui/err/properties-value-non-string.rs | 6 +++++ .../ui/err/properties-value-non-string.stderr | 5 ++++ .../tests/ui/err/short-name-is-not-bool.rs | 6 +++++ .../ui/err/short-name-is-not-bool.stderr | 5 ++++ .../tests/ui/ok/has-generic-signatures.rs | 23 +++++++++++++++++++ tests/macros/tests/ui/ok/has-methods.rs | 23 +++++++++++++++++++ .../macros/tests/ui/ok/has-sync-properties.rs | 15 ++++++++++++ tests/macros/tests/ui/ok/has-unsafe-extern.rs | 10 ++++++++ 16 files changed, 141 insertions(+) create mode 100644 tests/macros/tests/ui/err/has-duplicated-properties.rs create mode 100644 tests/macros/tests/ui/err/has-duplicated-properties.stderr create mode 100644 tests/macros/tests/ui/err/has-unknown-argument.rs create mode 100644 tests/macros/tests/ui/err/has-unknown-argument.stderr create mode 100644 tests/macros/tests/ui/err/name-is-not-string.rs create mode 100644 tests/macros/tests/ui/err/name-is-not-string.stderr create mode 100644 tests/macros/tests/ui/err/properties-missing-braces.rs create mode 100644 tests/macros/tests/ui/err/properties-missing-braces.stderr create mode 100644 tests/macros/tests/ui/err/properties-value-non-string.rs create mode 100644 tests/macros/tests/ui/err/properties-value-non-string.stderr create mode 100644 tests/macros/tests/ui/err/short-name-is-not-bool.rs create mode 100644 tests/macros/tests/ui/err/short-name-is-not-bool.stderr create mode 100644 tests/macros/tests/ui/ok/has-generic-signatures.rs create mode 100644 tests/macros/tests/ui/ok/has-methods.rs create mode 100644 tests/macros/tests/ui/ok/has-sync-properties.rs create mode 100644 tests/macros/tests/ui/ok/has-unsafe-extern.rs diff --git a/tests/macros/tests/ui/err/has-duplicated-properties.rs b/tests/macros/tests/ui/err/has-duplicated-properties.rs new file mode 100644 index 0000000..d85ef24 --- /dev/null +++ b/tests/macros/tests/ui/err/has-duplicated-properties.rs @@ -0,0 +1,6 @@ +use fastrace::trace; + +#[trace(properties = { "key": "first", "key": "second" })] +fn f() {} + +fn main() {} diff --git a/tests/macros/tests/ui/err/has-duplicated-properties.stderr b/tests/macros/tests/ui/err/has-duplicated-properties.stderr new file mode 100644 index 0000000..d372321 --- /dev/null +++ b/tests/macros/tests/ui/err/has-duplicated-properties.stderr @@ -0,0 +1,7 @@ +error: duplicate property key: key + --> tests/ui/err/has-duplicated-properties.rs:3:1 + | +3 | #[trace(properties = { "key": "first", "key": "second" })] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = note: this error originates in the attribute macro `trace` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/tests/macros/tests/ui/err/has-unknown-argument.rs b/tests/macros/tests/ui/err/has-unknown-argument.rs new file mode 100644 index 0000000..66cb660 --- /dev/null +++ b/tests/macros/tests/ui/err/has-unknown-argument.rs @@ -0,0 +1,6 @@ +use fastrace::trace; + +#[trace(unknown = true)] +fn f() {} + +fn main() {} diff --git a/tests/macros/tests/ui/err/has-unknown-argument.stderr b/tests/macros/tests/ui/err/has-unknown-argument.stderr new file mode 100644 index 0000000..83186bc --- /dev/null +++ b/tests/macros/tests/ui/err/has-unknown-argument.stderr @@ -0,0 +1,7 @@ +error: unexpected identifier + --> tests/ui/err/has-unknown-argument.rs:3:1 + | +3 | #[trace(unknown = true)] + | ^^^^^^^^^^^^^^^^^^^^^^^^ + | + = note: this error originates in the attribute macro `trace` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/tests/macros/tests/ui/err/name-is-not-string.rs b/tests/macros/tests/ui/err/name-is-not-string.rs new file mode 100644 index 0000000..ced1135 --- /dev/null +++ b/tests/macros/tests/ui/err/name-is-not-string.rs @@ -0,0 +1,6 @@ +use fastrace::trace; + +#[trace(name = true)] +fn f() {} + +fn main() {} diff --git a/tests/macros/tests/ui/err/name-is-not-string.stderr b/tests/macros/tests/ui/err/name-is-not-string.stderr new file mode 100644 index 0000000..7a8f632 --- /dev/null +++ b/tests/macros/tests/ui/err/name-is-not-string.stderr @@ -0,0 +1,5 @@ +error: expected string literal + --> tests/ui/err/name-is-not-string.rs:3:16 + | +3 | #[trace(name = true)] + | ^^^^ diff --git a/tests/macros/tests/ui/err/properties-missing-braces.rs b/tests/macros/tests/ui/err/properties-missing-braces.rs new file mode 100644 index 0000000..68af1aa --- /dev/null +++ b/tests/macros/tests/ui/err/properties-missing-braces.rs @@ -0,0 +1,6 @@ +use fastrace::trace; + +#[trace(properties = "key")] +fn f() {} + +fn main() {} diff --git a/tests/macros/tests/ui/err/properties-missing-braces.stderr b/tests/macros/tests/ui/err/properties-missing-braces.stderr new file mode 100644 index 0000000..aa14069 --- /dev/null +++ b/tests/macros/tests/ui/err/properties-missing-braces.stderr @@ -0,0 +1,5 @@ +error: expected curly braces + --> tests/ui/err/properties-missing-braces.rs:3:22 + | +3 | #[trace(properties = "key")] + | ^^^^^ diff --git a/tests/macros/tests/ui/err/properties-value-non-string.rs b/tests/macros/tests/ui/err/properties-value-non-string.rs new file mode 100644 index 0000000..715a228 --- /dev/null +++ b/tests/macros/tests/ui/err/properties-value-non-string.rs @@ -0,0 +1,6 @@ +use fastrace::trace; + +#[trace(properties = { "key": true })] +fn f() {} + +fn main() {} diff --git a/tests/macros/tests/ui/err/properties-value-non-string.stderr b/tests/macros/tests/ui/err/properties-value-non-string.stderr new file mode 100644 index 0000000..7c88b1e --- /dev/null +++ b/tests/macros/tests/ui/err/properties-value-non-string.stderr @@ -0,0 +1,5 @@ +error: expected string literal + --> tests/ui/err/properties-value-non-string.rs:3:31 + | +3 | #[trace(properties = { "key": true })] + | ^^^^ diff --git a/tests/macros/tests/ui/err/short-name-is-not-bool.rs b/tests/macros/tests/ui/err/short-name-is-not-bool.rs new file mode 100644 index 0000000..5946dea --- /dev/null +++ b/tests/macros/tests/ui/err/short-name-is-not-bool.rs @@ -0,0 +1,6 @@ +use fastrace::trace; + +#[trace(short_name = "true")] +fn f() {} + +fn main() {} diff --git a/tests/macros/tests/ui/err/short-name-is-not-bool.stderr b/tests/macros/tests/ui/err/short-name-is-not-bool.stderr new file mode 100644 index 0000000..a8618b5 --- /dev/null +++ b/tests/macros/tests/ui/err/short-name-is-not-bool.stderr @@ -0,0 +1,5 @@ +error: expected boolean literal + --> tests/ui/err/short-name-is-not-bool.rs:3:22 + | +3 | #[trace(short_name = "true")] + | ^^^^^^ diff --git a/tests/macros/tests/ui/ok/has-generic-signatures.rs b/tests/macros/tests/ui/ok/has-generic-signatures.rs new file mode 100644 index 0000000..ce14d6d --- /dev/null +++ b/tests/macros/tests/ui/ok/has-generic-signatures.rs @@ -0,0 +1,23 @@ +use fastrace::trace; + +#[trace(short_name = true)] +fn sync_generic<'a, T, E>(value: &'a T) -> Result<&'a T, E> +where + T: ?Sized, +{ + Ok(value) +} + +#[trace(short_name = true)] +async fn async_generic(value: T) -> impl AsRef +where + T: Into, +{ + value.into() +} + +#[tokio::main] +async fn main() { + let _ = sync_generic::<_, ()>("value"); + let _ = async_generic("value").await; +} diff --git a/tests/macros/tests/ui/ok/has-methods.rs b/tests/macros/tests/ui/ok/has-methods.rs new file mode 100644 index 0000000..d40156f --- /dev/null +++ b/tests/macros/tests/ui/ok/has-methods.rs @@ -0,0 +1,23 @@ +use fastrace::trace; + +struct Worker(String); + +impl Worker { + #[trace(short_name = true)] + fn sync_method(&self) -> &str { + &self.0 + } + + #[trace(short_name = true)] + async fn async_method(&mut self, suffix: &str) -> String { + self.0.push_str(suffix); + self.0.clone() + } +} + +#[tokio::main] +async fn main() { + let mut worker = Worker(String::from("fast")); + let _ = worker.sync_method(); + let _ = worker.async_method("race").await; +} diff --git a/tests/macros/tests/ui/ok/has-sync-properties.rs b/tests/macros/tests/ui/ok/has-sync-properties.rs new file mode 100644 index 0000000..70eeab6 --- /dev/null +++ b/tests/macros/tests/ui/ok/has-sync-properties.rs @@ -0,0 +1,15 @@ +use fastrace::trace; + +#[derive(Debug)] +struct Input { + value: u64, +} + +#[trace(short_name = true, properties = { "literal": "value", "input": "{input:?}", "escaped": "{{input}}" })] +fn f(input: &Input) -> u64 { + input.value +} + +fn main() { + let _ = f(&Input { value: 7 }); +} diff --git a/tests/macros/tests/ui/ok/has-unsafe-extern.rs b/tests/macros/tests/ui/ok/has-unsafe-extern.rs new file mode 100644 index 0000000..9596c27 --- /dev/null +++ b/tests/macros/tests/ui/ok/has-unsafe-extern.rs @@ -0,0 +1,10 @@ +use fastrace::trace; + +#[trace(short_name = true)] +pub(crate) unsafe extern "C" fn f(value: u32) -> u32 { + value +} + +fn main() { + let _ = unsafe { f(7) }; +} From 189849d475e177952968123bd564dc99c921aa26 Mon Sep 17 00:00:00 2001 From: tison Date: Thu, 11 Jun 2026 14:50:33 +0800 Subject: [PATCH 3/5] fine tune Signed-off-by: tison --- fastrace-macro/src/impls.rs | 174 ++++++++++++++++++++---------------- fastrace-macro/src/lib.rs | 4 + 2 files changed, 100 insertions(+), 78 deletions(-) diff --git a/fastrace-macro/src/impls.rs b/fastrace-macro/src/impls.rs index 3c095dd..67a1f52 100644 --- a/fastrace-macro/src/impls.rs +++ b/fastrace-macro/src/impls.rs @@ -14,6 +14,7 @@ use proc_macro2::Ident; use proc_macro2::Span; +use proc_macro2::TokenStream; use quote::ToTokens; use quote::quote; use quote::quote_spanned; @@ -42,7 +43,7 @@ use syn::visit_mut::VisitMut; use crate::args::Args; -pub(crate) fn gen_trace(args: Args, input: ItemFn) -> Result { +pub(crate) fn gen_trace(args: Args, input: ItemFn) -> Result { let func_name = &input.sig.ident; // Check for async_trait-like patterns in the block, and instrument @@ -68,8 +69,8 @@ pub(crate) fn gen_trace(args: Args, input: ItemFn) -> Result (**ty).clone(), + let output_ty = match &input.sig.output { + ReturnType::Type(_, ty) => (**ty).clone(), ReturnType::Default => parse_quote! { () }, }; gen_block( @@ -114,25 +115,35 @@ pub(crate) fn gen_trace(args: Args, input: ItemFn) -> Result Result { - let crate_path = &args.crate_path; - match &args.name { - Some(name) if name.value().is_empty() => { +fn gen_span_name(func_name: &Ident, args: &Args) -> Result { + let Args { + name, + short_name, + crate_path, + .. + } = args; + + if let Some(span_name) = name { + if span_name.value().is_empty() { Err(Error::new(Span::call_site(), "`name` can not be empty")) + } else if *short_name { + Err(Error::new( + Span::call_site(), + "`name` and `short_name` can not be used together", + )) + } else { + Ok(name.into_token_stream()) } - Some(_) if args.short_name => Err(Error::new( - Span::call_site(), - "`name` and `short_name` can not be used together", - )), - Some(name) => Ok(name.into_token_stream()), - None if args.short_name => { + } else { + if *short_name { Ok(LitStr::new(&func_name.to_string(), func_name.span()).into_token_stream()) + } else { + Ok(quote!(#crate_path::func_path!())) } - None => Ok(quote!(#crate_path::func_path!())), } } -fn gen_properties(args: &Args) -> Result { +fn gen_properties(args: &Args) -> Result { if args.properties.is_empty() { return Ok(quote!()); } @@ -163,7 +174,6 @@ fn gen_properties(args: &Args) -> Result { )) } -/// Instrument a block fn gen_block( func_name: &Ident, block: &Block, @@ -171,13 +181,33 @@ fn gen_block( async_keyword: bool, args: &Args, output_ty: Option, -) -> Result { - let name = gen_name(func_name, args)?; +) -> Result { + let name = gen_span_name(func_name, args)?; let properties = gen_properties(args)?; let crate_path = &args.crate_path; - let output_ty_hint = output_ty - .map(erase_impl_trait) - .unwrap_or_else(|| parse_quote! { _ }); + + let output_ty_hint = if let Some(mut ty) = output_ty { + // Replaces `impl Trait` with `_`, so that it can be used as the type + // in the LHS of `let` statements. + struct EraseImplTrait; + + impl VisitMut for EraseImplTrait { + fn visit_type_mut(&mut self, ty: &mut Type) { + if let Type::ImplTrait(..) = ty { + *ty = Type::Infer(TypeInfer { + underscore_token: Token![_](ty.span()), + }); + } else { + visit_mut::visit_type_mut(self, ty); + } + } + } + + visit_mut::visit_type_mut(&mut EraseImplTrait, &mut ty); + ty + } else { + parse_quote!(_) + }; // Generate the instrumented function body. // If the function is an `async fn`, this will wrap it in an async block. @@ -236,35 +266,43 @@ enum AsyncTraitKind<'a> { } struct AsyncTraitInfo<'a> { - // statement that must be patched - _source_stmt: &'a Stmt, + // source statement to be patched + #[expect(unused)] + stmt: &'a Stmt, kind: AsyncTraitKind<'a>, } -// Get the AST of the inner function we need to hook, if it was generated -// by async-trait. -// When we are given a function annotated by async-trait, that function -// is only a placeholder that returns a pinned future containing the -// user logic, and it is that pinned future that needs to be instrumented. -// Were we to instrument its parent, we would only collect information -// regarding the allocation of that future, and not its own span of execution. -// Depending on the version of async-trait, we inspect the block of the function -// to find if it matches the pattern -// `async fn foo<...>(...) {...}; Box::pin(foo<...>(...))` (<=0.1.43), or if -// it matches `Box::pin(async move { ... }) (>=0.1.44). We the return the -// statement that must be instrumented, along with some other information. -// 'gen_body' will then be able to use that information to instrument the -// proper function/future. -// (this follows the approach suggested in -// https://github.com/dtolnay/async-trait/issues/45#issuecomment-571245673) +/// Get the AST of the inner function we need to hook, if it was generated by `async-trait`. +/// +/// When we are given a function annotated by `async-trait`, that function is only a placeholder +/// that returns a pinned future containing the user logic, and it is that pinned future that needs +/// to be instrumented. Were we to instrument its parent, we would only collect information +/// regarding the allocation of that future, and not its own span of execution. +/// +/// Depending on the version of async-trait, we inspect the block of the function to find if it +/// matches the pattern: +/// +/// ```rust,ignore +/// // for async-trait <=0.1.43 +/// async fn foo<...>(...) {...} +/// Box::pin(foo<...>(...)) +/// +/// // for async-trait >= 0.1.44 +/// Box::pin(async move { ... }) +/// ``` +/// +/// We the return the statement to be instrumented, along with some other information. [`gen_block`] +/// will then be able to use that information to instrument the proper function or future. +/// +/// This follows the approach suggested in https://github.com/dtolnay/async-trait/issues/45#issuecomment-571245673. fn get_async_trait_info(block: &Block, block_is_async: bool) -> Option> { - // are we in an async context? If yes, this isn't an async_trait-like pattern + // Are we in an async context? If yes, this isn't an async_trait-like pattern if block_is_async { return None; } // list of async functions declared inside the block - let inside_funs = block.stmts.iter().filter_map(|stmt| { + let inside_fns = block.stmts.iter().filter_map(|stmt| { if let Stmt::Item(Item::Fn(fun)) = &stmt { // If the function is async, this is a candidate if fun.sig.asyncness.is_some() { @@ -274,10 +312,11 @@ fn get_async_trait_info(block: &Block, block_is_async: bool) -> Option Option (func, args), _ => return None, }; - // is it a call to `Box::pin()`? + // Is it a call to `Box::pin()`? let path = match outside_func.as_ref() { Expr::Path(path) => &path.path, _ => return None, @@ -301,21 +340,21 @@ fn get_async_trait_info(block: &Block, block_is_async: bool) -> Option Option String { } res } - -/// Replaces any `impl Trait` with `_` so it can be used as the type in -/// a `let` statement's LHS. -struct ImplTraitEraser; - -impl VisitMut for ImplTraitEraser { - fn visit_type_mut(&mut self, t: &mut Type) { - if let Type::ImplTrait(..) = t { - *t = TypeInfer { - underscore_token: Token![_](t.span()), - } - .into(); - } else { - visit_mut::visit_type_mut(self, t); - } - } -} - -fn erase_impl_trait(mut ty: Type) -> Type { - ImplTraitEraser.visit_type_mut(&mut ty); - ty -} diff --git a/fastrace-macro/src/lib.rs b/fastrace-macro/src/lib.rs index dfa9ddc..62d5b1a 100644 --- a/fastrace-macro/src/lib.rs +++ b/fastrace-macro/src/lib.rs @@ -12,6 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. +// This crate is derived from [1] under the original license header: +// Copyright 2020 TiKV Project Authors. Licensed under Apache-2.0. +// [1]: https://github.com/tikv/minitrace-rust/blob/v0.6.4/minitrace-macro/src/lib.rs + //! An attribute macro designed to eliminate boilerplate code for [`fastrace`]. //! //! [`fastrace`]: https://crates.io/crates/fastrace From 163c538091b50ac020ccf8931c02cf715b3f3057 Mon Sep 17 00:00:00 2001 From: tison Date: Thu, 11 Jun 2026 15:15:24 +0800 Subject: [PATCH 4/5] fixup Signed-off-by: tison --- fastrace-macro/src/args.rs | 24 ++++-------------------- fastrace-macro/src/impls.rs | 2 +- 2 files changed, 5 insertions(+), 21 deletions(-) diff --git a/fastrace-macro/src/args.rs b/fastrace-macro/src/args.rs index 64b75f3..d69554a 100644 --- a/fastrace-macro/src/args.rs +++ b/fastrace-macro/src/args.rs @@ -13,8 +13,6 @@ // limitations under the License. use std::collections::HashSet; -use std::hash::Hash; -use std::hash::Hasher; use proc_macro2::Span; use syn::Error; @@ -35,20 +33,6 @@ pub struct Property { pub value: LitStr, } -impl PartialEq for Property { - fn eq(&self, other: &Self) -> bool { - PartialEq::eq(&self.key, &other.key) - } -} - -impl Eq for Property {} - -impl Hash for Property { - fn hash(&self, state: &mut H) { - self.key.hash(state); - } -} - impl Parse for Property { fn parse(input: ParseStream) -> Result { let key: LitStr = input.parse()?; @@ -63,7 +47,7 @@ pub struct Args { pub name: Option, pub short_name: bool, pub enter_on_poll: bool, - pub properties: HashSet, + pub properties: Vec, pub crate_path: Path, } @@ -72,7 +56,7 @@ impl Parse for Args { let mut name = None; let mut short_name = false; let mut enter_on_poll = false; - let mut properties = HashSet::new(); + let mut properties = vec![]; let mut crate_path = parse_quote!(::fastrace); let mut seen = HashSet::new(); @@ -104,13 +88,13 @@ impl Parse for Args { let _brace_token = braced!(content in input); let property_list = content.parse_terminated(Property::parse, Token![,])?; for property in property_list { - if properties.contains(&property) { + if properties.iter().any(|p: &Property| p.key == property.key) { return Err(Error::new( Span::call_site(), format!("duplicate property key: {}", property.key.value()), )); } - properties.insert(property); + properties.push(property); } } "crate" => { diff --git a/fastrace-macro/src/impls.rs b/fastrace-macro/src/impls.rs index 67a1f52..0ff06c8 100644 --- a/fastrace-macro/src/impls.rs +++ b/fastrace-macro/src/impls.rs @@ -291,7 +291,7 @@ struct AsyncTraitInfo<'a> { /// Box::pin(async move { ... }) /// ``` /// -/// We the return the statement to be instrumented, along with some other information. [`gen_block`] +/// We then return the statement to be instrumented, along with some other information. [`gen_block`] /// will then be able to use that information to instrument the proper function or future. /// /// This follows the approach suggested in https://github.com/dtolnay/async-trait/issues/45#issuecomment-571245673. From caeaa392bb5e4f2fe2fd059de69c99df6a962f7f Mon Sep 17 00:00:00 2001 From: tison Date: Thu, 11 Jun 2026 15:24:05 +0800 Subject: [PATCH 5/5] fixup Signed-off-by: tison --- fastrace-macro/src/impls.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/fastrace-macro/src/impls.rs b/fastrace-macro/src/impls.rs index 0ff06c8..bffe00f 100644 --- a/fastrace-macro/src/impls.rs +++ b/fastrace-macro/src/impls.rs @@ -203,7 +203,7 @@ fn gen_block( } } - visit_mut::visit_type_mut(&mut EraseImplTrait, &mut ty); + EraseImplTrait.visit_type_mut(&mut ty); ty } else { parse_quote!(_) @@ -291,8 +291,9 @@ struct AsyncTraitInfo<'a> { /// Box::pin(async move { ... }) /// ``` /// -/// We then return the statement to be instrumented, along with some other information. [`gen_block`] -/// will then be able to use that information to instrument the proper function or future. +/// We then return the statement to be instrumented, along with some other information. +/// [`gen_block`] will then be able to use that information to instrument the proper +/// function or future. /// /// This follows the approach suggested in https://github.com/dtolnay/async-trait/issues/45#issuecomment-571245673. fn get_async_trait_info(block: &Block, block_is_async: bool) -> Option> {