diff --git a/Cargo.lock b/Cargo.lock index b006c67..d3e84d2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -187,6 +187,7 @@ dependencies = [ "once_cell", "rand", "tests-scripts-lib", + "thiserror", ] [[package]] @@ -276,9 +277,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.33" +version = "1.0.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" dependencies = [ "proc-macro2", ] @@ -350,9 +351,9 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "syn" -version = "2.0.38" +version = "2.0.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e96b79aaa137db8f61e26363a0c9b47d8b4ec75da28b7d1d614c2303e232408b" +checksum = "1fceb41e3d546d0bd83421d3409b1460cc7444cd389341a4c880fe7a042cb3d7" dependencies = [ "proc-macro2", "quote", @@ -366,6 +367,26 @@ dependencies = [ "godot-rust-script", ] +[[package]] +name = "thiserror" +version = "1.0.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "unicode-ident" version = "1.0.12" diff --git a/Cargo.toml b/Cargo.toml index 2cefdd5..ab736a6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,6 +19,7 @@ proc-macro2 = "1.0.68" quote = "1.0.33" syn = "2.0.38" const-str = "0.5.6" +thiserror = "1" godot-rust-script-derive = { path = "derive" } tests-scripts-lib = { path = "tests-scripts-lib" } diff --git a/derive/src/impl_attribute.rs b/derive/src/impl_attribute.rs index 71bacec..588a0bd 100644 --- a/derive/src/impl_attribute.rs +++ b/derive/src/impl_attribute.rs @@ -6,10 +6,13 @@ use proc_macro2::TokenStream; use quote::{quote, quote_spanned, ToTokens}; -use syn::{parse_macro_input, spanned::Spanned, FnArg, ImplItem, ItemImpl, ReturnType, Type}; +use syn::{ + parse2, parse_macro_input, spanned::Spanned, FnArg, Ident, ImplItem, ImplItemFn, ItemImpl, + PatIdent, PatType, ReturnType, Token, Type, Visibility, +}; use crate::{ - is_context_type, rust_to_variant_type, + compile_error, is_context_type, rust_to_variant_type, type_paths::{godot_types, property_hints, string_name_ty, variant_ty}, }; @@ -173,12 +176,166 @@ pub fn godot_script_impl( ); }; + let pub_interface = generate_public_interface(&body); + quote! { #body #trait_impl + #pub_interface + #metadata } .into() } + +fn extract_script_name_from_type(impl_target: &syn::Type) -> Result { + match impl_target { + Type::Array(_) => Err(compile_error("Arrays are not supported!", impl_target)), + Type::BareFn(_) => Err(compile_error( + "Bare functions are not supported!", + impl_target, + )), + Type::Group(_) => Err(compile_error("Groups are not supported!", impl_target)), + Type::ImplTrait(_) => Err(compile_error("Impl traits are not suppored!", impl_target)), + Type::Infer(_) => Err(compile_error("Infer is not supported!", impl_target)), + Type::Macro(_) => Err(compile_error("Macro types are not supported!", impl_target)), + Type::Never(_) => Err(compile_error("Never type is not supported!", impl_target)), + Type::Paren(_) => Err(compile_error("Unsupported type!", impl_target)), + Type::Path(ref path) => Ok(path.path.segments.last().unwrap().ident.clone()), + Type::Ptr(_) => Err(compile_error( + "Pointer types are not supported!", + impl_target, + )), + Type::Reference(_) => Err(compile_error("References are not supported!", impl_target)), + Type::Slice(_) => Err(compile_error("Slices are not supported!", impl_target)), + Type::TraitObject(_) => Err(compile_error( + "Trait objects are not supported!", + impl_target, + )), + Type::Tuple(_) => Err(compile_error("Tuples are not supported!", impl_target)), + Type::Verbatim(_) => Err(compile_error("Verbatim is not supported!", impl_target)), + _ => Err(compile_error("Unsupported type!", impl_target)), + } +} + +fn sanitize_trait_fn_arg(arg: FnArg) -> FnArg { + match arg { + FnArg::Receiver(mut rec) => { + rec.mutability = Some(Token![mut](rec.span())); + rec.ty = parse2(quote!(&mut Self)).unwrap(); + + FnArg::Receiver(rec) + } + FnArg::Typed(ty) => FnArg::Typed(PatType { + attrs: ty.attrs, + pat: match *ty.pat { + syn::Pat::Const(_) + | syn::Pat::Lit(_) + | syn::Pat::Macro(_) + | syn::Pat::Or(_) + | syn::Pat::Paren(_) + | syn::Pat::Path(_) + | syn::Pat::Range(_) + | syn::Pat::Reference(_) + | syn::Pat::Rest(_) + | syn::Pat::Slice(_) + | syn::Pat::Struct(_) + | syn::Pat::Tuple(_) + | syn::Pat::TupleStruct(_) + | syn::Pat::Type(_) + | syn::Pat::Verbatim(_) + | syn::Pat::Wild(_) => ty.pat, + syn::Pat::Ident(ident_pat) => Box::new(syn::Pat::Ident(PatIdent { + attrs: ident_pat.attrs, + by_ref: None, + mutability: None, + ident: ident_pat.ident, + subpat: None, + })), + _ => ty.pat, + }, + colon_token: ty.colon_token, + ty: ty.ty, + }), + } +} + +fn generate_public_interface(impl_body: &ItemImpl) -> TokenStream { + let impl_target = impl_body.self_ty.as_ref(); + let script_name = match extract_script_name_from_type(impl_target) { + Ok(target) => target, + Err(err) => return err, + }; + + let trait_name = Ident::new(&format!("I{}", script_name), script_name.span()); + + let functions: Vec<_> = impl_body + .items + .iter() + .filter_map(|func| match func { + ImplItem::Fn(func @ ImplItemFn{ vis: Visibility::Public(_), .. }) => Some(func), + _ => None, + }) + .map(|func| { + let mut sig = func.sig.clone(); + + sig.inputs = sig + .inputs + .into_iter() + .filter(|arg| { + !matches!(arg, FnArg::Typed(PatType { attrs: _, pat: _, colon_token: _, ty }) if matches!(ty.as_ref(), Type::Path(path) if path.path.segments.last().unwrap().ident == "Context")) + }) + .map(sanitize_trait_fn_arg) + .collect(); + sig + }) + .collect(); + + let function_defs: TokenStream = functions + .iter() + .map(|func| quote_spanned! { func.span() => #func; }) + .collect(); + let function_impls: TokenStream = functions + .iter() + .map(|func| { + let func_name = func.ident.to_string(); + let args: TokenStream = func + .inputs + .iter() + .filter_map(|arg| match arg { + FnArg::Receiver(_) => None, + FnArg::Typed(arg) => Some(arg), + }) + .map(|arg| { + let pat = arg.pat.clone(); + + quote_spanned! { pat.span() => + ::godot::meta::ToGodot::to_variant(&#pat), + } + }) + .collect(); + + quote_spanned! { func.span() => + #func { + (*self).call(#func_name.into(), &[#args]).to() + } + } + }) + .collect(); + + quote! { + #[automatically_derived] + #[allow(dead_code)] + pub trait #trait_name { + #function_defs + } + + #[automatically_derived] + #[allow(dead_code)] + impl #trait_name for ::godot_rust_script::RsRef<#impl_target> { + #function_impls + } + } +} diff --git a/derive/src/lib.rs b/derive/src/lib.rs index cd9ca2e..3e4915a 100644 --- a/derive/src/lib.rs +++ b/derive/src/lib.rs @@ -34,6 +34,7 @@ pub fn derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream { .unwrap_or_else(|| quote!(::godot_rust_script::godot::prelude::RefCounted)); let script_type_ident = opts.ident; + let class_name = script_type_ident.to_string(); let fields = opts.data.take_struct().unwrap().fields; let public_fields = fields.iter().filter(|field| { @@ -129,6 +130,8 @@ pub fn derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream { let output = quote! { impl ::godot_rust_script::GodotScript for #script_type_ident { type Base = #base_class; + + const CLASS_NAME: &'static str = #class_name; #get_fields_impl @@ -383,3 +386,7 @@ pub fn godot_script_impl( ) -> proc_macro::TokenStream { impl_attribute::godot_script_impl(args, body) } + +fn compile_error(message: &str, tokens: impl ToTokens) -> TokenStream { + syn::Error::new_spanned(tokens, message).into_compile_error() +} diff --git a/rust-script/Cargo.toml b/rust-script/Cargo.toml index bb5af51..c61761b 100644 --- a/rust-script/Cargo.toml +++ b/rust-script/Cargo.toml @@ -15,6 +15,7 @@ rand = { workspace = true, optional = true } godot-rust-script-derive = { workspace = true, optional = true } once_cell = "1.19.0" const-str.workspace = true +thiserror.workspace = true [dev-dependencies] tests-scripts-lib = { path = "../tests-scripts-lib" } diff --git a/rust-script/src/lib.rs b/rust-script/src/lib.rs index 50aa087..0a06f19 100644 --- a/rust-script/src/lib.rs +++ b/rust-script/src/lib.rs @@ -23,6 +23,7 @@ pub use godot_rust_script_derive::{godot_script_impl, GodotScript}; pub mod private_export { pub use super::shared::__godot_rust_plugin_SCRIPT_REGISTRY; + pub use crate::script_registry::RsRefOwner; pub use const_str::{concat, replace, strip_prefix, unwrap}; pub use godot::sys::{plugin_add, plugin_registry}; } diff --git a/rust-script/src/library.rs b/rust-script/src/library.rs index b474362..c67804f 100644 --- a/rust-script/src/library.rs +++ b/rust-script/src/library.rs @@ -13,12 +13,12 @@ use godot::{ sys::VariantType, }; +pub use crate::script_registry::{ + CastToScript, GodotScript, GodotScriptImpl, RsRef, RustScriptMetaData, RustScriptMethodInfo, +}; use crate::script_registry::{ CreateScriptInstanceData, GodotScriptObject, RustScriptPropertyInfo, RustScriptSignalInfo, }; -pub use crate::script_registry::{ - GodotScript, GodotScriptImpl, RustScriptMetaData, RustScriptMethodInfo, -}; pub use signals::{ScriptSignal, Signal, SignalArguments}; mod signals; diff --git a/rust-script/src/runtime/mod.rs b/rust-script/src/runtime/mod.rs index 7ced174..bff3cd2 100644 --- a/rust-script/src/runtime/mod.rs +++ b/rust-script/src/runtime/mod.rs @@ -29,6 +29,7 @@ use crate::{ use self::rust_script_language::RustScriptLanguage; +pub(crate) use rust_script::RustScript; pub use rust_script_instance::{Context, GenericContext}; #[macro_export] diff --git a/rust-script/src/runtime/rust_script.rs b/rust-script/src/runtime/rust_script.rs index c5a63d3..227eb44 100644 --- a/rust-script/src/runtime/rust_script.rs +++ b/rust-script/src/runtime/rust_script.rs @@ -33,7 +33,7 @@ const NOTIFICATION_EXTENSION_RELOADED: i32 = 2; #[derive(GodotClass)] #[class(base = ScriptExtension, tool)] -pub(super) struct RustScript { +pub(crate) struct RustScript { #[var(get = get_class_name, set = set_class_name, usage_flags = [STORAGE])] class_name: GString, diff --git a/rust-script/src/script_registry.rs b/rust-script/src/script_registry.rs index f775ad8..7b1733f 100644 --- a/rust-script/src/script_registry.rs +++ b/rust-script/src/script_registry.rs @@ -4,6 +4,8 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +use std::marker::PhantomData; +use std::ops::{Deref, DerefMut}; use std::{any::Any, collections::HashMap, fmt::Debug, sync::Arc}; use godot::global::{MethodFlags, PropertyHint, PropertyUsageFlags}; @@ -17,6 +19,8 @@ use crate::runtime::{Context, GenericContext}; pub trait GodotScript: Debug + GodotScriptImpl { type Base: Inherits; + const CLASS_NAME: &'static str; + fn set(&mut self, name: StringName, value: Variant) -> bool; fn get(&self, name: StringName) -> Option; fn call( @@ -244,3 +248,127 @@ where self(base) } } + +pub trait RsRefOwner { + fn owner_mut(&mut self) -> &mut Gd; +} + +#[derive(Debug)] +pub struct RsRef { + owner: Gd, + script_ty: PhantomData, +} + +impl RsRef { + pub(crate) fn new + Inherits>(owner: Gd) -> Self { + Self { + owner: owner.upcast(), + script_ty: PhantomData, + } + } + + fn validate_script>(owner: &Gd) -> Option { + let script = owner + .upcast_ref::() + .get_script() + .try_to::>>(); + + let Ok(script) = script else { + return Some(GodotScriptCastError::NotRustScript); + }; + + let Some(script) = script else { + return Some(GodotScriptCastError::NoScriptAttached); + }; + + let class_name = script.bind().str_class_name(); + + (class_name != T::CLASS_NAME).then(|| { + GodotScriptCastError::ClassMismatch(T::CLASS_NAME, script.get_class().to_string()) + }) + } +} + +impl Deref for RsRef { + type Target = Gd; + + fn deref(&self) -> &Self::Target { + &self.owner + } +} + +impl DerefMut for RsRef { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.owner + } +} + +impl Clone for RsRef { + fn clone(&self) -> Self { + Self { + owner: self.owner.clone(), + script_ty: PhantomData, + } + } +} + +#[derive(thiserror::Error, Debug)] +pub enum GodotScriptCastError { + #[error("Object has no script attached!")] + NoScriptAttached, + + #[error("Script attached to object is not a RustScript!")] + NotRustScript, + + #[error( + "Script attached to object does not match expected script class `{0}` but found `{1}`!" + )] + ClassMismatch(&'static str, String), +} + +pub trait CastToScript { + fn try_to_script(&self) -> Result, GodotScriptCastError>; + fn try_into_script(self) -> Result, GodotScriptCastError>; + fn to_script(&self) -> RsRef; + fn into_script(self) -> RsRef; +} + +impl + Inherits> CastToScript for Gd { + fn try_to_script(&self) -> Result, GodotScriptCastError> { + if let Some(err) = RsRef::::validate_script(self) { + return Err(err); + } + + Ok(RsRef::new(self.clone())) + } + + fn try_into_script(self) -> Result, GodotScriptCastError> { + if let Some(err) = RsRef::::validate_script(&self) { + return Err(err); + } + + Ok(RsRef::new(self)) + } + + fn to_script(&self) -> RsRef { + self.try_to_script().unwrap_or_else(|err| { + panic!( + "`{}` was assumed to have rust script `{}`, but this was not the case at runtime!\nError: {}", + B::class_name(), + T::CLASS_NAME, + err, + ); + }) + } + + fn into_script(self) -> RsRef { + self.try_into_script().unwrap_or_else(|err| { + panic!( + "`{}` was assumed to have rust script `{}`, but this was not the case at runtime!\nError: {}", + B::class_name(), + T::CLASS_NAME, + err + ); + }) + } +} diff --git a/rust-script/tests/script_derive.rs b/rust-script/tests/script_derive.rs index 941d26a..92719a1 100644 --- a/rust-script/tests/script_derive.rs +++ b/rust-script/tests/script_derive.rs @@ -24,6 +24,8 @@ struct TestScript { #[godot_script_impl] impl TestScript { + pub fn _init(&self) {} + pub fn record(&mut self, value: u8) -> bool { value > 2 }