Skip to main content

palladium_plugin_derive/
lib.rs

1extern crate proc_macro;
2
3use proc_macro::TokenStream;
4use quote::quote;
5use syn::{parse_macro_input, Attribute, DeriveInput, ItemStruct, LitStr};
6
7// ── Helper: parse the response type from #[pd_message(response = Type)] ──────
8
9fn find_response_type(attrs: &[Attribute]) -> Option<syn::Type> {
10    for attr in attrs {
11        if !attr.path().is_ident("pd_message") {
12            continue;
13        }
14        let mut response: Option<syn::Type> = None;
15        let _ = attr.parse_nested_meta(|meta| {
16            if meta.path.is_ident("response") {
17                let value: syn::Type = meta.value()?.parse()?;
18                response = Some(value);
19                Ok(())
20            } else {
21                Err(meta.error("unknown pd_message attribute key"))
22            }
23        });
24        return response;
25    }
26    None
27}
28
29// ── #[derive(Message)] ────────────────────────────────────────────────────────
30
31/// Derive macro for `palladium_actor::Message`.
32///
33/// Generates a `TYPE_TAG` constant via FNV-1a of the fully-qualified type name.
34///
35/// # Usage
36///
37/// ```ignore
38/// use palladium_actor::Message;
39///
40/// #[derive(Message)]
41/// #[pd_message(response = u64)]
42/// struct Ping(u64);
43/// ```
44///
45/// The optional `#[pd_message(response = T)]` attribute sets `Message::Response`.
46/// Omitting it defaults `Response` to `()`.
47#[proc_macro_derive(Message, attributes(pd_message))]
48pub fn derive_message(input: TokenStream) -> TokenStream {
49    let input = parse_macro_input!(input as DeriveInput);
50    let name = &input.ident;
51
52    // Response type defaults to `()` when no attribute is present.
53    let response_type: syn::Type =
54        find_response_type(&input.attrs).unwrap_or_else(|| syn::parse_str("()").unwrap());
55
56    let expanded = quote! {
57        impl ::palladium_actor::Message for #name {
58            type Response = #response_type;
59            const TYPE_TAG: u64 = ::palladium_actor::fnv1a_64(concat!(module_path!(), "::", stringify!(#name)));
60        }
61    };
62
63    TokenStream::from(expanded)
64}
65
66// ── #[palladium_actor] helpers ───────────────────────────────────────────────────────
67
68/// Convert `CamelCase` to `snake_case` for the default plugin name.
69fn to_snake_case(s: &str) -> String {
70    let mut out = String::with_capacity(s.len() + 4);
71    for (i, c) in s.chars().enumerate() {
72        if c.is_uppercase() && i > 0 {
73            out.push('_');
74        }
75        out.extend(c.to_lowercase());
76    }
77    out
78}
79
80/// Parse `"major.minor.patch"` into three `u32` values.
81fn parse_version(s: &str) -> (u32, u32, u32) {
82    let mut parts = s.splitn(3, '.').map(|p| p.parse::<u32>().unwrap_or(0));
83    (
84        parts.next().unwrap_or(0),
85        parts.next().unwrap_or(0),
86        parts.next().unwrap_or(0),
87    )
88}
89
90/// Parse optional `name = "..."` and `version = "..."` from the attribute args.
91///
92/// Returns `(name, version)`, both `None` if absent.
93fn parse_pd_actor_args(attr: TokenStream) -> (Option<String>, Option<String>) {
94    if attr.is_empty() {
95        return (None, None);
96    }
97
98    let mut name: Option<String> = None;
99    let mut version: Option<String> = None;
100
101    let parser = syn::meta::parser(|meta| {
102        if meta.path.is_ident("name") {
103            let s: LitStr = meta.value()?.parse()?;
104            name = Some(s.value());
105            Ok(())
106        } else if meta.path.is_ident("version") {
107            let s: LitStr = meta.value()?.parse()?;
108            version = Some(s.value());
109            Ok(())
110        } else {
111            Err(meta.error("unknown palladium_actor argument; expected `name` or `version`"))
112        }
113    });
114
115    // Ignore parse errors from malformed attributes (they surface as compile
116    // errors from syn anyway).
117    let _ = syn::parse::Parser::parse(parser, attr);
118
119    (name, version)
120}
121
122// ── #[palladium_actor] ───────────────────────────────────────────────────────────────
123
124/// Attribute macro for native plugin actor structs.
125///
126/// Place `#[palladium_actor]` before a struct that implements `palladium_actor::Actor` to
127/// generate the C ABI exports required by the Palladium plugin host.
128///
129/// # Requirements
130///
131/// * The annotated struct must implement `Default` (used by `pd_actor_create`).
132/// * The crate must also depend on `pd-plugin-api` and `pd-plugin`.
133/// * The crate must allow `unsafe_code` (e.g. `[lints.rust] unsafe_code = "allow"`).
134///
135/// # Generated symbols
136///
137/// | Symbol | Signature |
138/// |--------|-----------|
139/// | `pd_plugin_init` | `unsafe extern "C" fn() -> *const PdPluginInfo` |
140/// | `pd_actor_create` | `unsafe extern "C" fn(*const u8, u32, *const u8, u32) -> *mut c_void` |
141/// | `pd_actor_destroy` | `unsafe extern "C" fn(*mut c_void)` |
142/// | `pd_actor_on_start` | `unsafe extern "C" fn(*mut c_void, *mut PdActorContext) -> i32` |
143/// | `pd_actor_on_message` | `unsafe extern "C" fn(*mut c_void, *mut PdActorContext, *const u8, *const u8, u32) -> i32` |
144/// | `pd_actor_on_stop` | `unsafe extern "C" fn(*mut c_void, *mut PdActorContext, i32)` |
145///
146/// # Optional arguments
147///
148/// ```rust,ignore
149/// #[palladium_actor(name = "my_plugin", version = "1.2.3")]
150/// struct MyActor;
151/// ```
152///
153/// * `name` — plugin name string (default: struct name in `snake_case`).
154/// * `version` — semantic version string (default: `"0.1.0"`).
155#[proc_macro_attribute]
156pub fn palladium_actor(attr: TokenStream, item: TokenStream) -> TokenStream {
157    let (name_opt, version_opt) = parse_pd_actor_args(attr);
158    let input = parse_macro_input!(item as ItemStruct);
159    let struct_name = &input.ident;
160
161    // Plugin name: from attribute or struct name in snake_case.
162    let plugin_name = name_opt.unwrap_or_else(|| to_snake_case(&struct_name.to_string()));
163    let (ver_major, ver_minor, ver_patch) =
164        parse_version(&version_opt.unwrap_or_else(|| "0.1.0".to_string()));
165
166    // Byte slices for the static name buffers embedded in pd_plugin_init.
167    let plugin_name_bytes: Vec<u8> = plugin_name.bytes().collect();
168    let plugin_name_len = plugin_name.len() as u32;
169    let type_name_bytes: Vec<u8> = struct_name.to_string().bytes().collect();
170    let type_name_len = type_name_bytes.len() as u32;
171
172    let expanded = quote! {
173        // Pass through the original struct definition unchanged.
174        #input
175
176        // ── C ABI exports ────────────────────────────────────────────────────
177
178        /// Return plugin metadata.  Called once at load time by the engine.
179        ///
180        /// Uses `Box::leak` for the metadata structs — plugin init is called
181        /// once per process lifetime, so the leak is intentional and bounded.
182        #[no_mangle]
183        pub unsafe extern "C" fn pd_plugin_init()
184            -> *const ::palladium_plugin_api::PdPluginInfo
185        {
186            // &[u8] is Sync; safe to use as function-level statics.
187            static __PD_PLUGIN_NAME: &[u8] = &[#(#plugin_name_bytes),*];
188            static __PD_ACTOR_TYPE_NAME: &[u8] = &[#(#type_name_bytes),*];
189
190            let types = ::std::boxed::Box::leak(::std::boxed::Box::new([
191                ::palladium_plugin_api::PdActorTypeInfo {
192                    type_name: __PD_ACTOR_TYPE_NAME.as_ptr(),
193                    type_name_len: #type_name_len,
194                    config_schema: ::core::ptr::null(),
195                    config_schema_len: 0,
196                },
197            ]));
198
199            ::std::boxed::Box::leak(::std::boxed::Box::new(
200                ::palladium_plugin_api::PdPluginInfo {
201                    name: __PD_PLUGIN_NAME.as_ptr(),
202                    name_len: #plugin_name_len,
203                    version_major: #ver_major,
204                    version_minor: #ver_minor,
205                    version_patch: #ver_patch,
206                    abi_version: ::palladium_plugin_api::PD_ABI_VERSION,
207                    actor_type_count: 1,
208                    actor_types: types.as_ptr(),
209                },
210            ))
211        }
212
213        /// Allocate a new actor instance.  `config` bytes are passed through
214        /// to the actor but ignored in the `Default`-based constructor.
215        #[no_mangle]
216        pub unsafe extern "C" fn pd_actor_create(
217            _type_name: *const u8,
218            _type_name_len: u32,
219            _config: *const u8,
220            _config_len: u32,
221        ) -> *mut ::core::ffi::c_void {
222            ::std::boxed::Box::into_raw(
223                ::std::boxed::Box::new(
224                    <#struct_name as ::core::default::Default>::default()
225                )
226            ) as *mut ::core::ffi::c_void
227        }
228
229        /// Destroy an actor instance previously created by `pd_actor_create`.
230        #[no_mangle]
231        pub unsafe extern "C" fn pd_actor_destroy(state: *mut ::core::ffi::c_void) {
232            drop(unsafe { ::std::boxed::Box::from_raw(state as *mut #struct_name) });
233        }
234
235        /// Call `Actor::on_start`.  Returns 0 on success, -1 on error.
236        #[no_mangle]
237        pub unsafe extern "C" fn pd_actor_on_start(
238            state: *mut ::core::ffi::c_void,
239            ctx_ptr: *mut ::palladium_plugin_api::PdActorContext,
240        ) -> i32 {
241            let actor = unsafe { &mut *(state as *mut #struct_name) };
242            let mut ctx = ::palladium_plugin::make_ffi_context(ctx_ptr);
243            match ::palladium_actor::Actor::on_start(actor, &mut ctx) {
244                Ok(()) => 0,
245                Err(_) => -1,
246            }
247        }
248
249        /// Call `Actor::on_message`.  Returns 0 on success, -2 on error.
250        #[no_mangle]
251        pub unsafe extern "C" fn pd_actor_on_message(
252            state: *mut ::core::ffi::c_void,
253            ctx_ptr: *mut ::palladium_plugin_api::PdActorContext,
254            envelope_bytes: *const u8,
255            payload: *const u8,
256            payload_len: u32,
257        ) -> i32 {
258            let actor = unsafe { &mut *(state as *mut #struct_name) };
259            let mut ctx = ::palladium_plugin::make_ffi_context(ctx_ptr);
260            let env_buf: [u8; 80] =
261                unsafe { *(envelope_bytes as *const [u8; 80]) };
262            let envelope = ::palladium_actor::Envelope::from_bytes(&env_buf);
263            let mp = if payload.is_null() || payload_len == 0 {
264                ::palladium_actor::MessagePayload::serialized(
265                    ::std::vec::Vec::<u8>::new()
266                )
267            } else {
268                let s = unsafe {
269                    ::std::slice::from_raw_parts(payload, payload_len as usize)
270                };
271                ::palladium_actor::MessagePayload::serialized(s.to_vec())
272            };
273            match ::palladium_actor::Actor::on_message(actor, &mut ctx, &envelope, mp) {
274                Ok(()) => 0,
275                Err(_) => -2,
276            }
277        }
278
279        /// Call `Actor::on_stop` with the decoded `StopReason`.
280        #[no_mangle]
281        pub unsafe extern "C" fn pd_actor_on_stop(
282            state: *mut ::core::ffi::c_void,
283            ctx_ptr: *mut ::palladium_plugin_api::PdActorContext,
284            reason: i32,
285        ) {
286            let actor = unsafe { &mut *(state as *mut #struct_name) };
287            let mut ctx = ::palladium_plugin::make_ffi_context(ctx_ptr);
288            let stop_reason = match reason {
289                0 => ::palladium_actor::StopReason::Normal,
290                1 => ::palladium_actor::StopReason::Requested,
291                2 => ::palladium_actor::StopReason::Supervisor,
292                3 => ::palladium_actor::StopReason::Shutdown,
293                4 => ::palladium_actor::StopReason::Killed,
294                _ => ::palladium_actor::StopReason::Error(::palladium_actor::ActorError::Handler),
295            };
296            ::palladium_actor::Actor::on_stop(actor, &mut ctx, stop_reason);
297        }
298    };
299
300    TokenStream::from(expanded)
301}