jstraceable_derive/
lib.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5use syn::parse_quote;
6use synstructure::{decl_derive, quote};
7
8decl_derive!([JSTraceable, attributes(no_trace, custom_trace)] =>
9/// Implements `JSTraceable` on structs and enums
10///
11/// Example:
12/// ```rust
13/// #[derive(JSTraceable)]
14/// struct S {
15///   js_managed: JSManagedType,
16///   #[no_trace]
17///   non_js: NonJSManagedType,
18///   #[custom_trace] // Extern type implements CustomTraceable that is in servo => no problem with orphan rules
19///   extern_managed_type: Extern<JSManagedType>,
20/// }
21/// ```
22///
23/// creates:
24///
25/// ```rust
26/// unsafe impl JSTraceable for S {
27///     #[inline]
28///     unsafe fn trace(&self, tracer: *mut js::jsapi::JSTracer) {
29///         match *self {
30///             S {
31///                 js_managed: ref __binding_0,
32///                 non_js: ref __binding_1,
33///                 extern_managed_type: ref __binding_2,
34///             } => {
35///                 {
36///                     __binding_0.trace(tracer);
37///                 }
38///                 {
39///                     // __binding_1 is not traceable so we do not need to trace it
40///                 }
41///                 {
42///                     <crate::dom::bindings::trace::CustomTraceable>::trace(__binding_2, tracer);
43///                 }
44///             },
45///         }
46///     }
47/// }
48/// ```
49///
50/// In cases where there is a need to make type (empty) traceable (`HashMap<NoTraceable, Traceable>`),
51/// NoTrace wrapper can be used, because it implements empty traceble:
52/// ```rust
53/// unsafe impl<T> JSTraceable for NoTrace<T> {
54///     unsafe fn trace(&self, _: *mut ::js::jsapi::JSTracer) { /* nop */}
55/// }
56/// ```
57///
58/// ## SAFETY
59/// Puting `#[no_trace]` on fields is safe if there are no types that are JS managed in that field.
60/// `#[no_trace]` should NOT be put on field that does implement (non-empty) `JSTraceable` (is JS managed).
61/// There are safeguards in place to prevent such mistakes. Example error:
62///
63/// ```console
64/// error[E0282]: type annotations needed
65/// |
66/// | #[derive(JSTraceable, MallocSizeOf)]
67/// |          ^^^^^^^^^^^ cannot infer type of the type parameter `Self` declared on the trait `NoTraceOnJSTraceable`
68/// |
69/// = note: this error originates in the derive macro `JSTraceable`
70/// ```
71///
72/// If you can assure that type has empty JSTraceable impl, you can bypass guards, providing your reasoning:
73/// ```rust
74/// #[derive(JSTraceable)]
75/// struct S {
76///   #[no_trace = "Safe because both u32 and u64 are empty traceable"]
77///   field: HashMap<u32, u64>,
78/// }
79/// ```
80js_traceable_derive);
81
82// based off https://docs.rs/static_assertions/latest/src/static_assertions/assert_impl.rs.html#263
83fn assert_not_impl_traceable(ty: &syn::Type) -> proc_macro2::TokenStream {
84    quote!(
85        const _: fn() = || {
86            // Generic trait with a blanket impl over `()` for all types.
87            // becomes ambiguous if impl
88            trait NoTraceOnJSTraceable<A> {
89                // Required for actually being able to reference the trait.
90                fn some_item() {}
91            }
92
93            impl<T: ?Sized> NoTraceOnJSTraceable<()> for T {}
94
95            // Used for the specialized impl when JSTraceable is implemented.
96            #[expect(dead_code)]
97            struct Invalid0;
98            // forbids JSTraceable
99            impl<T> NoTraceOnJSTraceable<Invalid0> for T where T: ?Sized + crate::JSTraceable {}
100
101            #[expect(dead_code)]
102            struct Invalid2;
103            // forbids HashMap<JSTraceble, _>
104            impl<K, V, S> NoTraceOnJSTraceable<Invalid2> for std::collections::HashMap<K, V, S>
105            where
106                K: crate::JSTraceable + std::cmp::Eq + std::hash::Hash,
107                S: std::hash::BuildHasher,
108            {
109            }
110
111            #[expect(dead_code)]
112            struct Invalid3;
113            // forbids HashMap<_, JSTraceble>
114            impl<K, V, S> NoTraceOnJSTraceable<Invalid3> for std::collections::HashMap<K, V, S>
115            where
116                K: std::cmp::Eq + std::hash::Hash,
117                V: crate::JSTraceable,
118                S: std::hash::BuildHasher,
119            {
120            }
121
122            #[expect(dead_code)]
123            struct Invalid4;
124            // forbids BTreeMap<_, JSTraceble>
125            impl<K, V> NoTraceOnJSTraceable<Invalid4> for std::collections::BTreeMap<K, V> where
126                K: crate::JSTraceable + std::cmp::Eq + std::hash::Hash
127            {
128            }
129
130            #[expect(dead_code)]
131            struct Invalid5;
132            // forbids BTreeMap<_, JSTraceble>
133            impl<K, V> NoTraceOnJSTraceable<Invalid5> for std::collections::BTreeMap<K, V>
134            where
135                K: std::cmp::Eq + std::hash::Hash,
136                V: crate::JSTraceable,
137            {
138            }
139
140            // If there is only one specialized trait impl, type inference with
141            // `_` can be resolved and this can compile. Fails to compile if
142            // ty implements `NoTraceOnJSTraceable<InvalidX>`.
143            let _ = <#ty as NoTraceOnJSTraceable<_>>::some_item;
144        };
145    )
146}
147
148fn js_traceable_derive(s: synstructure::Structure) -> proc_macro2::TokenStream {
149    let mut asserts = quote!();
150    let match_body = s.each(|binding| {
151        for attr in binding.ast().attrs.iter() {
152            if attr.path().is_ident("no_trace") {
153                // If no reason argument is provided to `no_trace` (ie `#[no_trace="This types does not need..."]`),
154                // assert that the type in this bound field does not implement traceable.
155                if !matches!(attr.meta, syn::Meta::NameValue(_)) {
156                    asserts.extend(assert_not_impl_traceable(&binding.ast().ty));
157                }
158                return None;
159            } else if attr.path().is_ident("custom_trace") {
160                return Some(quote!(<dyn crate::CustomTraceable>::trace(#binding, tracer);));
161            }
162        }
163        Some(quote!(#binding.trace(tracer);))
164    });
165
166    let ast = s.ast();
167    let name = &ast.ident;
168    let (impl_generics, ty_generics, where_clause) = ast.generics.split_for_impl();
169    let mut where_clause = where_clause.unwrap_or(&parse_quote!(where)).clone();
170    for param in ast.generics.type_params() {
171        let ident = &param.ident;
172        where_clause
173            .predicates
174            .push(parse_quote!(#ident: crate::JSTraceable))
175    }
176
177    let tokens = quote! {
178        #asserts
179
180        #[expect(unsafe_code)]
181        unsafe impl #impl_generics crate::JSTraceable for #name #ty_generics #where_clause {
182            #[inline]
183            #[expect(unused_variables, unused_imports)]
184            unsafe fn trace(&self, tracer: *mut js::jsapi::JSTracer) {
185                use crate::JSTraceable;
186                match *self {
187                    #match_body
188                }
189            }
190        }
191    };
192
193    tokens
194}