Skip to main content

spacetimedb_bindings_macro/
lib.rs

1//! Defines procedural macros like `#[spacetimedb::table]`,
2//! simplifying writing SpacetimeDB modules in Rust.
3
4// DO NOT WRITE (public) DOCS IN THIS MODULE.
5// Docs should be written in the `spacetimedb` crate (i.e. `bindings/`) at reexport sites
6// using `#[doc(inline)]`.
7// We do this so that links to library traits, structs, etc can resolve correctly.
8//
9// (private documentation for the macro authors is totally fine here and you SHOULD write that!)
10
11mod http;
12mod procedure;
13
14#[proc_macro_attribute]
15pub fn procedure(args: StdTokenStream, item: StdTokenStream) -> StdTokenStream {
16    cvt_attr::<ItemFn>(args, item, quote!(), |args, original_function| {
17        let args = procedure::ProcedureArgs::parse(args)?;
18        procedure::procedure_impl(args, original_function)
19    })
20}
21
22#[proc_macro_attribute]
23pub fn http_handler(args: StdTokenStream, item: StdTokenStream) -> StdTokenStream {
24    ok_or_compile_error(|| {
25        let item_ts: TokenStream = item.into();
26        let original_function: ItemFn = syn::parse2(item_ts)?;
27        http::handler_impl(args.into(), &original_function)
28    })
29}
30
31#[proc_macro_attribute]
32pub fn http_router(args: StdTokenStream, item: StdTokenStream) -> StdTokenStream {
33    ok_or_compile_error(|| {
34        let item_ts: TokenStream = item.into();
35        let original_function: ItemFn = syn::parse2(item_ts)?;
36        http::router_impl(args.into(), &original_function)
37    })
38}
39mod reducer;
40
41#[proc_macro_attribute]
42pub fn reducer(args: StdTokenStream, item: StdTokenStream) -> StdTokenStream {
43    cvt_attr::<ItemFn>(args, item, quote!(), |args, original_function| {
44        let args = reducer::ReducerArgs::parse(args)?;
45        reducer::reducer_impl(args, original_function)
46    })
47}
48mod sats;
49mod table;
50
51#[proc_macro_attribute]
52pub fn table(args: StdTokenStream, item: StdTokenStream) -> StdTokenStream {
53    // put this on the struct so we don't get unknown attribute errors
54    let derive_table_helper: syn::Attribute = derive_table_helper_attr();
55
56    ok_or_compile_error(|| {
57        let item = TokenStream::from(item);
58        let mut derive_input: syn::DeriveInput = syn::parse2(item.clone())?;
59
60        // Add `derive(__TableHelper)` only if it's not already in the attributes of the `derive_input.`
61        // If multiple `#[table]` attributes are applied to the same `struct` item,
62        // this will ensure that we don't emit multiple conflicting implementations
63        // for traits like `SpacetimeType`, `Serialize` and `Deserialize`.
64        //
65        // We need to push at the end, rather than the beginning,
66        // because rustc expands attribute macros (including derives) top-to-bottom,
67        // and we need *all* `#[table]` attributes *before* the `derive(__TableHelper)`.
68        // This way, the first `table` will insert a `derive(__TableHelper)`,
69        // and all subsequent `#[table]`s on the same `struct` will see it,
70        // and not add another.
71        //
72        // Note, thank goodness, that `syn`'s `PartialEq` impls (provided with the `extra-traits` feature)
73        // skip any [`Span`]s contained in the items,
74        // thereby comparing for syntactic rather than structural equality. This shouldn't matter,
75        // since we expect that the `derive_table_helper` will always have the same [`Span`]s,
76        // but it's nice to know.
77        if !derive_input.attrs.contains(&derive_table_helper) {
78            derive_input.attrs.push(derive_table_helper);
79        }
80
81        let args = table::TableArgs::parse(args.into(), &derive_input.ident)?;
82        let generated = table::table_impl(args, &derive_input)?;
83        Ok(TokenStream::from_iter([quote!(#derive_input), generated]))
84    })
85}
86mod util;
87mod view;
88
89#[proc_macro_attribute]
90pub fn view(args: StdTokenStream, item: StdTokenStream) -> StdTokenStream {
91    let item_ts: TokenStream = item.into();
92    let original_function = match syn::parse2::<ItemFn>(item_ts.clone()) {
93        Ok(f) => f,
94        Err(e) => return TokenStream::from_iter([item_ts, e.into_compile_error()]).into(),
95    };
96    let args = match view::ViewArgs::parse(args.into(), &original_function.sig.ident) {
97        Ok(a) => a,
98        Err(e) => return TokenStream::from_iter([item_ts, e.into_compile_error()]).into(),
99    };
100    match view::view_impl(args, &original_function) {
101        Ok(ts) => ts.into(),
102        Err(e) => TokenStream::from_iter([item_ts, e.into_compile_error()]).into(),
103    }
104}
105
106use proc_macro::TokenStream as StdTokenStream;
107use proc_macro2::TokenStream;
108use quote::quote;
109use std::time::Duration;
110use syn::{parse::ParseStream, Attribute};
111use syn::{ItemConst, ItemFn};
112use util::{cvt_attr, ok_or_compile_error};
113
114mod sym {
115    /// A symbol known at compile-time against
116    /// which identifiers and paths may be matched.
117    pub struct Symbol(&'static str);
118
119    macro_rules! symbol {
120        ($ident:ident) => {
121            symbol!($ident, $ident);
122        };
123        ($const:ident, $ident:ident) => {
124            #[allow(non_upper_case_globals)]
125            #[doc = concat!("Matches `", stringify!($ident), "`.")]
126            pub const $const: Symbol = Symbol(stringify!($ident));
127        };
128    }
129
130    symbol!(accessor);
131    symbol!(at);
132    symbol!(auto_inc);
133    symbol!(btree);
134    symbol!(client_connected);
135    symbol!(client_disconnected);
136    symbol!(column);
137    symbol!(columns);
138    symbol!(crate_, crate);
139    symbol!(direct);
140    symbol!(hash);
141    symbol!(index);
142    symbol!(init);
143    symbol!(name);
144    symbol!(primary_key);
145    symbol!(private);
146    symbol!(public);
147    symbol!(repr);
148    symbol!(sats);
149    symbol!(scheduled);
150    symbol!(unique);
151    symbol!(update);
152    symbol!(default);
153    symbol!(event);
154
155    symbol!(u8);
156    symbol!(i8);
157    symbol!(u16);
158    symbol!(i16);
159    symbol!(u32);
160    symbol!(i32);
161    symbol!(u64);
162    symbol!(i64);
163    symbol!(u128);
164    symbol!(i128);
165    symbol!(f32);
166    symbol!(f64);
167
168    impl PartialEq<Symbol> for syn::Ident {
169        fn eq(&self, sym: &Symbol) -> bool {
170            self == sym.0
171        }
172    }
173    impl PartialEq<Symbol> for &syn::Ident {
174        fn eq(&self, sym: &Symbol) -> bool {
175            *self == sym.0
176        }
177    }
178    impl PartialEq<Symbol> for syn::Path {
179        fn eq(&self, sym: &Symbol) -> bool {
180            self.is_ident(sym)
181        }
182    }
183    impl PartialEq<Symbol> for &syn::Path {
184        fn eq(&self, sym: &Symbol) -> bool {
185            self.is_ident(sym)
186        }
187    }
188    impl std::fmt::Display for Symbol {
189        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
190            f.write_str(self.0)
191        }
192    }
193    impl std::borrow::Borrow<str> for Symbol {
194        fn borrow(&self) -> &str {
195            self.0
196        }
197    }
198}
199
200/// It turns out to be shockingly difficult to construct an [`Attribute`].
201/// That type is not [`Parse`], instead having two distinct methods
202/// for parsing "inner" vs "outer" attributes.
203///
204/// We need this [`Attribute`] in [`table`] so that we can "pushnew" it
205/// onto the end of a list of attributes. See comments within [`table`].
206fn derive_table_helper_attr() -> Attribute {
207    let source = quote!(#[derive(spacetimedb::__TableHelper)]);
208
209    syn::parse::Parser::parse2(Attribute::parse_outer, source)
210        .unwrap()
211        .into_iter()
212        .next()
213        .unwrap()
214}
215
216/// Special alias for `derive(SpacetimeType)`, aka [`schema_type`], for use by [`table`].
217///
218/// Provides helper attributes for `#[spacetimedb::table]`, so that we don't get unknown attribute errors.
219#[doc(hidden)]
220#[proc_macro_derive(__TableHelper, attributes(sats, unique, auto_inc, primary_key, index, default))]
221pub fn table_helper(input: StdTokenStream) -> StdTokenStream {
222    schema_type(input)
223}
224
225#[proc_macro]
226pub fn duration(input: StdTokenStream) -> StdTokenStream {
227    let dur = syn::parse_macro_input!(input with parse_duration);
228    let (secs, nanos) = (dur.as_secs(), dur.subsec_nanos());
229    quote!({
230        const DUR: ::core::time::Duration = ::core::time::Duration::new(#secs, #nanos);
231        DUR
232    })
233    .into()
234}
235
236fn parse_duration(input: ParseStream) -> syn::Result<Duration> {
237    let lookahead = input.lookahead1();
238    let (s, span) = if lookahead.peek(syn::LitStr) {
239        let s = input.parse::<syn::LitStr>()?;
240        (s.value(), s.span())
241    } else if lookahead.peek(syn::LitInt) {
242        let i = input.parse::<syn::LitInt>()?;
243        (i.to_string(), i.span())
244    } else {
245        return Err(lookahead.error());
246    };
247    humantime::parse_duration(&s).map_err(|e| syn::Error::new(span, format_args!("can't parse as duration: {e}")))
248}
249
250/// A helper for the common bits of the derive macros.
251fn sats_derive(
252    input: StdTokenStream,
253    assume_in_module: bool,
254    logic: impl FnOnce(&sats::SatsType) -> TokenStream,
255) -> StdTokenStream {
256    let input = syn::parse_macro_input!(input as syn::DeriveInput);
257    let crate_fallback = if assume_in_module {
258        quote!(spacetimedb::spacetimedb_lib)
259    } else {
260        quote!(spacetimedb_lib)
261    };
262    sats::sats_type_from_derive(&input, crate_fallback)
263        .map(|ty| logic(&ty))
264        .unwrap_or_else(syn::Error::into_compile_error)
265        .into()
266}
267
268#[proc_macro_derive(Deserialize, attributes(sats))]
269pub fn deserialize(input: StdTokenStream) -> StdTokenStream {
270    sats_derive(input, false, sats::derive_deserialize)
271}
272
273#[proc_macro_derive(Serialize, attributes(sats))]
274pub fn serialize(input: StdTokenStream) -> StdTokenStream {
275    sats_derive(input, false, sats::derive_serialize)
276}
277
278#[proc_macro_derive(SpacetimeType, attributes(sats))]
279pub fn schema_type(input: StdTokenStream) -> StdTokenStream {
280    sats_derive(input, true, |ty| {
281        let ident = ty.ident;
282        let name = &ty.name;
283
284        let krate = &ty.krate;
285        TokenStream::from_iter([
286            sats::derive_satstype(ty),
287            sats::derive_deserialize(ty),
288            sats::derive_serialize(ty),
289            // unfortunately, generic types don't work in modules at the moment.
290            quote!(#krate::__make_register_reftype!(#ident, #name);),
291        ])
292    })
293}
294
295#[proc_macro_attribute]
296pub fn client_visibility_filter(args: StdTokenStream, item: StdTokenStream) -> StdTokenStream {
297    ok_or_compile_error(|| {
298        if !args.is_empty() {
299            return Err(syn::Error::new_spanned(
300                TokenStream::from(args),
301                "The `client_visibility_filter` attribute does not accept arguments",
302            ));
303        }
304
305        let item: ItemConst = syn::parse(item)?;
306        let rls_ident = item.ident.clone();
307        let register_rls_symbol = format!("__preinit__20_register_row_level_security_{rls_ident}");
308
309        Ok(quote! {
310            #item
311
312            const _: () = {
313                #[unsafe(export_name = #register_rls_symbol)]
314                extern "C" fn __register_client_visibility_filter() {
315                    spacetimedb::rt::register_row_level_security(#rls_ident.sql_text())
316                }
317            };
318        })
319    })
320}
321
322/// Known setting names and their registration code generators.
323const KNOWN_SETTINGS: &[&str] = &["CASE_CONVERSION_POLICY"];
324
325#[proc_macro_attribute]
326pub fn settings(args: StdTokenStream, item: StdTokenStream) -> StdTokenStream {
327    ok_or_compile_error(|| {
328        if !args.is_empty() {
329            return Err(syn::Error::new_spanned(
330                TokenStream::from(args),
331                "The `settings` attribute does not accept arguments",
332            ));
333        }
334
335        let item: ItemConst = syn::parse(item)?;
336        let ident = &item.ident;
337        let ident_str = ident.to_string();
338
339        if !KNOWN_SETTINGS.contains(&ident_str.as_str()) {
340            return Err(syn::Error::new_spanned(
341                ident,
342                format!(
343                    "unknown setting `{ident_str}`. Known settings: {}",
344                    KNOWN_SETTINGS.join(", ")
345                ),
346            ));
347        }
348
349        // Use a fixed export name so that two `#[spacetimedb::settings]` consts
350        // for the same setting produce a linker error (duplicate symbol).
351        let register_symbol = format!("__preinit__05_setting_{ident_str}");
352
353        // Generate the registration call based on the setting name.
354        let register_call = match ident_str.as_str() {
355            "CASE_CONVERSION_POLICY" => quote! {
356                spacetimedb::rt::register_case_conversion_policy(#ident)
357            },
358            _ => unreachable!("validated above"),
359        };
360
361        Ok(quote! {
362            #item
363
364            const _: () = {
365                #[unsafe(export_name = #register_symbol)]
366                extern "C" fn __register_setting() {
367                    #register_call
368                }
369            };
370        })
371    })
372}