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;