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}