Skip to main content

kailash_plugin_macros/
lib.rs

1//! Proc-macros for the Kailash plugin guest SDK.
2//!
3//! Provides `#[kailash_plugin]` for annotating plugin entry-point functions.
4//! The macro generates the WASM ABI `kailash_execute` export that wraps the
5//! user's function with JSON serialization and memory management.
6//!
7//! # Usage
8//!
9//! ```ignore
10//! use kailash_plugin_guest::prelude::*;
11//!
12//! #[kailash_plugin(
13//!     name = "my-plugin",
14//!     description = "Processes data",
15//!     version = "0.1.0",
16//! )]
17//! fn process(inputs: GuestValueMap) -> Result<GuestValueMap, PluginError> {
18//!     Ok(inputs) // echo
19//! }
20//! ```
21
22use proc_macro::TokenStream;
23use quote::quote;
24use syn::{
25    parse::{Parse, ParseStream},
26    parse_macro_input,
27    punctuated::Punctuated,
28    Ident, ItemFn, LitStr, Token,
29};
30
31// ---------------------------------------------------------------------------
32// Attribute argument parsing
33// ---------------------------------------------------------------------------
34
35/// A single `key = "value"` pair in the macro attributes.
36struct AttrKV {
37    key: Ident,
38    _eq: Token![=],
39    value: AttrValue,
40}
41
42/// The right-hand side of an attribute `key = value`.
43enum AttrValue {
44    /// A string literal: `"my-plugin"`.
45    Str(LitStr),
46    /// An integer literal: `64`. Parsed but not yet used by any attribute.
47    #[allow(dead_code)]
48    Int(syn::LitInt),
49}
50
51impl Parse for AttrKV {
52    fn parse(input: ParseStream) -> syn::Result<Self> {
53        let key: Ident = input.parse()?;
54        let _eq: Token![=] = input.parse()?;
55
56        let value = if input.peek(LitStr) {
57            AttrValue::Str(input.parse()?)
58        } else {
59            AttrValue::Int(input.parse()?)
60        };
61
62        Ok(AttrKV { key, _eq, value })
63    }
64}
65
66/// Parsed collection of `#[kailash_plugin(...)]` attributes.
67struct PluginAttrs {
68    pairs: Punctuated<AttrKV, Token![,]>,
69}
70
71impl Parse for PluginAttrs {
72    fn parse(input: ParseStream) -> syn::Result<Self> {
73        let pairs = Punctuated::parse_terminated(input)?;
74        Ok(PluginAttrs { pairs })
75    }
76}
77
78/// Extracted and validated plugin metadata.
79struct PluginMeta {
80    name: String,
81    description: String,
82    version: String,
83}
84
85impl PluginMeta {
86    fn from_attrs(attrs: &PluginAttrs) -> syn::Result<Self> {
87        let mut name: Option<String> = None;
88        let mut description: Option<String> = None;
89        let mut version: Option<String> = None;
90
91        for kv in &attrs.pairs {
92            let key_str = kv.key.to_string();
93            match key_str.as_str() {
94                "name" => {
95                    if let AttrValue::Str(lit) = &kv.value {
96                        name = Some(lit.value());
97                    } else {
98                        return Err(syn::Error::new_spanned(
99                            &kv.key,
100                            "expected string literal for `name`",
101                        ));
102                    }
103                },
104                "description" => {
105                    if let AttrValue::Str(lit) = &kv.value {
106                        description = Some(lit.value());
107                    } else {
108                        return Err(syn::Error::new_spanned(
109                            &kv.key,
110                            "expected string literal for `description`",
111                        ));
112                    }
113                },
114                "version" => {
115                    if let AttrValue::Str(lit) = &kv.value {
116                        version = Some(lit.value());
117                    } else {
118                        return Err(syn::Error::new_spanned(
119                            &kv.key,
120                            "expected string literal for `version`",
121                        ));
122                    }
123                },
124                other => {
125                    return Err(syn::Error::new_spanned(
126                        &kv.key,
127                        format!(
128                            "unknown attribute `{other}`; expected `name`, `description`, or `version`"
129                        ),
130                    ));
131                },
132            }
133        }
134
135        let name = name.ok_or_else(|| {
136            syn::Error::new(
137                proc_macro2::Span::call_site(),
138                "missing required attribute `name`",
139            )
140        })?;
141        let description = description.unwrap_or_default();
142        let version = version.unwrap_or_else(|| "0.1.0".to_owned());
143
144        Ok(PluginMeta {
145            name,
146            description,
147            version,
148        })
149    }
150}
151
152// ---------------------------------------------------------------------------
153// Proc-macro entry point
154// ---------------------------------------------------------------------------
155
156/// Marks a function as a Kailash WASM plugin entry point.
157///
158/// The annotated function must have the signature:
159///
160/// ```ignore
161/// fn name(inputs: GuestValueMap) -> Result<GuestValueMap, PluginError>
162/// ```
163///
164/// # Attributes
165///
166/// | Attribute     | Type   | Required | Default   | Description                     |
167/// |---------------|--------|----------|-----------|---------------------------------|
168/// | `name`        | string | yes      | --        | Plugin name for the registry    |
169/// | `description` | string | no       | `""`      | Human-readable description      |
170/// | `version`     | string | no       | `"0.1.0"` | Semver version                  |
171///
172/// # Generated code
173///
174/// The macro generates:
175///
176/// 1. The original function, unchanged.
177/// 2. A `_KAILASH_MANIFEST_*` constant containing the JSON manifest string.
178/// 3. A `#[no_mangle] pub extern "C" fn kailash_execute(...)` that wraps
179///    the user function through `kailash_plugin_guest::abi::execute_with_fn`.
180///
181/// # Example
182///
183/// ```ignore
184/// use kailash_plugin_guest::prelude::*;
185///
186/// #[kailash_plugin(
187///     name = "echo",
188///     description = "Echoes inputs as outputs",
189/// )]
190/// fn echo(inputs: GuestValueMap) -> Result<GuestValueMap, PluginError> {
191///     Ok(inputs)
192/// }
193/// ```
194#[proc_macro_attribute]
195pub fn kailash_plugin(attr: TokenStream, item: TokenStream) -> TokenStream {
196    let attrs = parse_macro_input!(attr as PluginAttrs);
197    let func = parse_macro_input!(item as ItemFn);
198
199    let meta = match PluginMeta::from_attrs(&attrs) {
200        Ok(m) => m,
201        Err(e) => return e.to_compile_error().into(),
202    };
203
204    let func_name = &func.sig.ident;
205    let plugin_name = &meta.name;
206    let plugin_desc = &meta.description;
207    let plugin_version = &meta.version;
208
209    // Generate a unique manifest constant name to avoid collisions
210    // if multiple plugins are defined in the same crate (unlikely but safe)
211    let manifest_const_name = syn::Ident::new(
212        &format!("_KAILASH_MANIFEST_{}", func_name.to_string().to_uppercase()),
213        func_name.span(),
214    );
215
216    let expanded = quote! {
217        // Keep the original function unchanged
218        #func
219
220        /// JSON-serialized plugin manifest generated by `#[kailash_plugin]`.
221        #[doc(hidden)]
222        #[allow(dead_code)]
223        const #manifest_const_name: &str = concat!(
224            r#"{"name":""#, #plugin_name,
225            r#"","version":""#, #plugin_version,
226            r#"","description":""#, #plugin_desc,
227            r#"","abi_version":1,"inputs":[],"outputs":[]}"#
228        );
229
230        /// WASM ABI `kailash_execute` export generated by `#[kailash_plugin]`.
231        ///
232        /// Delegates to the user-defined function through the ABI trampoline.
233        #[doc(hidden)]
234        #[no_mangle]
235        pub extern "C" fn kailash_execute(
236            input_ptr: i32,
237            input_len: i32,
238            output_ptr_ptr: i32,
239            output_len_ptr: i32,
240        ) -> i32 {
241            ::kailash_plugin_guest::abi::execute_with_fn(
242                #func_name,
243                input_ptr,
244                input_len,
245                output_ptr_ptr,
246                output_len_ptr,
247            )
248        }
249    };
250
251    expanded.into()
252}