msfs_derive/
lib.rs

1extern crate proc_macro;
2use proc_macro::TokenStream;
3use quote::{format_ident, quote};
4use std::collections::HashMap;
5use syn::{
6    Expr, Ident, ItemFn, ItemStruct, Lit, Meta, Token, Type,
7    parse::{Parse, ParseStream, Result as SynResult},
8    parse_macro_input,
9};
10
11/// Declare a standalone module.
12/// ```rs
13/// #[standalone_module]
14/// async fn module(mut module: msfs::StandaloneModule) -> Result<(), Box<dyn std::error::Error>> {
15///   while let Some(event) = module.next_event().await {
16///     // ...
17///   }
18/// }
19/// ```
20#[proc_macro_attribute]
21pub fn standalone_module(_args: TokenStream, item: TokenStream) -> TokenStream {
22    let input = parse_macro_input!(item as ItemFn);
23
24    let rusty_name = input.sig.ident.clone();
25    let executor_name = format_ident!(
26        "{}_executor_do_not_use_or_you_will_be_fired",
27        input.sig.ident
28    );
29
30    let output = quote! {
31        #input
32
33        // SAFETY: it is safe to create references of this static since all WASM modules are single threaded
34        // and there is only 1 reference in use at all times
35        #[allow(non_upper_case_globals)]
36        static mut #executor_name: ::msfs::StandaloneModuleExecutor = ::msfs::StandaloneModuleExecutor {
37            executor: ::msfs::executor::Executor {
38                handle: |m| std::boxed::Box::pin(#rusty_name(m)),
39                future: None,
40                tx: None,
41            },
42        };
43
44        #[unsafe(no_mangle)]
45        pub extern "C" fn module_init() {
46            unsafe {
47                ::msfs::wrap_executor(&raw mut #executor_name, |e| e.handle_init());
48            }
49        }
50
51        #[unsafe(no_mangle)]
52        pub extern "C" fn module_deinit() {
53            unsafe {
54                ::msfs::wrap_executor(&raw mut #executor_name, |e| e.handle_deinit());
55            }
56        }
57    };
58
59    TokenStream::from(output)
60}
61
62struct GaugeArgs {
63    name: Option<String>,
64}
65
66impl Parse for GaugeArgs {
67    fn parse(input: ParseStream) -> SynResult<Self> {
68        match input.parse::<Ident>() {
69            Ok(i) if i == "name" => {
70                input.parse::<Token![=]>()?;
71                Ok(GaugeArgs {
72                    name: Some(input.parse::<Ident>()?.to_string()),
73                })
74            }
75            _ => Ok(GaugeArgs { name: None }),
76        }
77    }
78}
79
80/// Declare a gauge callback. It will be automatically exported with the name
81/// `NAME_gauge_callback`, where `NAME` is the name of the decorated function.
82/// ```rs
83/// use futures::stream::{Stream, StreamExt};
84/// // Declare and export `FOO_gauge_callback`
85/// #[msfs::gauge]
86/// async fn FOO(mut gauge: msfs::Gauge) -> Result<(), Box<dyn std::error::Error>> {
87///   while let Some(event) = gauge.next_event().await {
88///     // ...
89///   }
90/// }
91/// ```
92///
93/// The macro can also be given a parameter, `name`, to rename the exported function.
94/// ```rs
95/// // Declare and export `FOO_gauge_callback`
96/// #[msfs::gauge(name=FOO)]
97/// async fn xyz(...) {}
98#[proc_macro_attribute]
99pub fn gauge(args: TokenStream, item: TokenStream) -> TokenStream {
100    let args = parse_macro_input!(args as GaugeArgs);
101    let input = parse_macro_input!(item as ItemFn);
102
103    let rusty_name = input.sig.ident.clone();
104    let executor_name = format_ident!(
105        "{}_executor_do_not_use_or_you_will_be_fired",
106        input.sig.ident
107    );
108
109    let extern_name = args.name.unwrap_or_else(|| input.sig.ident.to_string());
110    let extern_gauge_name = format_ident!("{}_gauge_callback", extern_name);
111    let extern_mouse_name = format_ident!("{}_mouse_callback", extern_name);
112
113    let output = quote! {
114        #input
115
116        // SAFETY: it is safe to create references of this static since all WASM modules are single threaded
117        // and there is only 1 reference in use at all times
118        #[allow(non_upper_case_globals)]
119        static mut #executor_name: ::msfs::GaugeExecutor = ::msfs::GaugeExecutor {
120            fs_ctx: None,
121            executor: ::msfs::executor::Executor {
122                handle: |gauge| std::boxed::Box::pin(#rusty_name(gauge)),
123                tx: None,
124                future: None,
125            },
126        };
127
128        #[doc(hidden)]
129        #[unsafe(no_mangle)]
130        pub extern "C" fn #extern_gauge_name(
131            ctx: ::msfs::sys::FsContext,
132            service_id: std::os::raw::c_int,
133            p_data: *mut std::os::raw::c_void,
134        ) -> bool {
135            unsafe {
136                ::msfs::wrap_executor(&raw mut #executor_name, |e| e.handle_gauge(ctx, service_id, p_data))
137            }
138        }
139
140        #[doc(hidden)]
141        #[unsafe(no_mangle)]
142        pub extern "C" fn #extern_mouse_name(
143            fx: std::os::raw::c_float,
144            fy: std::os::raw::c_float,
145            i_flags: std::os::raw::c_uint,
146        ) {
147            unsafe {
148               ::msfs::wrap_executor(&raw mut #executor_name, |e| e.handle_mouse(fx, fy, i_flags));
149            }
150         }
151    };
152
153    TokenStream::from(output)
154}
155
156fn parse_struct_fields(
157    input: &mut ItemStruct,
158    attributes: &[&str],
159    get_type: Option<fn(&str) -> &str>,
160) -> Vec<HashMap<String, String>> {
161    let mut data = Vec::new();
162
163    for (i, field) in &mut input.fields.iter_mut().enumerate() {
164        let mut meta = HashMap::new();
165
166        meta.insert(
167            "field_name".to_string(),
168            if let Some(ident) = &field.ident {
169                ident.to_string()
170            } else {
171                i.to_string()
172            },
173        );
174
175        if let Some(get_type) = get_type {
176            let ty = match &field.ty {
177                Type::Path(p) => p.path.get_ident().unwrap().to_string(),
178                _ => panic!("Unsupported type"),
179            };
180
181            meta.insert("type".to_string(), get_type(ty.as_str()).to_string());
182        } else {
183            let t = &field.ty;
184            meta.insert("type".to_string(), quote!(#t).to_string());
185        }
186
187        let mut attrs = Vec::new();
188        for a in &field.attrs {
189            let simish = if let Some(i) = a.path().get_ident() {
190                attributes.contains(&i.to_string().as_str())
191            } else {
192                false
193            };
194            if simish {
195                let (name, value) = match &a.meta {
196                    Meta::NameValue(mnv) => {
197                        let name = mnv.path.get_ident().unwrap().to_string();
198                        let value = match &mnv.value {
199                            Expr::Lit(l) => match &l.lit {
200                                Lit::Str(s) => s.value(),
201                                Lit::Float(f) => f.base10_digits().to_string(),
202                                _ => panic!("argument must be a string or float"),
203                            },
204                            _ => panic!("argument must be a string or float"),
205                        };
206                        (name, value)
207                    }
208                    _ => panic!("attribute must be in for #[name = \"value\"]"),
209                };
210
211                meta.insert(name, value);
212            } else {
213                attrs.push(a.clone());
214            }
215        }
216        field.attrs = attrs;
217
218        data.push(meta);
219    }
220
221    data
222}
223
224/// Generate a struct which can be used with SimConnect's data definitions.
225/// ```rs
226/// #[sim_connect::data_definition]
227/// struct ControlSurfaces {
228///     #[name = "ELEVATOR POSITION"]
229///     #[unit = "Position"]
230///     elevator: f64,
231///     #[name = "AILERON POSITION"]
232///     #[unit = "Position"]
233///     ailerons: f64,
234///     #[name = "RUDDER POSITION"]
235///     #[unit = "Position"]
236///     rudder: f64,
237/// }
238///
239/// sim.add_data_definition::<ControlSurfaces>();
240/// ```
241#[proc_macro_attribute]
242pub fn sim_connect_data_definition(_args: TokenStream, item: TokenStream) -> TokenStream {
243    let mut input = parse_macro_input!(item as ItemStruct);
244    let name = input.ident.clone();
245
246    let data = parse_struct_fields(
247        &mut input,
248        &["name", "unit", "epsilon"],
249        Some(|ty| match ty {
250            "bool" => "INT32",
251            "i32" => "INT32",
252            "i64" => "INT64",
253            "f32" => "FLOAT32",
254            "f64" => "FLOAT64",
255            "DataXYZ" => "XYZ",
256            _ => panic!("Unsupported type {}", ty),
257        }),
258    );
259
260    let mut array = String::from("&[\n");
261    for meta in data {
262        let name = meta["name"].clone();
263        let unit = meta
264            .get("unit")
265            .unwrap_or_else(|| panic!("{} needs a #[unit] decorator", name));
266
267        let fallback = "0.0".to_string();
268        let epsilon = meta.get("epsilon").unwrap_or(&fallback);
269
270        let ty = meta["type"].clone();
271        array += &format!(
272            "  ({name:?}, {unit:?}, {epsilon}, ::msfs::sys::SIMCONNECT_DATATYPE_SIMCONNECT_DATATYPE_{ty}),\n"
273        );
274    }
275    array += "]";
276    let array = syn::parse_str::<Expr>(&array).unwrap();
277
278    let output = quote! {
279        #[repr(C)]
280        #input
281
282        impl ::msfs::sim_connect::DataDefinition for #name {
283            const DEFINITIONS: &'static [(&'static str, &'static str, f32, ::msfs::sys::SIMCONNECT_DATATYPE)] = #array;
284        }
285    };
286
287    TokenStream::from(output)
288}
289
290/// Generate a struct which can be used with SimConnect's client data definitions.
291/// ```rs
292/// #[sim_connect::client_data_definition]
293/// struct SomeData {
294///     foo: u8,
295///     bar: f64,
296///     #[epsilon = 0.5]
297///     baz: i8,
298/// }
299/// ```
300#[proc_macro_attribute]
301pub fn sim_connect_client_data_definition(_args: TokenStream, item: TokenStream) -> TokenStream {
302    let mut input = parse_macro_input!(item as ItemStruct);
303    let name = input.ident.clone();
304
305    let data = parse_struct_fields(&mut input, &["epsilon"], None);
306
307    let mut array = String::from("vec![\n");
308
309    for meta in data {
310        let fallback = "0.0".to_string();
311        let epsilon = meta.get("epsilon").unwrap_or(&fallback);
312
313        array += &format!(
314            "    (unsafe {{
315                     let uninit = std::mem::MaybeUninit::<{struct_name}>::uninit();
316                     let base = uninit.as_ptr() as *const {struct_name};
317                     let field = &((*base).{field_name}) as *const _;
318                     (field as usize) - (base as usize)
319                 }}, std::mem::size_of::<{type}>(), {epsilon}),
320            ",
321            struct_name=name, field_name=meta["field_name"], type=meta["type"], epsilon=epsilon,
322        );
323    }
324
325    array += "]";
326
327    let array = syn::parse_str::<Expr>(&array).unwrap();
328    let output = quote! {
329        #input
330
331        impl ::msfs::sim_connect::ClientDataDefinition for #name {
332            fn get_definitions() -> Vec<(usize, usize, f32)> { #array }
333        }
334    };
335
336    TokenStream::from(output)
337}