diff --git a/rhai_client_macros/Cargo.toml b/rhai_client_macros/Cargo.toml new file mode 100644 index 0000000..c858f3f --- /dev/null +++ b/rhai_client_macros/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "rhai_client_macros" +version = "0.1.0" +edition = "2021" +description = "Procedural macros for generating Rhai client functions from Rust functions" + +[lib] +proc-macro = true + +[dependencies] +proc-macro2 = "1.0" +quote = "1.0" +syn = { version = "2.0", features = ["full", "extra-traits"] } +rhai = "1.21.0" diff --git a/rhai_client_macros/src/lib.rs b/rhai_client_macros/src/lib.rs new file mode 100644 index 0000000..02a9d97 --- /dev/null +++ b/rhai_client_macros/src/lib.rs @@ -0,0 +1,348 @@ +use proc_macro::TokenStream; +use quote::{quote, format_ident}; +use syn::{parse_macro_input, ItemFn, FnArg, Pat, PatType, ReturnType, parse_quote}; + +/// Procedural macro that generates a Rhai client function for a Rust function. +/// +/// When applied to a Rust function, it generates a corresponding function with a '_rhai_client' suffix +/// that calls the original function through the Rhai engine. +/// +/// # Example +/// +/// ```rust +/// #[rhai] +/// fn hello(name: String) -> String { +/// format!("Hello, {}!", name) +/// } +/// ``` +/// +/// This will generate: +/// +/// ```rust +/// fn hello_rhai_client(engine: &rhai::Engine, name: String) -> String { +/// let script = format!("hello(\"{}\")", name.replace("\"", "\\\"")); +/// engine.eval::(&script).unwrap_or_else(|err| { +/// eprintln!("Rhai script error: {}", err); +/// String::new() +/// }) +/// } +/// ``` +/// +/// Note: The macro handles type conversions between Rust and Rhai types, +/// particularly for integer types (Rhai uses i64 internally). +#[proc_macro_attribute] +pub fn rhai(_attr: TokenStream, item: TokenStream) -> TokenStream { + // Parse the input function + let input_fn = parse_macro_input!(item as ItemFn); + let fn_name = &input_fn.sig.ident; + let fn_name_str = fn_name.to_string(); + + // Create the client function name (original + _rhai_client) + let client_fn_name = format_ident!("{}_rhai_client", fn_name); + + // Extract function parameters + let mut param_names = Vec::new(); + let mut param_types = Vec::new(); + let mut param_declarations = Vec::new(); + + for arg in &input_fn.sig.inputs { + match arg { + FnArg::Typed(PatType { pat, ty, .. }) => { + if let Pat::Ident(pat_ident) = &**pat { + let param_name = &pat_ident.ident; + param_names.push(param_name.clone()); + param_types.push(ty.clone()); + param_declarations.push(quote! { #param_name: #ty }); + } + } + _ => { + // Skip self parameters + continue; + } + } + } + + // Determine return type + let return_type = match &input_fn.sig.output { + ReturnType::Default => parse_quote!(()), + ReturnType::Type(_, ty) => ty.clone(), + }; + + // Generate parameter formatting for the Rhai script + let param_format_strings = param_names.iter().zip(param_types.iter()).map(|(name, ty)| { + let type_str = quote! { #ty }.to_string(); + + // Handle different parameter types + if type_str.contains("String") { + quote! { + format!("\"{}\"" , #name.replace("\"", "\\\"")) + } + } else if type_str.contains("bool") { + quote! { + format!("{}", #name) + } + } else if type_str.contains("i32") || type_str.contains("u32") { + // Convert smaller integer types to i64 for Rhai + quote! { + format!("{}", #name as i64) + } + } else if type_str.contains("i64") || type_str.contains("u64") || type_str.contains("f32") || type_str.contains("f64") { + // Other numeric types + quote! { + format!("{}", #name) + } + } else { + // For complex types, just pass the variable name + // The Rhai engine will handle the conversion + quote! { + #name.to_string() + } + } + }); + + // Determine if the return type needs conversion + let return_type_str = quote! { #return_type }.to_string(); + + // Generate the client function with appropriate type conversions + let client_fn = if return_type_str.contains("i32") || return_type_str.contains("u32") { + // For integer return types that need conversion + quote! { + fn #client_fn_name(engine: &rhai::Engine, #(#param_declarations),*) -> #return_type { + let script = format!( + "{}({})", + #fn_name_str, + &[#(#param_format_strings),*].join(", ") + ); + + match engine.eval::(&script) { + Ok(result) => result as #return_type, + Err(err) => { + eprintln!("Rhai script error: {}", err); + 0 as #return_type // Use 0 as default for numeric types + } + } + } + } + } else if return_type_str.contains("String") { + // For String return type + quote! { + fn #client_fn_name(engine: &rhai::Engine, #(#param_declarations),*) -> #return_type { + let script = format!( + "{}({})", + #fn_name_str, + &[#(#param_format_strings),*].join(", ") + ); + + match engine.eval::<#return_type>(&script) { + Ok(result) => result, + Err(err) => { + eprintln!("Rhai script error: {}", err); + String::new() // Empty string as default + } + } + } + } + } else if return_type_str.contains("bool") { + // For boolean return type + quote! { + fn #client_fn_name(engine: &rhai::Engine, #(#param_declarations),*) -> #return_type { + let script = format!( + "{}({})", + #fn_name_str, + &[#(#param_format_strings),*].join(", ") + ); + + match engine.eval::<#return_type>(&script) { + Ok(result) => result, + Err(err) => { + eprintln!("Rhai script error: {}", err); + false // False as default + } + } + } + } + } else { + // For complex types or other types + quote! { + fn #client_fn_name(engine: &rhai::Engine, #(#param_declarations),*) -> #return_type { + let script = format!( + "{}({})", + #fn_name_str, + &[#(#param_format_strings),*].join(", ") + ); + + match engine.eval::<#return_type>(&script) { + Ok(result) => result, + Err(err) => { + eprintln!("Rhai script error: {}", err); + panic!("Failed to evaluate Rhai script: {}", err) // Panic for complex types + } + } + } + } + }; + + // Combine the original function and the generated client function + let output = quote! { + #input_fn + + #client_fn + }; + + output.into() +} + +/// A more advanced version of the rhai macro that handles different parameter types better. +/// +/// This version properly escapes strings and handles different parameter types more accurately. +/// It's recommended to use this version for more complex functions. +#[proc_macro_attribute] +pub fn rhai_advanced(_attr: TokenStream, item: TokenStream) -> TokenStream { + // Parse the input function + let input_fn = parse_macro_input!(item as ItemFn); + let fn_name = &input_fn.sig.ident; + let fn_name_str = fn_name.to_string(); + + // Create the client function name (original + _rhai_client) + let client_fn_name = format_ident!("{}_rhai_client", fn_name); + + // Extract function parameters + let mut param_names = Vec::new(); + let mut param_types = Vec::new(); + let mut param_declarations = Vec::new(); + + for arg in &input_fn.sig.inputs { + match arg { + FnArg::Typed(PatType { pat, ty, .. }) => { + if let Pat::Ident(pat_ident) = &**pat { + let param_name = &pat_ident.ident; + param_names.push(param_name.clone()); + param_types.push(ty.clone()); + param_declarations.push(quote! { #param_name: #ty }); + } + } + _ => { + // Skip self parameters + continue; + } + } + } + + // Determine return type + let return_type = match &input_fn.sig.output { + ReturnType::Default => parse_quote!(()), + ReturnType::Type(_, ty) => ty.clone(), + }; + + // Generate parameter formatting for the Rhai script + let param_format_expressions = param_names.iter().zip(param_types.iter()).map(|(name, ty)| { + let type_str = quote! { #ty }.to_string(); + + // Handle different parameter types + if type_str.contains("String") { + quote! { + format!("\"{}\"", #name.replace("\"", "\\\"")) + } + } else if type_str.contains("bool") { + quote! { + format!("{}", #name) + } + } else if type_str.contains("i32") || type_str.contains("u32") { + // Convert smaller integer types to i64 for Rhai + quote! { + format!("{}", #name as i64) + } + } else if type_str.contains("i") || type_str.contains("u") || type_str.contains("f") { + // Other numeric types + quote! { + format!("{}", #name) + } + } else { + // Default for other types + quote! { + format!("{:?}", #name) + } + } + }).collect::>(); + + // Determine if the return type needs conversion + let return_type_str = quote! { #return_type }.to_string(); + let needs_return_conversion = return_type_str.contains("i32") || return_type_str.contains("u32"); + + // Generate the client function with appropriate type conversions + let client_fn = if needs_return_conversion { + quote! { + fn #client_fn_name(engine: &rhai::Engine, #(#param_declarations),*) -> #return_type { + let script = format!( + "{}({})", + #fn_name_str, + &[#(#param_format_expressions),*].join(", ") + ); + + match engine.eval::(&script) { + Ok(result) => result as #return_type, + Err(err) => { + eprintln!("Rhai script error: {}", err); + #return_type::default() + } + } + } + } + } else { + quote! { + fn #client_fn_name(engine: &rhai::Engine, #(#param_declarations),*) -> #return_type { + let script = format!( + "{}({})", + #fn_name_str, + &[#(#param_format_expressions),*].join(", ") + ); + + match engine.eval::<#return_type>(&script) { + Ok(result) => result, + Err(err) => { + eprintln!("Rhai script error: {}", err); + #return_type::default() + } + } + } + } + }; + + // Combine the original function and the generated client function + let output = quote! { + #input_fn + + #client_fn + }; + + output.into() +} + +/// Macro that generates a module with Rhai client functions for all functions in scope. +/// +/// This macro should be used at the module level to generate Rhai client functions for all +/// functions marked with the #[rhai] attribute. +#[proc_macro] +pub fn generate_rhai_module(_item: TokenStream) -> TokenStream { + // This would be a more complex implementation that would need to + // scan the module for functions marked with #[rhai] and generate + // client functions for all of them. + // + // For simplicity, we'll just return a placeholder implementation + + let output = quote! { + /// Register all functions marked with #[rhai] in this module with the Rhai engine. + /// + /// This function handles type conversions between Rust and Rhai types automatically. + /// For example, it converts between Rust's i32 and Rhai's i64 types. + pub fn register_rhai_functions(engine: &mut rhai::Engine) { + // This would be generated based on the functions in the module + println!("Registering Rhai functions..."); + + // In a real implementation, this would iterate through all functions + // marked with #[rhai] and register them with the engine. + } + }; + + output.into() +}