palladium-plugin-derive 0.7.0

Derive macros for Palladium actor messages and plugins
Documentation
extern crate proc_macro;

use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, Attribute, DeriveInput, ItemStruct, LitStr};

// ── Helper: parse the response type from #[pd_message(response = Type)] ──────

fn find_response_type(attrs: &[Attribute]) -> Option<syn::Type> {
    for attr in attrs {
        if !attr.path().is_ident("pd_message") {
            continue;
        }
        let mut response: Option<syn::Type> = None;
        let _ = attr.parse_nested_meta(|meta| {
            if meta.path.is_ident("response") {
                let value: syn::Type = meta.value()?.parse()?;
                response = Some(value);
                Ok(())
            } else {
                Err(meta.error("unknown pd_message attribute key"))
            }
        });
        return response;
    }
    None
}

// ── #[derive(Message)] ────────────────────────────────────────────────────────

/// Derive macro for `palladium_actor::Message`.
///
/// Generates a `TYPE_TAG` constant via FNV-1a of the fully-qualified type name.
///
/// # Usage
///
/// ```ignore
/// use palladium_actor::Message;
///
/// #[derive(Message)]
/// #[pd_message(response = u64)]
/// struct Ping(u64);
/// ```
///
/// The optional `#[pd_message(response = T)]` attribute sets `Message::Response`.
/// Omitting it defaults `Response` to `()`.
#[proc_macro_derive(Message, attributes(pd_message))]
pub fn derive_message(input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as DeriveInput);
    let name = &input.ident;

    // Response type defaults to `()` when no attribute is present.
    let response_type: syn::Type =
        find_response_type(&input.attrs).unwrap_or_else(|| syn::parse_str("()").unwrap());

    let expanded = quote! {
        impl ::palladium_actor::Message for #name {
            type Response = #response_type;
            const TYPE_TAG: u64 = ::palladium_actor::fnv1a_64(concat!(module_path!(), "::", stringify!(#name)));
        }
    };

    TokenStream::from(expanded)
}

// ── #[palladium_actor] helpers ───────────────────────────────────────────────────────

/// Convert `CamelCase` to `snake_case` for the default plugin name.
fn to_snake_case(s: &str) -> String {
    let mut out = String::with_capacity(s.len() + 4);
    for (i, c) in s.chars().enumerate() {
        if c.is_uppercase() && i > 0 {
            out.push('_');
        }
        out.extend(c.to_lowercase());
    }
    out
}

/// Parse `"major.minor.patch"` into three `u32` values.
fn parse_version(s: &str) -> (u32, u32, u32) {
    let mut parts = s.splitn(3, '.').map(|p| p.parse::<u32>().unwrap_or(0));
    (
        parts.next().unwrap_or(0),
        parts.next().unwrap_or(0),
        parts.next().unwrap_or(0),
    )
}

/// Parse optional `name = "..."` and `version = "..."` from the attribute args.
///
/// Returns `(name, version)`, both `None` if absent.
fn parse_pd_actor_args(attr: TokenStream) -> (Option<String>, Option<String>) {
    if attr.is_empty() {
        return (None, None);
    }

    let mut name: Option<String> = None;
    let mut version: Option<String> = None;

    let parser = syn::meta::parser(|meta| {
        if meta.path.is_ident("name") {
            let s: LitStr = meta.value()?.parse()?;
            name = Some(s.value());
            Ok(())
        } else if meta.path.is_ident("version") {
            let s: LitStr = meta.value()?.parse()?;
            version = Some(s.value());
            Ok(())
        } else {
            Err(meta.error("unknown palladium_actor argument; expected `name` or `version`"))
        }
    });

    // Ignore parse errors from malformed attributes (they surface as compile
    // errors from syn anyway).
    let _ = syn::parse::Parser::parse(parser, attr);

    (name, version)
}

// ── #[palladium_actor] ───────────────────────────────────────────────────────────────

/// Attribute macro for native plugin actor structs.
///
/// Place `#[palladium_actor]` before a struct that implements `palladium_actor::Actor` to
/// generate the C ABI exports required by the Palladium plugin host.
///
/// # Requirements
///
/// * The annotated struct must implement `Default` (used by `pd_actor_create`).
/// * The crate must also depend on `pd-plugin-api` and `pd-plugin`.
/// * The crate must allow `unsafe_code` (e.g. `[lints.rust] unsafe_code = "allow"`).
///
/// # Generated symbols
///
/// | Symbol | Signature |
/// |--------|-----------|
/// | `pd_plugin_init` | `unsafe extern "C" fn() -> *const PdPluginInfo` |
/// | `pd_actor_create` | `unsafe extern "C" fn(*const u8, u32, *const u8, u32) -> *mut c_void` |
/// | `pd_actor_destroy` | `unsafe extern "C" fn(*mut c_void)` |
/// | `pd_actor_on_start` | `unsafe extern "C" fn(*mut c_void, *mut PdActorContext) -> i32` |
/// | `pd_actor_on_message` | `unsafe extern "C" fn(*mut c_void, *mut PdActorContext, *const u8, *const u8, u32) -> i32` |
/// | `pd_actor_on_stop` | `unsafe extern "C" fn(*mut c_void, *mut PdActorContext, i32)` |
///
/// # Optional arguments
///
/// ```rust,ignore
/// #[palladium_actor(name = "my_plugin", version = "1.2.3")]
/// struct MyActor;
/// ```
///
/// * `name` — plugin name string (default: struct name in `snake_case`).
/// * `version` — semantic version string (default: `"0.1.0"`).
#[proc_macro_attribute]
pub fn palladium_actor(attr: TokenStream, item: TokenStream) -> TokenStream {
    let (name_opt, version_opt) = parse_pd_actor_args(attr);
    let input = parse_macro_input!(item as ItemStruct);
    let struct_name = &input.ident;

    // Plugin name: from attribute or struct name in snake_case.
    let plugin_name = name_opt.unwrap_or_else(|| to_snake_case(&struct_name.to_string()));
    let (ver_major, ver_minor, ver_patch) =
        parse_version(&version_opt.unwrap_or_else(|| "0.1.0".to_string()));

    // Byte slices for the static name buffers embedded in pd_plugin_init.
    let plugin_name_bytes: Vec<u8> = plugin_name.bytes().collect();
    let plugin_name_len = plugin_name.len() as u32;
    let type_name_bytes: Vec<u8> = struct_name.to_string().bytes().collect();
    let type_name_len = type_name_bytes.len() as u32;

    let expanded = quote! {
        // Pass through the original struct definition unchanged.
        #input

        // ── C ABI exports ────────────────────────────────────────────────────

        /// Return plugin metadata.  Called once at load time by the engine.
        ///
        /// Uses `Box::leak` for the metadata structs — plugin init is called
        /// once per process lifetime, so the leak is intentional and bounded.
        #[no_mangle]
        pub unsafe extern "C" fn pd_plugin_init()
            -> *const ::palladium_plugin_api::PdPluginInfo
        {
            // &[u8] is Sync; safe to use as function-level statics.
            static __PD_PLUGIN_NAME: &[u8] = &[#(#plugin_name_bytes),*];
            static __PD_ACTOR_TYPE_NAME: &[u8] = &[#(#type_name_bytes),*];

            let types = ::std::boxed::Box::leak(::std::boxed::Box::new([
                ::palladium_plugin_api::PdActorTypeInfo {
                    type_name: __PD_ACTOR_TYPE_NAME.as_ptr(),
                    type_name_len: #type_name_len,
                    config_schema: ::core::ptr::null(),
                    config_schema_len: 0,
                },
            ]));

            ::std::boxed::Box::leak(::std::boxed::Box::new(
                ::palladium_plugin_api::PdPluginInfo {
                    name: __PD_PLUGIN_NAME.as_ptr(),
                    name_len: #plugin_name_len,
                    version_major: #ver_major,
                    version_minor: #ver_minor,
                    version_patch: #ver_patch,
                    abi_version: ::palladium_plugin_api::PD_ABI_VERSION,
                    actor_type_count: 1,
                    actor_types: types.as_ptr(),
                },
            ))
        }

        /// Allocate a new actor instance.  `config` bytes are passed through
        /// to the actor but ignored in the `Default`-based constructor.
        #[no_mangle]
        pub unsafe extern "C" fn pd_actor_create(
            _type_name: *const u8,
            _type_name_len: u32,
            _config: *const u8,
            _config_len: u32,
        ) -> *mut ::core::ffi::c_void {
            ::std::boxed::Box::into_raw(
                ::std::boxed::Box::new(
                    <#struct_name as ::core::default::Default>::default()
                )
            ) as *mut ::core::ffi::c_void
        }

        /// Destroy an actor instance previously created by `pd_actor_create`.
        #[no_mangle]
        pub unsafe extern "C" fn pd_actor_destroy(state: *mut ::core::ffi::c_void) {
            drop(unsafe { ::std::boxed::Box::from_raw(state as *mut #struct_name) });
        }

        /// Call `Actor::on_start`.  Returns 0 on success, -1 on error.
        #[no_mangle]
        pub unsafe extern "C" fn pd_actor_on_start(
            state: *mut ::core::ffi::c_void,
            ctx_ptr: *mut ::palladium_plugin_api::PdActorContext,
        ) -> i32 {
            let actor = unsafe { &mut *(state as *mut #struct_name) };
            let mut ctx = ::palladium_plugin::make_ffi_context(ctx_ptr);
            match ::palladium_actor::Actor::on_start(actor, &mut ctx) {
                Ok(()) => 0,
                Err(_) => -1,
            }
        }

        /// Call `Actor::on_message`.  Returns 0 on success, -2 on error.
        #[no_mangle]
        pub unsafe extern "C" fn pd_actor_on_message(
            state: *mut ::core::ffi::c_void,
            ctx_ptr: *mut ::palladium_plugin_api::PdActorContext,
            envelope_bytes: *const u8,
            payload: *const u8,
            payload_len: u32,
        ) -> i32 {
            let actor = unsafe { &mut *(state as *mut #struct_name) };
            let mut ctx = ::palladium_plugin::make_ffi_context(ctx_ptr);
            let env_buf: [u8; 80] =
                unsafe { *(envelope_bytes as *const [u8; 80]) };
            let envelope = ::palladium_actor::Envelope::from_bytes(&env_buf);
            let mp = if payload.is_null() || payload_len == 0 {
                ::palladium_actor::MessagePayload::serialized(
                    ::std::vec::Vec::<u8>::new()
                )
            } else {
                let s = unsafe {
                    ::std::slice::from_raw_parts(payload, payload_len as usize)
                };
                ::palladium_actor::MessagePayload::serialized(s.to_vec())
            };
            match ::palladium_actor::Actor::on_message(actor, &mut ctx, &envelope, mp) {
                Ok(()) => 0,
                Err(_) => -2,
            }
        }

        /// Call `Actor::on_stop` with the decoded `StopReason`.
        #[no_mangle]
        pub unsafe extern "C" fn pd_actor_on_stop(
            state: *mut ::core::ffi::c_void,
            ctx_ptr: *mut ::palladium_plugin_api::PdActorContext,
            reason: i32,
        ) {
            let actor = unsafe { &mut *(state as *mut #struct_name) };
            let mut ctx = ::palladium_plugin::make_ffi_context(ctx_ptr);
            let stop_reason = match reason {
                0 => ::palladium_actor::StopReason::Normal,
                1 => ::palladium_actor::StopReason::Requested,
                2 => ::palladium_actor::StopReason::Supervisor,
                3 => ::palladium_actor::StopReason::Shutdown,
                4 => ::palladium_actor::StopReason::Killed,
                _ => ::palladium_actor::StopReason::Error(::palladium_actor::ActorError::Handler),
            };
            ::palladium_actor::Actor::on_stop(actor, &mut ctx, stop_reason);
        }
    };

    TokenStream::from(expanded)
}