Skip to main content

pyro_macro/ffi/
mod.rs

1//! This crate provides proc macros to generate FFI boilerplate for capabilities.
2use ::pyro_spec::InterfaceSpec;
3use syn::parse_file;
4
5use crate::{
6    ffi::{capability::CapabilityImpl, config::CapConfig, spec::build_spec},
7    format::{BridgeableArgs, DocRec, magma},
8    utils::has_attr,
9};
10
11pub mod capability;
12pub mod config;
13pub mod lifecycle;
14pub mod methods;
15pub mod paths;
16pub mod spec;
17
18/// For generating the interface lib.rs (module-side code)
19pub fn generate_interface(
20    content: &str,
21    cap_name: &str,
22    cap_version: &str,
23) -> syn::Result<(syn::File, InterfaceSpec<'static>)> {
24    let file = parse_file(content)?;
25    let spec = build_spec(cap_name, &file);
26    let import_location: syn::Path = syn::parse_quote!(::pyroduct);
27
28    let mut generated_code = quote::quote! {
29        //! Automatically generated by pyroduct. DO NOT EDIT.
30        #![allow(unused_imports, dead_code, unused_variables, nonstandard_style)]
31        use pyroduct;
32    };
33
34    for item in file.items {
35        match item {
36            syn::Item::Impl(item_impl) if has_attr(&item_impl.attrs, "capability") => {
37                let cap = CapabilityImpl::new(item_impl, true, cap_name, cap_version)?;
38                generated_code.extend(cap.expand_module());
39            }
40            syn::Item::Struct(mut item_struct) => {
41                if let Some(args) = extract_magma_args(&item_struct.attrs)? {
42                    item_struct.attrs.retain(|a| !is_magma_attr(a));
43                    let expanded = magma(args, &mut item_struct, &import_location)?;
44                    generated_code.extend(expanded);
45                }
46            }
47            _ => {}
48        }
49    }
50    let code: syn::File = syn::parse2(generated_code)?;
51    Ok((code, spec))
52}
53
54/// For generating the capability lib.rs (host-side code)
55pub fn generate_capability(
56    content: &str,
57    cap_name: &str,
58    cap_version: &str,
59) -> syn::Result<syn::File> {
60    let file = parse_file(content)?;
61
62    let mut generated_code = quote::quote! {
63        //! Automatically generated by pyroduct. DO NOT EDIT.
64        #![allow(unused_imports, dead_code, unused_variables, nonstandard_style)]
65        use ::pyroduct;
66    };
67    let import_location: syn::Path = syn::parse_quote!(::pyroduct);
68
69    for item in file.items {
70        match item {
71            syn::Item::Impl(item_impl) if has_attr(&item_impl.attrs, "capability") => {
72                let cap = CapabilityImpl::new(item_impl, true, cap_name, cap_version)?;
73                generated_code.extend(cap.expand_capability());
74            }
75            syn::Item::Struct(mut item_struct) => {
76                if let Some(args) = extract_magma_args(&item_struct.attrs)? {
77                    let expanded = magma(args, &mut item_struct, &import_location)?;
78                    generated_code.extend(expanded);
79                } else if has_attr(&item_struct.attrs, "config") {
80                    let found_config = CapConfig::new(item_struct, DocRec::StructDoc)?;
81                    generated_code.extend(found_config.expand());
82                }
83            }
84            _ => {}
85        }
86    }
87
88    let code: syn::File = syn::parse2(generated_code)?;
89    Ok(code)
90}
91
92/// Extract `BridgeableArgs` from a `#[magma(...)]` attribute.
93/// Handles both `#[magma]` (no args) and `#[magma(derive(Debug))]`.
94fn extract_magma_args(attrs: &[syn::Attribute]) -> syn::Result<Option<BridgeableArgs>> {
95    for attr in attrs {
96        if is_magma_attr(attr) {
97            return match &attr.meta {
98                syn::Meta::List(list) => {
99                    syn::parse2::<BridgeableArgs>(list.tokens.clone()).map(Some)
100                }
101                syn::Meta::Path(_) => {
102                    // Bare #[magma] with no arguments
103                    Ok(Some(BridgeableArgs {
104                        derives_to_pass: Vec::new(),
105                        compares_to_add: Vec::new(),
106                        doc_rec: DocRec::NoReq,
107                    }))
108                }
109                syn::Meta::NameValue(nv) => Err(syn::Error::new_spanned(
110                    nv,
111                    "Invalid magma attribute format",
112                )),
113            };
114        }
115    }
116    Ok(None)
117}
118
119fn is_magma_attr(attr: &syn::Attribute) -> bool {
120    if attr.path().is_ident("magma") {
121        return true;
122    }
123    if attr.path().segments.len() == 2
124        && attr.path().segments[0].ident == "pyroduct"
125        && attr.path().segments[1].ident == "magma"
126    {
127        return true;
128    }
129    false
130}