cvars_macros/
lib.rs

1#![doc = include_str!("../README.md")]
2#![warn(missing_docs)]
3#![allow(clippy::let_and_return)]
4
5use std::{collections::HashSet, env};
6
7use proc_macro::TokenStream;
8use proc_macro2::Span;
9use quote::quote;
10use syn::{
11    parse::{Parse, ParseStream},
12    parse_macro_input,
13    punctuated::Punctuated,
14    AttrStyle, Attribute, Data, DeriveInput, Expr, Fields, Ident, Meta, MetaList, Token, Type,
15};
16
17/// Parsed input to the `cvars!` macro.
18struct CvarsDef {
19    attrs: Vec<Attribute>,
20    /// Whether `#[cvars(skip)]` was present.
21    /// It has to be removed from the list of attributes before passing them on
22    /// so we save it here separately.
23    sorted: bool,
24    cvars: Vec<CvarDef>,
25}
26
27impl Parse for CvarsDef {
28    fn parse(input: ParseStream) -> syn::Result<Self> {
29        let attrs_raw = input.call(Attribute::parse_inner)?;
30        let mut attrs = Vec::new();
31        let mut sorted = false;
32        for attr in attrs_raw {
33            if is_sorted(&attr) {
34                sorted = true;
35            } else {
36                attrs.push(attr);
37            }
38        }
39
40        let punctuated = Punctuated::<CvarDef, Token![,]>::parse_terminated(input)?;
41        let cvars = punctuated.into_iter().collect();
42
43        Ok(CvarsDef {
44            attrs,
45            sorted,
46            cvars,
47        })
48    }
49}
50
51/// Definition of one cvar from the `cvars!` macro.
52struct CvarDef {
53    attrs: Vec<Attribute>,
54    /// Whether `#[cvars(skip)]` was present.
55    skip: bool,
56    name: Ident,
57    ty: Type,
58    value: Expr,
59}
60
61impl Parse for CvarDef {
62    fn parse(input: ParseStream) -> syn::Result<Self> {
63        let attrs_raw = input.call(Attribute::parse_outer)?;
64        let mut attrs = Vec::new();
65        let mut skip = false;
66        for attr in attrs_raw {
67            if is_skip(&attr) {
68                skip = true;
69            } else {
70                attrs.push(attr);
71            }
72        }
73        let name = input.parse()?;
74        let _: Token![:] = input.parse()?;
75        let ty = input.parse()?;
76        let _: Token![=] = input.parse()?;
77        let value = input.parse()?;
78        Ok(CvarDef {
79            attrs,
80            skip,
81            name,
82            ty,
83            value,
84        })
85    }
86}
87
88/// Is it `cvars(sorted)`?
89fn is_sorted(attr: &Attribute) -> bool {
90    if let Meta::List(MetaList { path, tokens, .. }) = &attr.meta {
91        if !path.is_ident("cvars") {
92            return false;
93        }
94
95        if tokens.to_string() == "sorted" {
96            return true;
97        } else {
98            panic!("Unknown cvars attribute: {}", tokens);
99        }
100    }
101
102    false
103}
104
105/// Is it `cvars(skip)`?
106fn is_skip(attr: &Attribute) -> bool {
107    if let Meta::List(MetaList { path, tokens, .. }) = &attr.meta {
108        if !path.is_ident("cvars") {
109            return false;
110        }
111
112        if tokens.to_string() == "skip" {
113            return true;
114        } else {
115            panic!("Unknown cvars attribute: {}", tokens);
116        }
117    }
118
119    false
120}
121
122/// Generate the `Cvars` struct and its impls. Each cvar and its default value is defined on one line.
123/// This is the recommended way of using this crate.
124///
125/// All types used as cvars have to impl `FromStr` and `Display`.
126///
127/// # Generated code
128///
129/// The macro generates:
130/// - the struct definition
131/// - `impl Default for Cvars` which sets the initial values
132/// - `impl Cvars` with methods for interacting with cvars
133///
134/// The generated methods:
135/// - `get_string` - take cvar name as string and return its value as a `String`
136/// - `set_str` - take cvar name as string and its new value as a `&str`
137/// - `get` - take cvar name as string and return its value as the correct type
138/// - `set` - take cvar name as string and its new value as the correct type
139///
140/// See your IDE or [the SetGet trait](https://docs.rs/cvars/latest/cvars/trait.SetGet.html)
141/// for their exact signatures.
142///
143/// # Example
144///
145/// ```rust
146/// use cvars::cvars;
147///
148/// cvars! {
149///     g_rocket_launcher_ammo_max: i32 = 20,
150///     g_rocket_launcher_damage: f32 = 75.0,
151/// }
152/// ```
153///
154/// # Attributes and doc-comments
155///
156/// To add attributes or doc-comments to the generated struct,
157/// use _inner_ attributes and _inner_ comments.
158///
159/// ```rust
160/// use cvars::cvars;
161///
162/// cvars! {
163///     //! Documentation for the generated struct
164///
165///     #![derive(Debug, Clone)]
166///     #![cvars(sorted)]
167///
168///     /// Documentation for the cvar
169///     cl_projectile_render_distance: f64 = 2048.0,
170/// }
171/// ```
172///
173/// Use `#![cvars(sorted)]` to check the cvars are in lexicographic order.
174/// If not, the macro will panic as there's currently no way to emit a warning from proc macros.
175#[proc_macro]
176pub fn cvars(input: TokenStream) -> TokenStream {
177    let begin = std::time::Instant::now();
178
179    let cvars_def: CvarsDef = parse_macro_input!(input);
180
181    let mut cvars_attrs = cvars_def.attrs;
182    for attr in &mut cvars_attrs {
183        attr.style = AttrStyle::Outer;
184    }
185
186    let mut attrss = Vec::new();
187    let mut skips = Vec::new();
188    let mut names = Vec::new();
189    let mut tys = Vec::new();
190    let mut values = Vec::new();
191    for cvar_def in cvars_def.cvars {
192        attrss.push(cvar_def.attrs);
193        skips.push(cvar_def.skip);
194        names.push(cvar_def.name);
195        tys.push(cvar_def.ty);
196        values.push(cvar_def.value);
197    }
198
199    let struct_name = Ident::new("Cvars", Span::call_site());
200    let generated = generate(struct_name, cvars_def.sorted, &skips, &names, &tys);
201
202    let expanded = quote! {
203        #(
204            #cvars_attrs
205        )*
206        pub struct Cvars {
207            #(
208                #( #attrss )*
209                pub #names: #tys,
210            )*
211        }
212
213        #[automatically_derived]
214        impl ::core::default::Default for Cvars {
215            fn default() -> Self {
216                Self {
217                    #( #names: #values, )*
218                }
219            }
220        }
221
222        #generated
223    };
224    let expanded = expanded.into();
225
226    let end = std::time::Instant::now();
227    if env::var("CVARS_STATS").is_ok() {
228        eprintln!("cvars! took {:?}", end - begin);
229    }
230
231    expanded
232}
233
234/// Generate setters and getters that take cvar names as string.
235/// This does the same thing as `cvars!` but you can use it on an existing struct.
236///
237/// Initial/default values have to be specified separately.
238///
239/// All types used as cvars have to impl `FromStr` and `Display`.
240///
241/// See [`cvars!`](cvars!#generated-code) for more details about the generated code.
242///
243/// # Example
244///
245/// ```rust
246/// use cvars::SetGet;
247///
248/// #[derive(SetGet)]
249/// pub struct Cvars {
250///     g_rocket_launcher_ammo_max: i32,
251///     g_rocket_launcher_damage: f32,
252/// }
253///
254/// impl Cvars {
255///     pub fn new() -> Self {
256///         Self {
257///             g_rocket_launcher_ammo_max: 20,
258///             g_rocket_launcher_damage: 75.0,
259///         }
260///     }
261/// }
262/// ```
263#[proc_macro_derive(SetGet, attributes(cvars))]
264pub fn derive(input: TokenStream) -> TokenStream {
265    let begin = std::time::Instant::now();
266
267    let input: DeriveInput = parse_macro_input!(input);
268
269    let struct_name = input.ident;
270    let named_fields = match input.data {
271        Data::Struct(struct_data) => match struct_data.fields {
272            Fields::Named(named_fields) => named_fields,
273            Fields::Unnamed(_) => panic!("tuple structs are not supported, use named fields"),
274            Fields::Unit => panic!("unit structs are not supported, use curly braces"),
275        },
276        Data::Enum(_) => panic!("enums are not supported, use a struct"),
277        Data::Union(_) => panic!("unions are not supported, use a struct"),
278    };
279    let sorted = input.attrs.iter().any(is_sorted);
280
281    // Get the list of all cvars and their types
282    let mut skips = Vec::new();
283    let mut names = Vec::new();
284    let mut tys = Vec::new();
285    for field in named_fields.named {
286        let contains_skip = field.attrs.iter().any(is_skip);
287        skips.push(contains_skip);
288        let name = field.ident.expect("unreachable: ident was None");
289        names.push(name);
290        tys.push(field.ty);
291    }
292
293    let expanded = generate(struct_name, sorted, &skips, &names, &tys);
294    let expanded = expanded.into();
295
296    let end = std::time::Instant::now();
297    if env::var("CVARS_STATS").is_ok() {
298        eprintln!("derive(SetGet) took {:?}", end - begin);
299    }
300
301    expanded
302}
303
304fn generate(
305    struct_name: Ident,
306    sorted: bool,
307    skips: &[bool],
308    names_all: &[Ident],
309    tys_all: &[Type],
310) -> proc_macro2::TokenStream {
311    let mut names = Vec::new();
312    let mut tys = Vec::new();
313    for i in 0..skips.len() {
314        if skips[i] {
315            continue;
316        }
317
318        names.push(&names_all[i]);
319        tys.push(&tys_all[i]);
320    }
321
322    if sorted {
323        for pair in names.windows(2) {
324            if pair[0] >= pair[1] {
325                // LATER A warning would make much more sense but it requires nightly for now:
326                // https://github.com/rust-lang/rust/issues/54140
327                panic!("cvars not sorted: `{}` >= `{}`", pair[0], pair[1]);
328            }
329        }
330    }
331
332    let cvar_count = names.len();
333
334    let set_get_impl = impl_set_get(&struct_name);
335
336    // Get the set of types used as cvars.
337    // We need to impl SetGetType for them and it needs to be done
338    // once per type, not once per cvar.
339    // Note: I benchmarked this against FnvHashSet and it doesn't make a difference.
340    let unique_tys: HashSet<_> = tys.iter().collect();
341    let mut trait_impls = Vec::new();
342    for unique_ty in unique_tys {
343        let mut getter_arms = Vec::new();
344        let mut setter_arms = Vec::new();
345
346        for i in 0..names.len() {
347            let field = names[i];
348            let ty = tys[i];
349            // Each `impl SetGetType for X` block only generates match arms for cvars of type X
350            // so that the getters and setters typecheck.
351            if ty == *unique_ty {
352                let getter_arm = quote! {
353                    stringify!(#field) => ::core::result::Result::Ok(cvars.#field.clone()),
354                };
355                getter_arms.push(getter_arm);
356
357                let setter_arm = quote! {
358                    stringify!(#field) => ::core::result::Result::Ok(cvars.#field = value),
359                };
360                setter_arms.push(setter_arm);
361            }
362        }
363
364        // LATER Is there a sane way to automatically convert? (even fallibly)
365        //       e.g. integers default to i32 even though cvar type is usize
366        //       At the very least, it should suggest specifying the type.
367        let trait_impl = quote! {
368            #[automatically_derived]
369            impl SetGetType for #unique_ty {
370                fn get(cvars: &Cvars, cvar_name: &str) -> ::core::result::Result<Self, String> {
371                    match cvar_name {
372                        #( #getter_arms )*
373                        _ => ::core::result::Result::Err(format!(
374                            "Cvar named {} with type {} not found",
375                            cvar_name,
376                            stringify!(#unique_ty)
377                        )),
378                    }
379                }
380
381                fn set(cvars: &mut Cvars, cvar_name: &str, value: Self) -> ::core::result::Result<(), String> {
382                    match cvar_name {
383                        #( #setter_arms )*
384                        _ => ::core::result::Result::Err(format!(
385                            "Cvar named {} with type {} not found",
386                            cvar_name,
387                            stringify!(#unique_ty),
388                        )),
389                    }
390                }
391            }
392        };
393        trait_impls.push(trait_impl);
394    }
395
396    quote! {
397        #[automatically_derived]
398        impl #struct_name {
399            /// Finds the cvar whose name matches `cvar_name` and returns its value.
400            ///
401            /// Returns `Err` if the cvar doesn't exist.
402            pub fn get<T: SetGetType>(&self, cvar_name: &str) -> ::core::result::Result<T, String> {
403                // We can't generate all the match arms here because we don't know what concrete type T is.
404                // Instead, we statically dispatch it through the SetGetType trait and then only look up
405                // fields of the correct type in each impl block.
406                SetGetType::get(self, cvar_name)
407            }
408
409            /// Finds the cvar whose name matches `cvar_name` and returns its value as a `String`.
410            ///
411            /// Returns `Err` if the cvar doesn't exist.
412            pub fn get_string(&self, cvar_name: &str) -> ::core::result::Result<String, String> {
413                // This doesn't need to be dispatched via SetGetType, it uses Display instead.
414
415                // Separate function - see set_str for why.
416                #[inline(never)]
417                fn get_string<T: ::core::fmt::Display>(cvar: &T) -> ::core::result::Result<String, String> {
418                    ::core::result::Result::Ok(cvar.to_string())
419                }
420                match cvar_name {
421                    #( stringify!(#names) => get_string(&self.#names), )*
422                    _ => ::core::result::Result::Err(format!(
423                        "Cvar named {} not found",
424                        cvar_name,
425                    )),
426                }
427            }
428
429            /// Finds the cvar whose name matches `cvar_name` and sets it to `value`.
430            ///
431            /// Returns `Err` if the cvar doesn't exist or its type doesn't match that of `value`.
432            ///
433            /// **Note**: Rust can't infer the type of `value` based on the type of the cvar
434            /// because `cvar_name` is resolved to the right struct field only at runtime.
435            /// This means integer literals default to `i32` and float literals to `f64`.
436            /// This in turn means if the cvar's type is e.g. usize and you try
437            /// `cvars.set("sometihng_with_type_usize", 123);`, it will fail
438            /// because at compile time, `123` is inferred to be `i32`.
439            /// Use `123_usize` to specify the correct type.
440            ///
441            /// This limitation doesn't apply to `set_str` since it determines which type to parse to
442            /// *after* looking up the right field at runtime.
443            pub fn set<T: SetGetType>(&mut self, cvar_name: &str, value: T) -> ::core::result::Result<(), String> {
444                SetGetType::set(self, cvar_name, value)
445            }
446
447            /// Finds the cvar whose name matches `cvar_name`, tries to parse `str_value` to its type and sets it to the parsed value.
448            ///
449            /// Returns `Err` if the cvar doesn't exist or if `str_value` fails to parse to its type.
450            pub fn set_str(&mut self, cvar_name: &str, str_value: &str) -> ::core::result::Result<(), String> {
451                // This doesn't need to be dispatched via SetGetType, it uses FromStr instead.
452
453                // Put most of the logic in a separate function
454                // so that the repeated code is only a single function call.
455                // This roughly halves incremental compilation time
456                // when the Cvars struct is modified for 1k cvars.
457                #[inline(never)]
458                fn set_str<T>(cvar: &mut T, mut str_value: &str) -> ::core::result::Result<(), String>
459                where
460                    T: ::core::str::FromStr,
461                    T::Err: ::core::fmt::Display,
462                {
463                    if ::std::any::type_name::<T>() == "bool" {
464                        if str_value == "t" || str_value == "1" {
465                            str_value = "true";
466                        } else if str_value == "f" || str_value == "0" {
467                            str_value = "false";
468                        }
469                    }
470                    match str_value.parse() {
471                        ::core::result::Result::Ok(val) => ::core::result::Result::Ok(*cvar = val),
472                        ::core::result::Result::Err(err) => ::core::result::Result::Err(format!("failed to parse {} as type {}: {}",
473                            str_value,
474                            ::std::any::type_name::<T>(),
475                            err,
476                        ))
477                    }
478                }
479                match cvar_name {
480                    #( stringify!(#names) => set_str(&mut self.#names, str_value), )*
481                    _ => ::core::result::Result::Err(format!(
482                        "Cvar named {} not found",
483                        cvar_name
484                    )),
485                }
486            }
487
488            /// Returns the number of cvars.
489            pub fn cvar_count(&self) -> usize {
490                #cvar_count
491            }
492
493            /// The number of cvars.
494            pub const CVAR_COUNT: usize = #cvar_count;
495        }
496
497        #set_get_impl
498
499        /// This trait is needed to dispatch cvar get/set based on its type.
500        /// You're not meant to impl it yourself, it's done automatically
501        /// for all types used as cvars.
502        pub trait SetGetType {
503            fn get(cvars: &Cvars, cvar_name: &str) -> ::core::result::Result<Self, String>
504                where Self: Sized;
505            fn set(cvars: &mut Cvars, cvar_name: &str, value: Self) -> ::core::result::Result<(), String>;
506        }
507
508        #( #trait_impls )*
509    }
510}
511
512/// Dummy version of SetGet for debugging how much cvars add to _incremental_ compile times of your project.
513///
514/// Generates the 4 setters and getters like SetGet but they contain only `unimplemented!()`,
515/// therefore the code to dispatch from string to struct field is not generated.
516/// This exists only to test how using SetGet affects the total compile time of downstream crates.
517/// Simply replace SetGet with SetGetDummy and compare how long `cargo build` takes.
518/// The resulting code should compile but will crash if the generated methods are used.
519#[doc(hidden)]
520#[proc_macro_derive(SetGetDummy, attributes(cvars))]
521pub fn derive_dummy(input: TokenStream) -> TokenStream {
522    let begin = std::time::Instant::now();
523
524    let input: DeriveInput = parse_macro_input!(input);
525    let struct_name = input.ident;
526    let set_get_impl = impl_set_get(&struct_name);
527
528    let expanded = quote! {
529        #[automatically_derived]
530        impl #struct_name {
531            pub fn get<T>(&self, cvar_name: &str) -> ::core::result::Result<T, String> {
532                unimplemented!("SetGetDummy is only for compile time testing.");
533            }
534            pub fn get_string(&self, cvar_name: &str) -> ::core::result::Result<String, String> {
535                unimplemented!("SetGetDummy is only for compile time testing.");
536            }
537            pub fn set<T>(&mut self, cvar_name: &str, value: T) -> ::core::result::Result<(), String> {
538                unimplemented!("SetGetDummy is only for compile time testing.");
539            }
540            pub fn set_str(&mut self, cvar_name: &str, str_value: &str) -> ::core::result::Result<(), String> {
541                unimplemented!("SetGetDummy is only for compile time testing.");
542            }
543            pub fn cvar_count(&self) -> usize {
544                unimplemented!("SetGetDummy is only for compile time testing.");
545            }
546        }
547
548        #set_get_impl
549    };
550    let expanded = TokenStream::from(expanded);
551
552    let end = std::time::Instant::now();
553    if env::var("CVARS_STATS").is_ok() {
554        eprintln!("derive(SetGetDummy) took {:?}", end - begin);
555    }
556
557    expanded
558}
559
560fn impl_set_get(struct_name: &Ident) -> proc_macro2::TokenStream {
561    quote! {
562        #[automatically_derived]
563        impl ::cvars::SetGet for #struct_name {
564            fn get_string(&self, cvar_name: &str) -> ::core::result::Result<String, String> {
565                self.get_string(cvar_name)
566            }
567
568            fn set_str(&mut self, cvar_name: &str, cvar_value: &str) -> ::core::result::Result<(), String> {
569                self.set_str(cvar_name, cvar_value)
570            }
571
572            fn cvar_count(&self) -> usize {
573                self.cvar_count()
574            }
575        }
576    }
577}