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 miniquote_to!(@@@(-$tt-); $out => $($rest)*);
165 }};
166
167 (@@@($(-)$+); $out:expr => $($rest:tt)*) => {{
168 $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 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}