1use proc_macro::TokenStream;
149use proc_macro2::{Span as Span2, TokenStream as TokenStream2};
150use quote::{format_ident, quote, ToTokens as _};
151use std::collections::{BTreeSet, HashSet};
152use syn::parse::{Parse, ParseStream};
153use syn::spanned::Spanned as _;
154use syn::{
155 parenthesized, parse, parse_macro_input, token, Attribute, Error, FnArg, ForeignItemFn,
156 GenericParam, ItemFn, ItemMod, ItemStruct, ItemUse, Pat, PatType, ReturnType, Signature,
157 TraitItemFn, UseTree, Visibility,
158};
159
160#[proc_macro_attribute]
169pub fn sys_trait_function(attr: TokenStream, item: TokenStream) -> TokenStream {
170 let attr = parse_macro_input!(attr as AttrOptions);
171 let cfg_attr = attr.convert_to_cfg_attr();
172
173 let trait_fn = parse_macro_input!(item as TraitItemFn);
174
175 quote! {
176 #cfg_attr
177 #trait_fn
178 }
179 .into()
180}
181
182#[proc_macro_attribute]
205pub fn sys_function(attr: TokenStream, item: TokenStream) -> TokenStream {
206 let attr = parse_macro_input!(attr as AttrOptions);
207 let cfg_attr = attr.convert_to_cfg_attr();
208
209 let struct_info = match parse::<ForeignItemFn>(item.clone()) {
210 Ok(foreign_item_fn) => foreign_item_fn,
211 Err(_) => {
212 return match parse::<ItemFn>(item) {
213 Ok(item_fn) => {
214 quote! {
215 #cfg_attr
216 #item_fn
217 }
218 }
219 Err(err) => err.to_compile_error(),
220 }
221 .into()
222 }
223 };
224
225 let ForeignItemFn {
226 attrs,
227 vis,
228 sig,
229 semi_token: _,
230 } = struct_info;
231
232 let &Signature {
233 constness: _,
234 ref asyncness,
235 ref unsafety,
236 abi: _,
237 fn_token: _,
238 ref ident,
239 ref generics,
240 paren_token: _,
241 ref inputs,
242 ref variadic,
243 ref output,
244 } = &sig;
245
246 let sys_ident = format_ident!("{ident}_impl");
247 let asyncness = asyncness
248 .as_ref()
249 .map_or_else(TokenStream2::new, |_| quote!(.await));
250 let output_semicolon = if matches!(output, ReturnType::Default) {
251 quote!(;)
252 } else {
253 TokenStream2::new()
254 };
255
256 let mut param_errors = TokenStream2::new();
257 let input_names = inputs.iter().filter_map(|fn_arg| match *fn_arg {
258 FnArg::Receiver(_) => Some(quote!(self)),
259 FnArg::Typed(PatType { ref pat, .. }) => match **pat {
260 Pat::Ident(ref pat_ident) => Some(pat_ident.ident.to_token_stream()),
261 ref other => {
262 const MSG: &str = "Complex patterns in arguments are not supported by #[sys_function]: give the argument a name";
263 param_errors.extend(Error::new(other.span(), MSG).to_compile_error());
264 None
265 },
266 },
267 });
268
269 let generic_names = generics
270 .params
271 .iter()
272 .filter_map(|generic_param| match *generic_param {
273 GenericParam::Lifetime(_) => None,
274 GenericParam::Type(ref type_param) => Some(type_param.ident.to_token_stream()),
275 GenericParam::Const(ref const_param) => Some(const_param.ident.to_token_stream()),
276 })
277 .collect::<Vec<_>>();
278 let generic_names = if generic_names.is_empty() {
279 TokenStream2::new()
280 } else {
281 quote!(::<#(#generic_names),*>)
282 };
283
284 let mut body = quote! {
285 Self::#sys_ident #generic_names(#(#input_names),*)#asyncness #output_semicolon
286 };
287 if unsafety.is_some() {
288 body = quote!(unsafe { #body });
289 }
290
291 let result = quote! {
292 #cfg_attr
293 #(#attrs)*
294 #vis #sig {
295 #body
296 }
297 };
298
299 let variadic_error = variadic
300 .as_ref()
301 .map_or_else(TokenStream2::new, |variadic| {
302 Error::new(variadic.dots.span(), "Variadic arguments are not permitted")
303 .to_compile_error()
304 });
305
306 quote! {
307 #result
308 #param_errors
309 #variadic_error
310 }
311 .into()
312}
313
314#[proc_macro_attribute]
327pub fn sys_struct(attr: TokenStream, item: TokenStream) -> TokenStream {
328 let attr = parse_macro_input!(attr as StructOptions);
329 let cfg_attr = attr.options.convert_to_cfg_attr();
330
331 let allowed_set: BTreeSet<_> = attr.options.allowed_set(|platform| match platform {
332 Platform::All | Platform::Posix => unreachable!("Should have been expanded"),
333 Platform::Linux => ("linux", "Linux"),
334 Platform::Macos => ("macos", "MacOS"),
335 Platform::Windows => ("windows", "Windows"),
336 });
337
338 let item_struct = parse_macro_input!(item as ItemStruct);
339 let &ItemStruct {
340 ref attrs,
341 ref vis,
342 struct_token: _,
343 ref ident,
344 ref generics,
345 fields: _,
346 semi_token: _,
347 } = &item_struct;
348
349 let deprecated_attr = attrs
350 .iter()
351 .find(|next_attr| next_attr.path().is_ident("deprecated"));
352
353 let generics_names = if generics.params.is_empty() {
354 TokenStream2::new()
355 } else {
356 let generics_names = generics
357 .params
358 .iter()
359 .map(|generic_param| match *generic_param {
360 GenericParam::Lifetime(ref lifetime_param) => {
361 lifetime_param.lifetime.to_token_stream()
362 }
363 GenericParam::Type(ref type_param) => type_param.ident.to_token_stream(),
364 GenericParam::Const(ref const_param) => const_param.ident.to_token_stream(),
365 });
366 quote!(<#(#generics_names),*>)
367 };
368
369 let aliases = allowed_set.into_iter().map(|(platform, ident_postfix)| {
370 let deprecated_attr = deprecated_attr.map_or_else(
371 TokenStream2::new,
372 |deprecated_attr| quote!(#deprecated_attr),
373 );
374 let doc_msg = format!("Platform-specific alias for [{ident}].");
375 let alias_ident = format_ident!("{ident}{ident_postfix}");
376
377 #[cfg(feature = "warn-platform-struct-usage")]
378 let warning = {
379 let warn_msg = format!("[warn-platform-struct-usage] Platform struct is explicitly used: use '{ident}' instead");
380 quote!(#[deprecated(note = #warn_msg)])
381 };
382 #[cfg(not(feature = "warn-platform-struct-usage"))]
383 let warning = TokenStream2::new();
384
385 quote! {
386 #[doc = #doc_msg]
387 #deprecated_attr
388 #warning
389 #[cfg(target_os = #platform)]
390 #vis type #alias_ident #generics_names = #ident #generics_names;
391 }
392 });
393
394 let trait_asserts = if attr.traits.is_empty() {
395 TokenStream2::new()
396 } else {
397 let cfg_attr = attr.options.convert_to_cfg_attr();
398 let traits = attr.traits;
399 let generics_where_clause = generics.where_clause.as_ref();
400
401 let separator = if traits.is_empty() {
402 TokenStream2::new()
403 } else {
404 quote!(+)
405 };
406
407 let generics_usages = if generics.params.is_empty() {
408 TokenStream2::new()
409 } else {
410 let generics_usages =
411 generics
412 .params
413 .iter()
414 .map(|generic_param| match *generic_param {
415 GenericParam::Lifetime(_) => quote!('_),
416 GenericParam::Type(ref type_param) => type_param.ident.to_token_stream(),
417 GenericParam::Const(ref const_param) => const_param.ident.to_token_stream(),
418 });
419 quote!(<#(#generics_usages),*>)
420 };
421
422 quote! {
423 #cfg_attr
424 const _: () = {
425 fn _assert_traits<T: #(#traits)+* #separator ?Sized>() {}
426 fn _check #generics() #generics_where_clause { _assert_traits::<#ident #generics_usages>(); }
427 };
428 }
429 };
430
431 quote! {
432 #(#aliases)*
433 #cfg_attr
434 #item_struct
435 #trait_asserts
436 }
437 .into()
438}
439
440#[proc_macro_attribute]
462pub fn platform_mod(attr: TokenStream, item: TokenStream) -> TokenStream {
463 struct DModInfo {
464 attrs: Vec<Attribute>,
465 vis: Visibility,
466 ident: proc_macro2::Ident,
467 }
468
469 let attr = parse_macro_input!(attr as AttrOptions);
470 let allowed_set: BTreeSet<_> = attr.allowed_set(|platform| match platform {
471 Platform::All | Platform::Posix => unreachable!("Should have been expanded"),
472 Platform::Linux => "linux",
473 Platform::Macos => "macos",
474 Platform::Windows => "windows",
475 });
476
477 let mod_info = match parse::<ItemUse>(item.clone()) {
478 Ok(item_use) => {
479 let ItemUse {
480 attrs,
481 vis,
482 use_token: _,
483 leading_colon,
484 tree,
485 semi_token: _,
486 } = item_use;
487
488 if let Some(leading_colon) = leading_colon {
489 return Error::new(
490 leading_colon.span(),
491 "#[platform_mod] does not support absolute paths (leading `::`). Please use a local identifier"
492 ).to_compile_error().into();
493 }
494
495 let use_ident = match tree {
496 UseTree::Name(use_name) => use_name.ident,
497 other @ (UseTree::Path(_)
498 | UseTree::Rename(_)
499 | UseTree::Glob(_)
500 | UseTree::Group(_)) => {
501 return Error::new(
502 other.span(),
503 "#[platform_mod] on `use` statements only supports simple direct aliases (e.g., `use name;`)"
504 ).to_compile_error().into();
505 }
506 };
507
508 DModInfo {
509 attrs,
510 vis,
511 ident: use_ident,
512 }
513 }
514 Err(_) => match parse::<ItemMod>(item) {
515 Ok(item_mod) => {
516 let item_mod_span = item_mod.span();
517
518 let ItemMod {
519 attrs,
520 vis,
521 unsafety,
522 mod_token: _,
523 ident,
524 content,
525 semi: _,
526 } = item_mod;
527
528 if let Some(unsafety) = unsafety {
529 return Error::new(
530 unsafety.span(),
531 "#[platform_mod] does not support `unsafe` modules",
532 )
533 .to_compile_error()
534 .into();
535 }
536
537 if content.is_some() {
538 return Error::new(
539 item_mod_span,
540 "#[platform_mod] does not support inline modules with a body `{ ... }`.\n\
541 Please use a declaration like `mod name;` to allow swapping the file based on the platform."
542 ).to_compile_error().into();
543 }
544
545 DModInfo { attrs, vis, ident }
546 }
547 Err(_) => {
548 return Error::new(
549 Span2::call_site(),
550 "#[platform_mod] expected a `mod declaration` (e.g., `mod foo;`) or a `use statement` (e.g., `use foo;`)"
551 ).to_compile_error().into();
552 }
553 },
554 };
555
556 let DModInfo { attrs, vis, ident } = mod_info;
557
558 let mods = allowed_set.into_iter().map(|platform| {
559 let platform_ident = format_ident!("{platform}");
560
561 quote! {
562 #[cfg(target_os = #platform)]
563 #(#attrs)*
564 #vis mod #platform_ident;
565 #[cfg(target_os = #platform)]
566 #(#attrs)*
567 use #platform_ident as #ident;
568 }
569 });
570
571 quote!(#(#mods)*).into()
572}
573
574mod keywords {
577 use syn::custom_keyword;
578
579 custom_keyword!(traits);
580
581 custom_keyword!(exclude);
582 custom_keyword!(include);
583
584 custom_keyword!(all);
585 custom_keyword!(posix);
586 custom_keyword!(linux);
587 custom_keyword!(macos);
588 custom_keyword!(windows);
589}
590
591#[derive(Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd)]
592enum Platform {
593 All,
594 Posix,
595 Linux,
596 Macos,
597 Windows,
598}
599
600impl Platform {
601 #[must_use]
602 fn expand(self) -> Vec<Self> {
603 match self {
604 Self::All => vec![Self::Linux, Self::Macos, Self::Windows],
605 Self::Posix => vec![Self::Linux, Self::Macos],
606 Self::Linux | Self::Macos | Self::Windows => vec![self],
607 }
608 }
609}
610
611impl Parse for Platform {
612 fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
613 let lookahead = input.lookahead1();
614 if lookahead.peek(keywords::all) {
615 input.parse::<keywords::all>()?;
616 Ok(Self::All)
617 } else if lookahead.peek(keywords::posix) {
618 input.parse::<keywords::posix>()?;
619 Ok(Self::Posix)
620 } else if lookahead.peek(keywords::linux) {
621 input.parse::<keywords::linux>()?;
622 Ok(Self::Linux)
623 } else if lookahead.peek(keywords::macos) {
624 input.parse::<keywords::macos>()?;
625 Ok(Self::Macos)
626 } else if lookahead.peek(keywords::windows) {
627 input.parse::<keywords::windows>()?;
628 Ok(Self::Windows)
629 } else {
630 Err(lookahead.error())
631 }
632 }
633}
634
635struct AttrOptions {
636 span: Span2,
637 exclude: HashSet<Platform>,
638 include: HashSet<Platform>,
639}
640
641impl AttrOptions {
642 #[must_use]
643 fn allowed_set<B: FromIterator<O>, M: Fn(Platform) -> O, O>(&self, mapping: M) -> B {
644 let all_includes = self
645 .include
646 .iter()
647 .copied()
648 .flat_map(Platform::expand)
649 .collect::<HashSet<_>>();
650 let all_excludes = self
651 .exclude
652 .iter()
653 .copied()
654 .flat_map(Platform::expand)
655 .collect::<HashSet<_>>();
656 all_includes
657 .difference(&all_excludes)
658 .map(|platform| mapping(*platform))
659 .collect()
660 }
661
662 #[must_use]
663 fn convert_to_cfg_attr(&self) -> TokenStream2 {
664 let allowed_set: BTreeSet<_> = self.allowed_set(|platform| match platform {
665 Platform::All | Platform::Posix => unreachable!("Should have been expanded"),
666 Platform::Linux => "linux",
667 Platform::Macos => "macos",
668 Platform::Windows => "windows",
669 });
670
671 let error = if allowed_set.is_empty() {
672 Error::new(
673 self.span,
674 "Configuration excludes all platforms: 'include' and 'exclude' cancel each other out",
675 )
676 .to_compile_error()
677 } else {
678 TokenStream2::new()
679 };
680
681 let mut cfg_attrs = quote!(#(target_os = #allowed_set),*);
682 if allowed_set.len() != 1 {
683 cfg_attrs = quote!(any(#cfg_attrs));
684 }
685
686 quote! {
687 #error
688 #[cfg(#cfg_attrs)]
689 }
690 }
691}
692
693impl Parse for AttrOptions {
694 fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
695 parse_attributes(input, false).map(|options| {
696 let StructOptions { options, traits } = options;
697 assert_eq!(traits.len(), 0, "Implementation error");
698 options
699 })
700 }
701}
702
703struct StructOptions {
704 options: AttrOptions,
705 traits: Vec<syn::Path>,
706}
707
708impl Parse for StructOptions {
709 fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
710 parse_attributes(input, true)
711 }
712}
713
714fn parse_attributes(input: ParseStream<'_>, allow_traits: bool) -> syn::Result<StructOptions> {
715 let mut result = StructOptions {
716 options: AttrOptions {
717 span: input.span(),
718 exclude: HashSet::default(),
719 include: HashSet::default(),
720 },
721 traits: Vec::default(),
722 };
723
724 while !input.is_empty() {
725 let lookahead = input.lookahead1();
726
727 if allow_traits && lookahead.peek(keywords::traits) {
728 input.parse::<keywords::traits>()?;
729
730 let content;
731 parenthesized!(content in input);
732
733 let traits = content.parse_terminated(syn::Path::parse, token::Comma)?;
734 result.traits.extend(traits);
735 } else if lookahead.peek(keywords::exclude) {
736 input.parse::<keywords::exclude>()?;
737
738 let content;
739 parenthesized!(content in input);
740
741 let platforms = content.parse_terminated(Platform::parse, token::Comma)?;
742 result.options.exclude.extend(platforms);
743 } else if lookahead.peek(keywords::include) {
744 input.parse::<keywords::include>()?;
745
746 let content;
747 parenthesized!(content in input);
748
749 let platforms = content.parse_terminated(Platform::parse, token::Comma)?;
750 result.options.include.extend(platforms);
751 } else {
752 return Err(lookahead.error());
753 }
754
755 if !input.is_empty() {
756 input.parse::<token::Comma>()?;
757 }
758 }
759
760 if result.options.include.is_empty() {
761 result.options.include.insert(Platform::All);
762 }
763
764 Ok(result)
765}