Skip to main content

sdl3_main_macros/
lib.rs

1#![doc = include_str!("../README.md")]
2
3use parse::{Error, Function, Generic, ImplBlock, IntoTokenTrees, Item, Parse, Type, Visibility};
4use proc_macro::{Delimiter, Group, Ident, Punct, Spacing, Span, TokenStream, TokenTree};
5
6const SDL3_MAIN: &str = "sdl3_main";
7
8macro_rules! input {
9    ($input:expr) => {
10        &mut { &$crate::parse::into_input($input) as &[::proc_macro::TokenTree] }
11    };
12}
13
14macro_rules! miniquote_to {
15    ($out:expr =>) => {};
16
17    ($out:expr => #{$expr:expr} $($rest:tt)*) => {{
18        $expr.into_token_trees($out);
19        miniquote_to!($out => $($rest)*);
20    }};
21
22    ($out:expr => #$ident:ident $($rest:tt)*) => {{
23        $ident.into_token_trees($out);
24        miniquote_to!($out => $($rest)*);
25    }};
26
27    ($out:expr => ($($group:tt)*) $($rest:tt)*) => {{
28        $out.extend([TokenTree::Group(Group::new(Delimiter::Parenthesis, miniquote!($($group)*)))]);
29        miniquote_to!($out => $($rest)*);
30    }};
31
32    ($out:expr => [$($group:tt)*] $($rest:tt)*) => {{
33        $out.extend([TokenTree::Group(Group::new(Delimiter::Bracket, miniquote!($($group)*)))]);
34        miniquote_to!($out => $($rest)*);
35    }};
36
37    ($out:expr => {$($group:tt)*} $($rest:tt)*) => {{
38        $out.extend([TokenTree::Group(Group::new(Delimiter::Brace, miniquote!($($group)*)))]);
39        miniquote_to!($out => $($rest)*);
40    }};
41
42    ($out:expr => # $($rest:tt)*) => {{
43        $out.extend($crate::op1('#'));
44        miniquote_to!($out => $($rest)*);
45    }};
46
47    ($out:expr => != $($rest:tt)*) => {
48        $out.extend($crate::op2('!', '='));
49        miniquote_to!($out => $($rest)*);
50    };
51
52    ($out:expr => ! $($rest:tt)*) => {{
53        $out.extend($crate::op1('!'));
54        miniquote_to!($out => $($rest)*);
55    }};
56
57    ($out:expr => == $($rest:tt)*) => {{
58        $out.extend($crate::op2('=', '='));
59        miniquote_to!($out => $($rest)*);
60    }};
61
62    ($out:expr => => $($rest:tt)*) => {{
63        $out.extend($crate::op2('=', '>'));
64        miniquote_to!($out => $($rest)*);
65    }};
66
67    ($out:expr => = $($rest:tt)*) => {{
68        $out.extend($crate::op1('='));
69        miniquote_to!($out => $($rest)*);
70    }};
71
72    ($out:expr => ; $($rest:tt)*) => {{
73        $out.extend($crate::op1(';'));
74        miniquote_to!($out => $($rest)*);
75    }};
76
77    ($out:expr => . $($rest:tt)*) => {{
78        $out.extend($crate::op1('.'));
79        miniquote_to!($out => $($rest)*);
80    }};
81
82    ($out:expr => :: $($rest:tt)*) => {{
83        $out.extend($crate::op2(':', ':'));
84        miniquote_to!($out => $($rest)*);
85    }};
86
87    ($out:expr => : $($rest:tt)*) => {{
88        $out.extend($crate::op1(':'));
89        miniquote_to!($out => $($rest)*);
90    }};
91
92    ($out:expr => * $($rest:tt)*) => {{
93        $out.extend($crate::op1('*'));
94        miniquote_to!($out => $($rest)*);
95    }};
96
97    ($out:expr => + $($rest:tt)*) => {{
98        $out.extend($crate::op1('+'));
99        miniquote_to!($out => $($rest)*);
100    }};
101
102    ($out:expr => -> $($rest:tt)*) => {{
103        $out.extend($crate::op2('-', '>'));
104        miniquote_to!($out => $($rest)*);
105    }};
106
107    ($out:expr => & $($rest:tt)*) => {{
108        $out.extend($crate::op1('&'));
109        miniquote_to!($out => $($rest)*);
110    }};
111
112    ($out:expr => || $($rest:tt)*) => {{
113        $out.extend($crate::op2('|', '|'));
114        miniquote_to!($out => $($rest)*);
115    }};
116
117    ($out:expr => | $($rest:tt)*) => {{
118        $out.extend($crate::op1('|'));
119        miniquote_to!($out => $($rest)*);
120    }};
121
122    ($out:expr => , $($rest:tt)*) => {{
123        $out.extend($crate::op1(','));
124        miniquote_to!($out => $($rest)*);
125    }};
126
127    ($out:expr => < $($rest:tt)*) => {{
128        $out.extend($crate::op1('<'));
129        miniquote_to!($out => $($rest)*);
130    }};
131
132    ($out:expr => >> $($rest:tt)*) => {{
133        $out.extend($crate::op2('>', '>'));
134        miniquote_to!($out => $($rest)*);
135    }};
136
137    ($out:expr => > $($rest:tt)*) => {{
138        $out.extend($crate::op1('>'));
139        miniquote_to!($out => $($rest)*);
140    }};
141
142    ($out:expr => _ $($rest:tt)*) => {{
143        $out.extend([TokenTree::Ident(Ident::new("_", Span::mixed_site()))]);
144        miniquote_to!($out => $($rest)*);
145    }};
146
147    ($out:expr => $ident:ident $($rest:tt)*) => {{
148        $out.extend([TokenTree::Ident(Ident::new(stringify!($ident), Span::mixed_site()))]);
149        miniquote_to!($out => $($rest)*);
150    }};
151
152    ($out:expr => $lt:lifetime $($rest:tt)*) => {{
153        stringify!($lt).parse::<TokenStream>().unwrap().into_token_trees($out);
154        miniquote_to!($out => $($rest)*);
155    }};
156
157    ($out:expr => $lit:literal $($rest:tt)*) => {{
158        $out.extend(stringify!($lit).parse::<TokenStream>().unwrap());
159        miniquote_to!($out => $($rest)*);
160    }};
161
162    ($out:expr => $tt:tt $($rest:tt)*) => {{
163        // match `$`
164        miniquote_to!(@@@(-$tt-); $out => $($rest)*);
165    }};
166
167    (@@@($(-)$+); $out:expr => $($rest:tt)*) => {{
168        // match `$`
169        $out.extend($crate::op1('$'));
170        miniquote_to!($out => $($rest)*);
171    }};
172}
173
174macro_rules! miniquote {
175    ($($tt:tt)*) => {{
176        #[allow(unused_mut)]
177        let mut out = TokenStream::new();
178        miniquote_to!(&mut out => $($tt)*);
179        out
180    }};
181}
182
183mod parse;
184
185fn op1(c0: char) -> [TokenTree; 1] {
186    [TokenTree::Punct(Punct::new(c0, Spacing::Alone))]
187}
188
189fn op2(c0: char, c1: char) -> [TokenTree; 2] {
190    [
191        TokenTree::Punct(Punct::new(c0, Spacing::Joint)),
192        TokenTree::Punct(Punct::new(c1, Spacing::Alone)),
193    ]
194}
195
196fn sdl3_main_path() -> TokenStream {
197    miniquote!(::#{Ident::new(SDL3_MAIN, Span::mixed_site())})
198}
199
200fn sdl3_main_internal_path() -> TokenStream {
201    miniquote!(#{sdl3_main_path()}::__internal)
202}
203
204fn sdl3_sys_path() -> TokenStream {
205    miniquote!(#{sdl3_main_internal_path()}::sdl3_sys)
206}
207
208fn priv_ident(kind: &str, name: &str) -> Ident {
209    Ident::new(&format!("__sdl3_main_{kind}_{name}"), Span::mixed_site())
210}
211
212fn app_raw_fn_ident(name: &str) -> Ident {
213    priv_ident("fnp", name)
214}
215
216fn app_fn_ident(name: &str) -> Ident {
217    priv_ident("fn", name)
218}
219
220fn app_type_ident(name: &str) -> Ident {
221    priv_ident("t", name)
222}
223
224fn app_fn(
225    name: &str,
226    attr: TokenStream,
227    item: TokenStream,
228    f: impl FnOnce(&mut TokenStream, Function) -> Result<(), Error>,
229) -> TokenStream {
230    wrap(attr, item, |out, attr, item| {
231        if !attr.is_empty() {
232            Err(Error::new(
233                Some(attr.first().unwrap().span()),
234                format!("other attributes aren't supported with `#[{name}]`"),
235            ))
236        } else {
237            let item = Function::parse_all(item)?;
238            if let Some(abi) = &item.abi {
239                return Err(Error::new(
240                    Some(abi.span),
241                    "this function shouldn't set an ABI",
242                ));
243            }
244            miniquote_to! { out =>
245                mod #{&item.ident} {}
246                #[allow(non_upper_case_globals)]
247                const #{app_raw_fn_ident(name)}: #{item.signature()} = const {
248                    #{&item}
249                    #{&item.ident}
250                };
251            };
252            f(out, item)
253        }
254    })
255}
256
257fn wrap(
258    attr: TokenStream,
259    item: TokenStream,
260    f: impl FnOnce(&mut TokenStream, &mut &[TokenTree], &mut &[TokenTree]) -> Result<(), Error>,
261) -> TokenStream {
262    let mut ts = TokenStream::new();
263    match f(&mut ts, input!(attr), input!(item)) {
264        Ok(()) => ts,
265        Err(err) => err.into_token_stream(),
266    }
267}
268
269fn shuttle_unsafe() -> TokenStream {
270    if cfg!(feature = "std") {
271        miniquote!(unsafe)
272    } else {
273        miniquote!()
274    }
275}
276
277fn shuttle_unit_def() -> TokenStream {
278    if cfg!(feature = "std") {
279        miniquote! {
280            #[allow(non_upper_case_globals)]
281            static #{priv_ident("static", "shuttle_unit")}:
282                #{sdl3_main_internal_path()}::Shuttle<()> =
283                #{sdl3_main_internal_path()}::Shuttle::new();
284        }
285    } else {
286        miniquote!()
287    }
288}
289
290fn shuttle_unit_capture_and_continue() -> TokenStream {
291    if cfg!(feature = "std") {
292        miniquote!(#{priv_ident("static", "shuttle_unit")}.capture_and_continue)
293    } else {
294        miniquote!((|_, f| f()))
295    }
296}
297
298fn shuttle_unit_capture() -> TokenStream {
299    if cfg!(feature = "std") {
300        miniquote!(#{priv_ident("static", "shuttle_unit")}.capture)
301    } else {
302        miniquote!((|f| f()))
303    }
304}
305
306fn shuttle_unit_resume() -> TokenStream {
307    if cfg!(feature = "std") {
308        miniquote!(unsafe { #{priv_ident("static", "shuttle_unit")}.resume() })
309    } else {
310        miniquote!()
311    }
312}
313
314#[proc_macro_attribute]
315pub fn main(attr: TokenStream, item: TokenStream) -> TokenStream {
316    app_fn("main", attr, item, |out, f| {
317        let app_main = app_raw_fn_ident("main");
318
319        let simple_return = if let Some(rtype) = &f.return_type {
320            rtype.is_ident_no_gen("bool") || rtype.is_ident_no_gen("c_int")
321        } else {
322            true
323        };
324
325        if simple_return {
326            if cfg!(feature = "std") {
327                miniquote_to! { out =>
328                    #{shuttle_unit_def()}
329
330                    fn main() -> ::core::result::Result<(), &'static ::core::ffi::CStr> {
331                        use ::core::{any::Any, ffi::{c_char, c_int, CStr}, option::Option, result::Result};
332                        use ::std::panic::catch_unwind;
333                        use #{sdl3_main_path()}::{app::AppMain, MainThreadToken};
334                        use #{sdl3_main_internal_path()}::{Shuttle, run_app};
335                        use #{sdl3_sys_path()}::error::SDL_GetError;
336
337                        unsafe extern "C" fn sdl_main(argc: c_int, argv: *mut *mut c_char) -> c_int {
338                            unsafe {
339                                #{shuttle_unit_capture_and_continue()}(
340                                    1,
341                                    || unsafe { #app_main.main(MainThreadToken::assert(), argc, argv) },
342                                )
343                            }
344                        }
345
346                        if unsafe { run_app(sdl_main) } == 0 {
347                            Result::Ok(())
348                        } else {
349                            #{shuttle_unit_resume()}
350                            Result::Err(unsafe { CStr::from_ptr(SDL_GetError()) })
351                        }
352                    }
353                }
354                Ok(())
355            } else {
356                // no_std main
357                miniquote_to! { out =>
358                    #[unsafe(no_mangle)]
359                    extern "C" fn main(argc: ::core::ffi::c_int, argv: *mut *mut ::core::ffi::c_char) -> ::core::ffi::c_int {
360                        use ::core::{ffi::{c_char, c_int}, option::Option, ptr};
361                        use #{sdl3_main_path()}::{app::AppMain, MainThreadToken};
362                        use #{sdl3_sys_path()}::main::SDL_RunApp;
363
364                        unsafe extern "C" fn sdl_main(argc: c_int, argv: *mut *mut c_char) -> c_int {
365                            unsafe { #app_main.main(MainThreadToken::assert(), argc, argv) }
366                        }
367
368                        unsafe { SDL_RunApp(argc, argv, Option::Some(sdl_main), ptr::null_mut()) }
369                    }
370                }
371                Ok(())
372            }
373        } else {
374            if !cfg!(feature = "std") {
375                return Err(Error::new(
376                    Some(f.ident.span()),
377                    "main return types other than `()`, `bool` or `c_int` require the `std` feature",
378                ));
379            }
380            let rtype = &f.return_type.unwrap();
381            miniquote_to! { out =>
382                fn main() -> #rtype {
383                    use ::core::{ffi::{c_char, c_int}, mem::MaybeUninit, ptr::{addr_of, addr_of_mut}};
384                    use #{sdl3_main_path()}::{app::AppMainWithResult, MainThreadToken};
385                    use #{sdl3_main_internal_path()}::{Shuttle, run_app};
386
387                    static SHUTTLE: Shuttle<#rtype> = Shuttle::new();
388
389                    unsafe extern "C" fn sdl_main(argc: c_int, argv: *mut *mut c_char) -> c_int {
390                        unsafe {
391                            SHUTTLE.capture(
392                                || #app_main.main(MainThreadToken::assert(), argc, argv)
393                            );
394                        };
395                        0
396                    }
397
398                    unsafe {
399                        run_app(sdl_main);
400                        SHUTTLE.resume()
401                    }
402                }
403            }
404            Ok(())
405        }
406    })
407}
408
409#[proc_macro_attribute]
410pub fn app_impl(attr: TokenStream, item: TokenStream) -> TokenStream {
411    wrap(attr, item, |out, attr, item| {
412        let impl_block = ImplBlock::parse_all(item)?;
413        let state_t = impl_block.ty.clone();
414        let mut has_init = false;
415        let mut has_iterate = false;
416        let mut has_event = false;
417        let mut has_quit = false;
418
419        for item in impl_block.items.iter() {
420            if let Item::Function(f) = item {
421                let attr = Ident::new(
422                    match f.ident.to_string().as_str() {
423                        "app_init" => {
424                            if has_init {
425                                return Err(Error::new(
426                                    Some(f.ident.span()),
427                                    "`app_init` already defined",
428                                ));
429                            }
430                            has_init = true;
431                            "app_init"
432                        }
433                        "app_iterate" => {
434                            if has_iterate {
435                                return Err(Error::new(
436                                    Some(f.ident.span()),
437                                    "`app_iterate` already defined",
438                                ));
439                            }
440                            has_iterate = true;
441                            "app_iterate"
442                        }
443                        "app_event" => {
444                            if has_event {
445                                return Err(Error::new(
446                                    Some(f.ident.span()),
447                                    "`app_event` already defined",
448                                ));
449                            }
450                            has_event = true;
451                            "app_event"
452                        }
453                        "app_quit" => {
454                            if has_quit {
455                                return Err(Error::new(
456                                    Some(f.ident.span()),
457                                    "`app_quit` already defined",
458                                ));
459                            }
460                            has_quit = true;
461                            "app_quit"
462                        }
463                        _ => continue,
464                    },
465                    Span::call_site(),
466                );
467                let mut wrapper = f.clone();
468                wrapper.attrs = Vec::new();
469                wrapper.vis = Visibility::default();
470                wrapper.abi = None;
471                wrapper.return_type = wrapper.return_type.map(|t| t.replace_self(state_t.clone()));
472                for param in wrapper.params.iter_mut() {
473                    if param.ident.to_string() == "self" {
474                        param.ident = Ident::new("__sdl3_main_self", Span::mixed_site());
475                    }
476                    param.ty = param.ty.replace_self(state_t.clone());
477                }
478                let args = wrapper.params.to_args();
479                wrapper.body = TokenTree::Group(Group::new(
480                    Delimiter::Brace,
481                    miniquote!(#{&state_t}::#{&f.ident} #args),
482                ));
483                miniquote_to!(out => #[#{sdl3_main_path()}::#attr] #wrapper)
484            }
485        }
486        if !has_init {
487            return Err(Error::new(None, "missing `app_init`"));
488        }
489        if !has_iterate {
490            return Err(Error::new(None, "missing `app_iterate`"));
491        }
492        if !has_event {
493            return Err(Error::new(None, "missing `app_event`"));
494        }
495        if !has_quit {
496            let f = Function::new(Ident::new("app_quit", Span::mixed_site()));
497            miniquote_to!(out => #[#{sdl3_main_path()}::app_quit] #f);
498        }
499        miniquote_to!(out => #attr #impl_block);
500        Ok(())
501    })
502}
503
504#[proc_macro_attribute]
505pub fn app_init(attr: TokenStream, item: TokenStream) -> TokenStream {
506    app_fn("app_init", attr, item, |out, f| {
507        let mut state = Type::unit();
508        if let Some(rtype) = &f.return_type {
509            if let Some(generics) = rtype.path_generics() {
510                if generics.params.len() == 1 {
511                    if let Generic::Type(t) = &generics.params[0] {
512                        state = t.clone();
513                    }
514                }
515            }
516        }
517
518        let state_t = &app_type_ident("AppState");
519
520        miniquote_to! { out =>
521            #[allow(non_camel_case_types)]
522            type #state_t = #state;
523
524            #[#{sdl3_main_path()}::main]
525            unsafe fn __sdl3_main_callbacks(argc: ::core::ffi::c_int, argv: *mut *mut ::core::ffi::c_char) -> ::core::ffi::c_int {
526                use ::core::ffi::{c_char, c_int, c_void};
527                use #{sdl3_sys_path()}::{init::SDL_AppResult, main::SDL_EnterAppMainCallbacks};
528                use #{sdl3_main_path()}::{app::AppInit, MainThreadToken};
529
530                unsafe extern "C" fn app_init(
531                    appstate: *mut *mut c_void, argc: c_int, argv: *mut *mut c_char
532                ) -> SDL_AppResult {
533                    #{shuttle_unsafe()} {
534                        #{shuttle_unit_capture_and_continue()}(
535                            SDL_AppResult::FAILURE,
536                            || AppInit::<#state_t>::init(
537                                #{app_raw_fn_ident("app_init")},
538                                MainThreadToken::assert(),
539                                appstate,
540                                argc,
541                                argv
542                            )
543                        )
544                    }
545                }
546
547                let st = unsafe {
548                    SDL_EnterAppMainCallbacks(
549                        argc,
550                        argv,
551                        Option::Some(app_init),
552                        Option::Some(#{app_fn_ident("app_iterate")}),
553                        Option::Some(#{app_fn_ident("app_event")}),
554                        Option::Some(#{app_fn_ident("app_quit")}),
555                    )
556                };
557                #{shuttle_unit_resume()}
558                st
559            }
560        }
561        Ok(())
562    })
563}
564
565#[proc_macro_attribute]
566pub fn app_iterate(attr: TokenStream, item: TokenStream) -> TokenStream {
567    let name = "app_iterate";
568    app_fn(name, attr, item, |out, f| {
569        let state_ac = match f.params.len() {
570            1 => f.params[0].ty.classify()? as u8,
571            _ => 0,
572        };
573        miniquote_to! { out =>
574            unsafe extern "C" fn #{app_fn_ident(name)}(
575                appstate: *mut ::core::ffi::c_void
576            ) -> #{sdl3_sys_path()}::init::SDL_AppResult {
577                #{shuttle_unsafe()} {
578                    #{shuttle_unit_capture_and_continue()}(
579                        #{sdl3_sys_path()}::init::SDL_AppResult::FAILURE,
580                        || #{sdl3_main_path()}::app::AppIterate::<#{app_type_ident("AppState")}, #state_ac>::iterate(
581                            #{app_raw_fn_ident(name)},
582                            appstate
583                        )
584                    )
585                }
586            }
587        }
588        Ok(())
589    })
590}
591
592#[proc_macro_attribute]
593pub fn app_event(attr: TokenStream, item: TokenStream) -> TokenStream {
594    let name = "app_event";
595    app_fn(name, attr, item, |out, f| {
596        let (state_ac, event_ac) = match f.params.len() {
597            1 => (0, f.params[0].ty.classify()? as u8),
598            2 => (
599                f.params[0].ty.classify()? as u8,
600                f.params[1].ty.classify()? as u8,
601            ),
602            _ => (0, 0),
603        };
604        miniquote_to! { out =>
605            unsafe extern "C" fn #{app_fn_ident(name)}(
606                appstate: *mut ::core::ffi::c_void,
607                event: *mut #{sdl3_sys_path()}::events::SDL_Event
608            ) -> #{sdl3_sys_path()}::init::SDL_AppResult {
609                #{shuttle_unsafe()} {
610                    #{shuttle_unit_capture_and_continue()}(
611                        #{sdl3_sys_path()}::init::SDL_AppResult::FAILURE,
612                        || #{sdl3_main_path()}::app::AppEvent::<#{app_type_ident("AppState")}, #state_ac, #event_ac>::event(
613                            #{app_raw_fn_ident(name)},
614                            appstate,
615                            event
616                        )
617                    )
618                }
619            }
620        }
621        Ok(())
622    })
623}
624
625#[proc_macro_attribute]
626pub fn app_quit(attr: TokenStream, item: TokenStream) -> TokenStream {
627    let name = "app_quit";
628    app_fn(name, attr, item, |out, f| {
629        let state_ac = match f.params.len() {
630            1 | 2 => f.params[0].ty.classify()? as u8,
631            _ => 0,
632        };
633        miniquote_to! { out =>
634            unsafe extern "C" fn #{app_fn_ident(name)}(
635                appstate: *mut ::core::ffi::c_void,
636                result: #{sdl3_sys_path()}::init::SDL_AppResult
637            ) {
638                #{shuttle_unsafe()} {
639                    #{shuttle_unit_capture()}(
640                        || #{sdl3_main_path()}::app::AppQuit::<#{app_type_ident("AppState")}, #state_ac>::quit(
641                            #{app_raw_fn_ident(name)},
642                            appstate,
643                            result
644                        )
645                    )
646                }
647            }
648        }
649        Ok(())
650    })
651}