use proc_macro::TokenStream;
use proc_macro2::TokenStream as TokenStream2;
use quote::{format_ident, quote};
use syn::parse::{Parse, ParseStream};
use syn::punctuated::Punctuated;
use syn::visit::{self, Visit};
use syn::{
Attribute, FnArg, GenericParam, Ident, ItemTrait, Pat, ReturnType, Signature, Token, TraitItem,
TraitItemFn, Type, TypeParamBound, parse_macro_input,
};
struct Args {
shim_name: Ident,
bounds: Punctuated<TypeParamBound, Token![+]>,
}
impl Parse for Args {
fn parse(input: ParseStream) -> syn::Result<Self> {
let shim_name = input.parse()?;
let mut bounds = Punctuated::new();
if input.peek(Token![:]) {
input.parse::<Token![:]>()?;
bounds = Punctuated::parse_separated_nonempty(input)?;
}
Ok(Args { shim_name, bounds })
}
}
fn plain_trait_bound(bound: &TypeParamBound) -> Option<&syn::Path> {
match bound {
TypeParamBound::Trait(t)
if matches!(t.modifier, syn::TraitBoundModifier::None) && t.lifetimes.is_none() =>
{
Some(&t.path)
}
_ => None,
}
}
#[derive(Clone, Copy, PartialEq)]
enum RecognizedBound {
Clone,
Hash,
}
impl RecognizedBound {
fn impl_bound(self) -> TokenStream2 {
match self {
RecognizedBound::Clone => quote! { ::std::clone::Clone },
RecognizedBound::Hash => quote! { ::std::hash::Hash },
}
}
fn doc_line(self, shim: &Ident) -> String {
match self {
RecognizedBound::Clone => format!(
"`Box<dyn {shim}>` implements [`Clone`], and `dyn {shim}` implements \
[`ToOwned`], both cloning the underlying concrete value."
),
RecognizedBound::Hash => format!(
"`dyn {shim}` implements [`Hash`], hashing like the underlying \
concrete value."
),
}
}
fn expand(
self,
shim: &Ident,
combos: &[MarkerCombo],
) -> (TokenStream2, TokenStream2, TokenStream2) {
match self {
RecognizedBound::Clone => expand_clone(shim, combos),
RecognizedBound::Hash => expand_hash(shim, combos),
}
}
}
#[derive(Clone, Copy, PartialEq)]
enum AutoTrait {
Send,
Sync,
Unpin,
UnwindSafe,
RefUnwindSafe,
}
impl AutoTrait {
fn suffix(self) -> &'static str {
match self {
AutoTrait::Send => "send",
AutoTrait::Sync => "sync",
AutoTrait::Unpin => "unpin",
AutoTrait::UnwindSafe => "unwind_safe",
AutoTrait::RefUnwindSafe => "ref_unwind_safe",
}
}
fn path(self) -> TokenStream2 {
match self {
AutoTrait::Send => quote! { ::std::marker::Send },
AutoTrait::Sync => quote! { ::std::marker::Sync },
AutoTrait::Unpin => quote! { ::std::marker::Unpin },
AutoTrait::UnwindSafe => quote! { ::std::panic::UnwindSafe },
AutoTrait::RefUnwindSafe => quote! { ::std::panic::RefUnwindSafe },
}
}
}
enum Classified {
Recognized(RecognizedBound),
Auto(AutoTrait),
Rejected(&'static str),
PassThrough,
}
fn classify(bound: &TypeParamBound) -> Classified {
let Some(ident) = plain_trait_bound(bound).and_then(syn::Path::get_ident) else {
return Classified::PassThrough;
};
match ident.to_string().as_str() {
"Clone" => Classified::Recognized(RecognizedBound::Clone),
"Hash" => Classified::Recognized(RecognizedBound::Hash),
"Send" => Classified::Auto(AutoTrait::Send),
"Sync" => Classified::Auto(AutoTrait::Sync),
"Unpin" => Classified::Auto(AutoTrait::Unpin),
"UnwindSafe" => Classified::Auto(AutoTrait::UnwindSafe),
"RefUnwindSafe" => Classified::Auto(AutoTrait::RefUnwindSafe),
"Copy" => Classified::Rejected(
"trait objects are unsized and can never be `Copy` (use a `Clone` bound to make the shim's boxes cloneable)",
),
"Sized" => Classified::Rejected(
"trait objects are unsized, so the shim's `dyn` type can never be `Sized`",
),
"Default" => Classified::Rejected(
"`Default` has no `self` receiver and cannot be dispatched through a trait object (construct values as concrete types and box them)",
),
"PartialEq" => Classified::Rejected(
"`PartialEq` is not yet a recognized bound (cross-type equality on trait objects needs an `Any` downcast the macro does not generate)",
),
"Eq" => Classified::Rejected(
"`Eq` is not yet a recognized bound (cross-type equality on trait objects needs an `Any` downcast the macro does not generate)",
),
"PartialOrd" => Classified::Rejected(
"`PartialOrd` is not supported: the macro cannot define an order between different implementor types (sort with `sort_by_key` or implement the comparison traits for the shim's `dyn` type by hand)",
),
"Ord" => Classified::Rejected(
"`Ord` is not supported: the macro cannot define a total order between different implementor types (sort with `sort_by_key` or implement the comparison traits for the shim's `dyn` type by hand)",
),
_ => Classified::PassThrough,
}
}
struct MarkerCombo {
suffix: String,
markers: TokenStream2,
}
fn marker_combos(autos: &[AutoTrait]) -> Vec<MarkerCombo> {
(0..1usize << autos.len())
.map(|mask| {
let mut suffix = String::new();
let mut markers = TokenStream2::new();
for (i, auto) in autos.iter().enumerate() {
if mask & (1 << i) == 0 {
continue;
}
suffix.push('_');
suffix.push_str(auto.suffix());
let path = auto.path();
markers.extend(quote! { + #path });
}
MarkerCombo { suffix, markers }
})
.collect()
}
fn expand_clone(
shim: &Ident,
combos: &[MarkerCombo],
) -> (TokenStream2, TokenStream2, TokenStream2) {
let mut sigs = TokenStream2::new();
let mut impls = TokenStream2::new();
let mut after = TokenStream2::new();
for MarkerCombo { suffix, markers } in combos {
let method = format_ident!("__dyn_shim_clone_box{suffix}");
sigs.extend(quote! {
#[doc(hidden)]
fn #method(&self) -> ::std::boxed::Box<dyn #shim #markers>
where
Self: 'static #markers;
});
impls.extend(quote! {
fn #method(&self) -> ::std::boxed::Box<dyn #shim #markers>
where
Self: 'static #markers,
{
::std::boxed::Box::new(::std::clone::Clone::clone(self))
}
});
after.extend(quote! {
impl ::std::clone::Clone for ::std::boxed::Box<dyn #shim #markers> {
fn clone(&self) -> Self {
(**self).#method()
}
}
impl ::std::borrow::ToOwned for dyn #shim #markers {
type Owned = ::std::boxed::Box<dyn #shim #markers>;
fn to_owned(&self) -> Self::Owned {
self.#method()
}
}
});
}
(sigs, impls, after)
}
fn expand_hash(shim: &Ident, combos: &[MarkerCombo]) -> (TokenStream2, TokenStream2, TokenStream2) {
let sigs = quote! {
#[doc(hidden)]
fn __dyn_shim_hash(&self, state: &mut dyn ::std::hash::Hasher);
};
let impls = quote! {
fn __dyn_shim_hash(&self, mut state: &mut dyn ::std::hash::Hasher) {
<__T as ::std::hash::Hash>::hash(self, &mut state)
}
};
let mut after = TokenStream2::new();
for MarkerCombo { markers, .. } in combos {
after.extend(quote! {
impl ::std::hash::Hash for dyn #shim #markers {
fn hash<__H: ::std::hash::Hasher>(&self, state: &mut __H) {
self.__dyn_shim_hash(state)
}
}
});
}
(sigs, impls, after)
}
#[proc_macro_attribute]
pub fn dyn_shim(attr: TokenStream, item: TokenStream) -> TokenStream {
let Args { shim_name, bounds } = parse_macro_input!(attr as Args);
let input = parse_macro_input!(item as ItemTrait);
if let Some(param) = input.generics.params.first() {
return syn::Error::new_spanned(param, "dyn_shim does not support generic source traits")
.to_compile_error()
.into();
}
let mut recognized = Vec::new();
let mut autos = Vec::new();
let mut passthrough: Punctuated<TypeParamBound, Token![+]> = Punctuated::new();
for bound in bounds {
match classify(&bound) {
Classified::Rejected(msg) => {
return syn::Error::new_spanned(&bound, msg)
.to_compile_error()
.into();
}
Classified::Recognized(k) => {
if !recognized.contains(&k) {
recognized.push(k);
}
}
Classified::Auto(auto) => {
if !autos.contains(&auto) {
autos.push(auto);
}
passthrough.push(bound);
}
Classified::PassThrough => passthrough.push(bound),
}
}
let combos = if recognized.is_empty() {
Vec::new()
} else {
marker_combos(&autos)
};
for item in &input.items {
let attrs = match item {
TraitItem::Fn(item) => {
for attr in item.attrs.iter().filter(|a| a.path().is_ident("dyn_shim")) {
if let Err(err) = require_skip(attr) {
return err.to_compile_error().into();
}
}
continue;
}
TraitItem::Const(item) => &item.attrs,
TraitItem::Type(item) => &item.attrs,
TraitItem::Macro(item) => &item.attrs,
_ => continue,
};
if let Some(attr) = attrs.iter().find(|a| a.path().is_ident("dyn_shim")) {
return syn::Error::new_spanned(
attr,
"#[dyn_shim] attributes are only supported on methods",
)
.to_compile_error()
.into();
}
}
let src = &input.ident;
let vis = &input.vis;
let mut sigs = Vec::new();
let mut impls = Vec::new();
let mut skipped: Vec<(String, &str)> = Vec::new();
for item in &input.items {
let TraitItem::Fn(method) = item else {
continue;
};
match skip(method) {
Some(reason) => skipped.push((method.sig.ident.to_string(), reason)),
None => {
let (sig, body) = forward(method, src);
sigs.push(sig);
impls.push(body);
}
}
}
let mut clean = input.clone();
for item in &mut clean.items {
if let TraitItem::Fn(method) = item {
method.attrs.retain(|a| !a.path().is_ident("dyn_shim"));
}
}
for line in source_doc(&shim_name) {
clean.attrs.push(syn::parse_quote! { #[doc = #line] });
}
let doc_attrs = shim_doc(src, &shim_name, &recognized, &skipped)
.into_iter()
.map(|line| quote! { #[doc = #line] });
let supertraits = (!passthrough.is_empty()).then(|| quote! { : #passthrough });
let impl_bounds = (!passthrough.is_empty()).then(|| quote! { + #passthrough });
let recognized_bounds: TokenStream2 = recognized
.iter()
.map(|k| {
let path = k.impl_bound();
quote! { + #path }
})
.collect();
let mut recognized_sigs = TokenStream2::new();
let mut recognized_impls = TokenStream2::new();
let mut recognized_extra = TokenStream2::new();
for k in &recognized {
let (sigs, impls, extra) = k.expand(&shim_name, &combos);
recognized_sigs.extend(sigs);
recognized_impls.extend(impls);
recognized_extra.extend(extra);
}
quote! {
#clean
#(#doc_attrs)*
#vis trait #shim_name #supertraits {
#(#sigs)*
#recognized_sigs
}
impl<__T: #src #impl_bounds #recognized_bounds> #shim_name for __T {
#(#impls)*
#recognized_impls
}
#recognized_extra
}
.into()
}
fn forward(method: &TraitItemFn, src: &Ident) -> (TokenStream2, TokenStream2) {
let mut sig = method.sig.clone();
let Some(FnArg::Receiver(recv)) = sig.inputs.first() else {
unreachable!("skip guarantees a receiver")
};
let by_value = recv.reference.is_none()
&& (recv.colon_token.is_none()
|| matches!(&*recv.ty, Type::Path(p) if p.qself.is_none() && p.path.is_ident("Self")));
let self_expr = if by_value {
sig.inputs[0] = syn::parse_quote! { self: ::std::boxed::Box<Self> };
quote! { *self }
} else {
quote! { self }
};
let mut names = Vec::new();
for (i, arg) in sig.inputs.iter_mut().skip(1).enumerate() {
let FnArg::Typed(pat) = arg else { continue };
let id = match &*pat.pat {
Pat::Ident(p) if p.by_ref.is_none() && p.subpat.is_none() => p.ident.clone(),
_ => format_ident!("__a{i}"),
};
*pat.pat = syn::parse_quote! { #id };
names.push(id);
}
let attrs: Vec<&Attribute> = method
.attrs
.iter()
.filter(|a| !a.path().is_ident("dyn_shim"))
.collect();
let cfg_attrs: Vec<&Attribute> = attrs
.iter()
.copied()
.filter(|a| a.path().is_ident("cfg"))
.collect();
let name = &sig.ident;
let shim_sig = quote! {
#(#attrs)*
#sig ;
};
let shim_impl = quote! {
#(#cfg_attrs)*
#[allow(deprecated)]
#sig {
<__T as #src>::#name(#self_expr #(, #names)*)
}
};
(shim_sig, shim_impl)
}
fn source_doc(shim_name: &Ident) -> Vec<String> {
vec![
String::new(),
"# Dyn Compatibility".to_string(),
String::new(),
format!(
"[`{shim_name}`] is a generated dyn-compatible shim for this trait. \
Use `dyn {shim_name}` to hold implementors behind a trait object."
),
]
}
fn shim_doc(
src: &Ident,
shim: &Ident,
recognized: &[RecognizedBound],
skipped: &[(String, &str)],
) -> Vec<String> {
let mut lines = vec![format!("Dyn-compatible shim for [`{src}`].")];
if !recognized.is_empty() {
lines.push(String::new());
for k in recognized {
lines.push(k.doc_line(shim));
}
}
if !skipped.is_empty() {
lines.push(String::new());
lines.push("These methods of the source trait are not dyn-compatible, so they".to_string());
lines.push("are not part of this shim. Call them on the concrete type.".to_string());
lines.push(String::new());
for (name, reason) in skipped {
lines.push(format!("- [`{src}::{name}`] ({reason})"));
}
}
lines
}
fn skip(method: &TraitItemFn) -> Option<&'static str> {
let sig = &method.sig;
if is_opted_out(method) {
Some("opted out with #[dyn_shim(skip)]")
} else if sig.asyncness.is_some() {
Some("async fn")
} else if !has_self_receiver(sig) {
Some("no self receiver")
} else if has_type_or_const_generics(sig) {
Some("generic type or const parameter")
} else if requires_self_sized(sig) {
Some("requires Self: Sized")
} else if signature_mentions_self_or_impl_trait(sig) {
Some("mentions Self or impl Trait")
} else {
None
}
}
fn require_skip(attr: &Attribute) -> syn::Result<()> {
let mut skip = false;
attr.parse_nested_meta(|meta| {
if meta.path.is_ident("skip") {
skip = true;
Ok(())
} else {
Err(meta.error("unsupported dyn_shim argument, expected `skip`"))
}
})?;
if skip {
Ok(())
} else {
Err(syn::Error::new_spanned(attr, "expected #[dyn_shim(skip)]"))
}
}
fn is_opted_out(method: &TraitItemFn) -> bool {
method.attrs.iter().any(|a| a.path().is_ident("dyn_shim"))
}
fn has_self_receiver(sig: &Signature) -> bool {
matches!(sig.inputs.first(), Some(FnArg::Receiver(_)))
}
fn requires_self_sized(sig: &Signature) -> bool {
let Some(where_clause) = &sig.generics.where_clause else {
return false;
};
where_clause.predicates.iter().any(|pred| {
let syn::WherePredicate::Type(pred) = pred else {
return false;
};
let Type::Path(bounded) = &pred.bounded_ty else {
return false;
};
if bounded.qself.is_some() || !bounded.path.is_ident("Self") {
return false;
}
pred.bounds
.iter()
.any(|bound| matches!(bound, syn::TypeParamBound::Trait(t) if t.path.is_ident("Sized")))
})
}
fn has_type_or_const_generics(sig: &Signature) -> bool {
sig.generics
.params
.iter()
.any(|p| !matches!(p, GenericParam::Lifetime(_)))
}
fn signature_mentions_self_or_impl_trait(sig: &Signature) -> bool {
let return_bad =
matches!(&sig.output, ReturnType::Type(_, ty) if mentions_self_or_impl_trait(ty));
let arg_bad = sig
.inputs
.iter()
.skip(1)
.any(|arg| matches!(arg, FnArg::Typed(pat) if mentions_self_or_impl_trait(&pat.ty)));
return_bad || arg_bad
}
fn mentions_self_or_impl_trait(ty: &Type) -> bool {
struct Finder(bool);
impl<'ast> Visit<'ast> for Finder {
fn visit_path(&mut self, path: &'ast syn::Path) {
if path.segments.iter().any(|s| s.ident == "Self") {
self.0 = true;
}
visit::visit_path(self, path);
}
fn visit_type_impl_trait(&mut self, it: &'ast syn::TypeImplTrait) {
self.0 = true;
visit::visit_type_impl_trait(self, it);
}
}
let mut finder = Finder(false);
finder.visit_type(ty);
finder.0
}