i_slint_core_macros/
lib.rs

1// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
2// Copyright © SixtyFPS GmbH <info@slint.dev>
3// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
4
5// Copyright © SixtyFPS GmbH <info@slint.dev>
6
7#![doc = include_str!("README.md")]
8#![doc(html_logo_url = "https://slint.dev/logo/slint-logo-square-light.svg")]
9
10extern crate proc_macro;
11use proc_macro::TokenStream;
12use quote::quote;
13
14mod slint_doc;
15
16/// This derive macro is used with structures in the run-time library that are meant
17/// to be exposed to the language. The structure is introspected for properties and fields
18/// marked with the `rtti_field` attribute and generates run-time type information for use
19/// with the interpreter.
20/// In addition all `Property<T> foo` fields get a convenient getter function generated
21/// that works on a `Pin<&Self>` receiver.
22#[proc_macro_derive(SlintElement, attributes(rtti_field))]
23pub fn slint_element(input: TokenStream) -> TokenStream {
24    let input = syn::parse_macro_input!(input as syn::DeriveInput);
25
26    let fields = match &input.data {
27        syn::Data::Struct(syn::DataStruct { fields: f @ syn::Fields::Named(..), .. }) => f,
28        _ => {
29            return syn::Error::new(
30                input.ident.span(),
31                "Only `struct` with named field are supported",
32            )
33            .to_compile_error()
34            .into()
35        }
36    };
37
38    let mut pub_prop_field_names = Vec::new();
39    let mut pub_prop_field_names_normalized = Vec::new();
40    let mut pub_prop_field_types = Vec::new();
41    let mut property_names = Vec::new();
42    let mut property_visibility = Vec::new();
43    let mut property_types = Vec::new();
44
45    for field in fields {
46        if let Some(property_type) = property_type(&field.ty) {
47            let name = field.ident.as_ref().unwrap();
48            if matches!(field.vis, syn::Visibility::Public(_)) {
49                pub_prop_field_names_normalized.push(normalize_identifier(name));
50                pub_prop_field_names.push(name);
51                pub_prop_field_types.push(&field.ty);
52            }
53
54            property_names.push(name);
55            property_visibility.push(field.vis.clone());
56            property_types.push(property_type);
57        }
58    }
59
60    let (plain_field_names, plain_field_types): (Vec<_>, Vec<_>) = fields
61        .iter()
62        .filter(|f| {
63            f.attrs.iter().any(|attr| {
64                matches!(&attr.meta, syn::Meta::Path(path) if path.get_ident().map(|ident| *ident == "rtti_field").unwrap_or(false))
65            })
66        })
67        .map(|f| (f.ident.as_ref().unwrap(), &f.ty))
68        .unzip();
69    let plain_field_names_normalized =
70        plain_field_names.iter().map(|f| normalize_identifier(f)).collect::<Vec<_>>();
71
72    let mut callback_field_names = Vec::new();
73    let mut callback_field_names_normalized = Vec::new();
74    let mut callback_args = Vec::new();
75    let mut callback_rets = Vec::new();
76    for field in fields {
77        if let Some((arg, ret)) = callback_arg(&field.ty) {
78            if matches!(field.vis, syn::Visibility::Public(_)) {
79                let name = field.ident.as_ref().unwrap();
80                callback_field_names_normalized.push(normalize_identifier(name));
81                callback_field_names.push(name);
82                callback_args.push(arg);
83                callback_rets.push(ret);
84            }
85        }
86    }
87
88    let item_name = &input.ident;
89
90    quote!(
91        #[allow(clippy::nonstandard_macro_braces)]
92        #[cfg(feature = "rtti")]
93        impl BuiltinItem for #item_name {
94            fn name() -> &'static str {
95                stringify!(#item_name)
96            }
97            fn properties<Value: ValueType>() -> ::alloc::vec::Vec<(&'static str, &'static dyn PropertyInfo<Self, Value>)> {
98                ::alloc::vec![#( {
99                    const O : MaybeAnimatedPropertyInfoWrapper<#item_name, #pub_prop_field_types> =
100                        MaybeAnimatedPropertyInfoWrapper(#item_name::FIELD_OFFSETS.#pub_prop_field_names);
101                    (#pub_prop_field_names_normalized, (&O).as_property_info())
102                } ),*]
103            }
104            fn fields<Value: ValueType>() -> ::alloc::vec::Vec<(&'static str, &'static dyn FieldInfo<Self, Value>)> {
105                ::alloc::vec![#( {
106                    const O : const_field_offset::FieldOffset<#item_name, #plain_field_types, const_field_offset::AllowPin> =
107                        #item_name::FIELD_OFFSETS.#plain_field_names;
108                    (#plain_field_names_normalized, &O as &'static dyn FieldInfo<Self, Value>)
109                } ),*]
110            }
111            fn callbacks<Value: ValueType>() -> ::alloc::vec::Vec<(&'static str, &'static dyn CallbackInfo<Self, Value>)> {
112                ::alloc::vec![#( {
113                    const O : const_field_offset::FieldOffset<#item_name, Callback<#callback_args, #callback_rets>, const_field_offset::AllowPin> =
114                         #item_name::FIELD_OFFSETS.#callback_field_names;
115                    (#callback_field_names_normalized, &O as  &'static dyn CallbackInfo<Self, Value>)
116                } ),*]
117            }
118        }
119
120        impl #item_name {
121            #(
122                #property_visibility fn #property_names(self: core::pin::Pin<&Self>) -> #property_types {
123                    Self::FIELD_OFFSETS.#property_names.apply_pin(self).get()
124                }
125            )*
126        }
127    )
128    .into()
129}
130
131fn normalize_identifier(name: &syn::Ident) -> String {
132    name.to_string().replace('_', "-")
133}
134
135// Try to match `Property<Foo>` on the syn tree and return Foo if found
136fn property_type(ty: &syn::Type) -> Option<&syn::Type> {
137    if let syn::Type::Path(syn::TypePath { path: syn::Path { segments, .. }, .. }) = ty {
138        if let Some(syn::PathSegment {
139            ident,
140            arguments:
141                syn::PathArguments::AngleBracketed(syn::AngleBracketedGenericArguments { args, .. }),
142        }) = segments.first()
143        {
144            match args.first() {
145                Some(syn::GenericArgument::Type(property_type)) if *ident == "Property" => {
146                    return Some(property_type)
147                }
148                _ => {}
149            }
150        }
151    }
152    None
153}
154
155// Try to match `Callback<Args, Ret>` on the syn tree and return Args and Ret if found
156fn callback_arg(ty: &syn::Type) -> Option<(&syn::Type, Option<&syn::Type>)> {
157    if let syn::Type::Path(syn::TypePath { path: syn::Path { segments, .. }, .. }) = ty {
158        if let Some(syn::PathSegment {
159            ident,
160            arguments:
161                syn::PathArguments::AngleBracketed(syn::AngleBracketedGenericArguments { args, .. }),
162        }) = segments.first()
163        {
164            if ident != "Callback" {
165                return None;
166            }
167            let mut it = args.iter();
168            let first = match it.next() {
169                Some(syn::GenericArgument::Type(ty)) => ty,
170                _ => return None,
171            };
172            let sec = match it.next() {
173                Some(syn::GenericArgument::Type(ty)) => Some(ty),
174                _ => None,
175            };
176            return Some((first, sec));
177        }
178    }
179    None
180}
181
182/// An attribute macro that simply return its input and ignore any arguments
183#[proc_macro_attribute]
184pub fn identity(_attr: TokenStream, item: TokenStream) -> TokenStream {
185    item
186}
187
188/// To be applied on any item that has documentation comment, it will convert link to `slint:Foo` to the link from the
189/// documentation map from link-data.json
190#[proc_macro_attribute]
191pub fn slint_doc(_attr: TokenStream, item: TokenStream) -> TokenStream {
192    use syn::visit_mut::VisitMut;
193    let mut visitor = slint_doc::Visitor::new();
194    let mut item = syn::parse_macro_input!(item as syn::Item);
195    visitor.visit_item_mut(&mut item);
196    assert!(visitor.1, "No slint link found");
197    quote!(#item).into()
198}
199
200/// Same as `slint_doc` but for string literals instead of doc comments (useful for crate level documentation that cannot have an attribute)
201#[proc_macro]
202pub fn slint_doc_str(input: TokenStream) -> TokenStream {
203    let input = syn::parse_macro_input!(input as syn::LitStr);
204    let mut doc = input.value();
205    let mut visitor = slint_doc::Visitor::new();
206    visitor.process_string(&mut doc);
207    assert!(visitor.1, "No slint link found");
208    quote!(#doc).into()
209}
210
211/// Attribute macro that removes `extern "..."` from the function signatures
212///
213/// This is useful because wasm does not support `extern "C-unwind"` and also
214/// warn about ABI incompatibilities we wouldn't care about.
215///
216/// (can be applied to a function or a vtable struct)
217#[proc_macro_attribute]
218pub fn remove_extern(_attr: TokenStream, item: TokenStream) -> TokenStream {
219    let mut input = syn::parse_macro_input!(item as syn::Item);
220
221    match &mut input {
222        syn::Item::Fn(item_fn) => {
223            item_fn.sig.abi.take();
224        }
225        syn::Item::Struct(item_struct) => {
226            for f in item_struct.fields.iter_mut() {
227                if let syn::Type::BareFn(f) = &mut f.ty {
228                    f.abi.take();
229                }
230            }
231        }
232        _ => (),
233    }
234
235    quote!(#input).into()
236}