Skip to main content

rustbridge_macros/
lib.rs

1//! rustbridge-macros - Procedural macros for rustbridge plugins
2//!
3//! This crate provides:
4//! - `#[rustbridge_plugin]` - Mark a struct as a plugin implementation
5//! - `#[rustbridge_handler]` - Mark a method as a message handler
6//! - `#[derive(Message)]` - Derive message traits for request/response types
7//! - `rustbridge_entry!` - Generate the FFI entry point
8
9use darling::FromDeriveInput;
10use proc_macro::TokenStream;
11use quote::quote;
12use syn::{DeriveInput, ItemFn, parse_macro_input};
13
14/// Attribute for marking a struct as a rustbridge plugin
15///
16/// This generates the necessary boilerplate for implementing the Plugin trait
17/// and dispatching messages to handler methods.
18///
19/// # Example
20///
21/// ```ignore
22/// use rustbridge_macros::rustbridge_plugin;
23///
24/// #[rustbridge_plugin]
25/// struct MyPlugin {
26///     // plugin state
27/// }
28///
29/// impl MyPlugin {
30///     #[rustbridge_handler("user.create")]
31///     fn create_user(&self, req: CreateUserRequest) -> Result<CreateUserResponse, PluginError> {
32///         // handler implementation
33///     }
34/// }
35/// ```
36#[proc_macro_attribute]
37pub fn rustbridge_plugin(_attr: TokenStream, item: TokenStream) -> TokenStream {
38    let input = parse_macro_input!(item as DeriveInput);
39    let name = &input.ident;
40
41    let expanded = quote! {
42        #input
43
44        impl #name {
45            /// Create a new plugin instance
46            pub fn new() -> Self {
47                Self::default()
48            }
49        }
50    };
51
52    TokenStream::from(expanded)
53}
54
55/// Attribute for marking a method as a message handler
56///
57/// The handler will be invoked when a message with the matching type tag is received.
58///
59/// # Example
60///
61/// ```ignore
62/// #[rustbridge_handler("user.create")]
63/// fn create_user(&self, req: CreateUserRequest) -> Result<CreateUserResponse, PluginError> {
64///     // ...
65/// }
66/// ```
67#[proc_macro_attribute]
68pub fn rustbridge_handler(attr: TokenStream, item: TokenStream) -> TokenStream {
69    let type_tag = parse_macro_input!(attr as syn::LitStr);
70    let input = parse_macro_input!(item as ItemFn);
71
72    let fn_name = &input.sig.ident;
73    let fn_vis = &input.vis;
74    let fn_block = &input.block;
75    let fn_inputs = &input.sig.inputs;
76    let fn_output = &input.sig.output;
77
78    // Generate the handler with metadata
79    let expanded = quote! {
80        #fn_vis fn #fn_name(#fn_inputs) #fn_output {
81            const _TYPE_TAG: &str = #type_tag;
82            #fn_block
83        }
84    };
85
86    TokenStream::from(expanded)
87}
88
89/// Options for the Message derive macro
90#[derive(Debug, FromDeriveInput)]
91#[darling(attributes(message))]
92struct MessageOpts {
93    ident: syn::Ident,
94    generics: syn::Generics,
95
96    /// The type tag for this message (e.g., "user.create")
97    #[darling(default)]
98    tag: Option<String>,
99}
100
101/// Derive macro for message types
102///
103/// Implements serialization and type tag metadata for request/response types.
104///
105/// # Example
106///
107/// ```ignore
108/// #[derive(Message, Serialize, Deserialize)]
109/// #[message(tag = "user.create")]
110/// struct CreateUserRequest {
111///     pub username: String,
112///     pub email: String,
113/// }
114/// ```
115#[proc_macro_derive(Message, attributes(message))]
116pub fn derive_message(input: TokenStream) -> TokenStream {
117    let input = parse_macro_input!(input as DeriveInput);
118
119    let opts = match MessageOpts::from_derive_input(&input) {
120        Ok(opts) => opts,
121        Err(e) => return TokenStream::from(e.write_errors()),
122    };
123
124    let name = &opts.ident;
125    let (impl_generics, ty_generics, where_clause) = opts.generics.split_for_impl();
126
127    let type_tag = opts.tag.unwrap_or_else(|| {
128        // Generate default tag from type name (e.g., CreateUserRequest -> create_user_request)
129        let name_str = name.to_string();
130        to_snake_case(&name_str)
131    });
132
133    let expanded = quote! {
134        impl #impl_generics #name #ty_generics #where_clause {
135            /// Get the type tag for this message
136            pub const fn type_tag() -> &'static str {
137                #type_tag
138            }
139        }
140    };
141
142    TokenStream::from(expanded)
143}
144
145/// Generate the FFI entry point for a plugin
146///
147/// This macro creates the `plugin_create` extern function that the FFI layer
148/// calls to instantiate the plugin.
149///
150/// # Usage
151///
152/// For plugins using `Default`:
153/// ```ignore
154/// rustbridge_entry!(MyPlugin::default);
155/// ```
156///
157/// For plugins using `PluginFactory::create` (receives config at construction time):
158/// ```ignore
159/// rustbridge_entry!(MyPlugin::create);
160/// ```
161///
162/// When using `::create`, the macro generates both `plugin_create()` and
163/// `plugin_create_with_config(config_json, config_len)` FFI functions.
164#[proc_macro]
165pub fn rustbridge_entry(input: TokenStream) -> TokenStream {
166    let factory_path = parse_macro_input!(input as syn::ExprPath);
167
168    // Check if the path ends with "::create" to enable PluginFactory support
169    let is_factory_create = factory_path
170        .path
171        .segments
172        .last()
173        .is_some_and(|seg| seg.ident == "create");
174
175    let expanded = if is_factory_create {
176        // Extract the type path (everything except the final ::create)
177        let type_path: syn::Path = {
178            let mut path = factory_path.path.clone();
179            path.segments.pop(); // Remove "create"
180            // Remove trailing punctuation if present
181            if let Some(pair) = path.segments.pop() {
182                path.segments.push(pair.into_value());
183            }
184            path
185        };
186
187        quote! {
188            /// FFI entry point - creates a new plugin instance with default config
189            #[unsafe(no_mangle)]
190            pub unsafe extern "C" fn plugin_create() -> *mut ::std::ffi::c_void {
191                let config = ::rustbridge::PluginConfig::default();
192                match <#type_path as ::rustbridge::PluginFactory>::create(&config) {
193                    Ok(plugin) => {
194                        let plugin: Box<dyn ::rustbridge::Plugin> = Box::new(plugin);
195                        let boxed: Box<Box<dyn ::rustbridge::Plugin>> = Box::new(plugin);
196                        Box::into_raw(boxed) as *mut ::std::ffi::c_void
197                    }
198                    Err(_) => ::std::ptr::null_mut()
199                }
200            }
201
202            /// FFI entry point - creates a new plugin instance with provided config
203            ///
204            /// # Safety
205            ///
206            /// - `config_json` must be a valid pointer to a UTF-8 JSON string, or null
207            /// - `config_len` must be the length of the JSON string in bytes
208            /// - If `config_json` is null, a default config is used
209            #[unsafe(no_mangle)]
210            pub unsafe extern "C" fn plugin_create_with_config(
211                config_json: *const u8,
212                config_len: usize,
213            ) -> *mut ::std::ffi::c_void {
214                let config = if config_json.is_null() || config_len == 0 {
215                    ::rustbridge::PluginConfig::default()
216                } else {
217                    // SAFETY: Caller guarantees config_json is valid for config_len bytes
218                    let bytes = unsafe { ::std::slice::from_raw_parts(config_json, config_len) };
219                    match ::rustbridge::PluginConfig::from_json(bytes) {
220                        Ok(c) => c,
221                        Err(_) => return ::std::ptr::null_mut(),
222                    }
223                };
224
225                match <#type_path as ::rustbridge::PluginFactory>::create(&config) {
226                    Ok(plugin) => {
227                        let plugin: Box<dyn ::rustbridge::Plugin> = Box::new(plugin);
228                        let boxed: Box<Box<dyn ::rustbridge::Plugin>> = Box::new(plugin);
229                        Box::into_raw(boxed) as *mut ::std::ffi::c_void
230                    }
231                    Err(_) => ::std::ptr::null_mut()
232                }
233            }
234        }
235    } else {
236        // Original behavior for ::default or other factory functions
237        quote! {
238            /// FFI entry point - creates a new plugin instance
239            #[unsafe(no_mangle)]
240            pub unsafe extern "C" fn plugin_create() -> *mut ::std::ffi::c_void {
241                let plugin: Box<dyn ::rustbridge::Plugin> = Box::new(#factory_path());
242                let boxed: Box<Box<dyn ::rustbridge::Plugin>> = Box::new(plugin);
243                Box::into_raw(boxed) as *mut ::std::ffi::c_void
244            }
245        }
246    };
247
248    TokenStream::from(expanded)
249}
250
251/// Macro to implement the Plugin trait with handler dispatch
252///
253/// This generates a Plugin implementation that routes messages to handler methods
254/// based on type tags.
255///
256/// # Example
257///
258/// ```ignore
259/// impl_plugin! {
260///     MyPlugin {
261///         "user.create" => create_user,
262///         "user.delete" => delete_user,
263///     }
264/// }
265/// ```
266#[proc_macro]
267pub fn impl_plugin(input: TokenStream) -> TokenStream {
268    // Parse: PluginType { "tag" => method, ... }
269    let _input_str = input.to_string();
270
271    // For now, generate a simple implementation
272    // Full parsing would require custom syntax handling
273    let expanded = quote! {
274        // Plugin implementation generated by impl_plugin!
275        // Use rustbridge_plugin attribute for full functionality
276    };
277
278    TokenStream::from(expanded)
279}
280
281/// Convert a PascalCase string to snake_case
282fn to_snake_case(s: &str) -> String {
283    let mut result = String::new();
284    for (i, c) in s.chars().enumerate() {
285        if c.is_uppercase() {
286            if i > 0 {
287                result.push('_');
288            }
289            result.push(c.to_ascii_lowercase());
290        } else {
291            result.push(c);
292        }
293    }
294    result
295}
296
297#[cfg(test)]
298mod lib_tests;