You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
I have recently been toying around with some nicer way for interfacing with Godot classes / types that are defined outside of the engine or the rust gdextension, i.e. in GDScript C# or another extension.
Currently, we can use Object::call(...) but it can be quite error-prone as we have to repeat and duplicate the calls to the same functions across all call sites. It would be much nicer if we could define the expected interface once and then re-use everywhere without having to resort to string-based method names and remembering the right argument orders.
Idea
My idea was to allow the user to define traits for the foreign interfaces they expect and then provide a proc-macro that generates a duck-type implementation against a Gd<T>.
// ------------- types and traits provided by gdext -------------/// Wrapper around Gd<T> on which traits will be implemented. This type cannot be constructed or /// obtained by the consumer.pubstructForeignClass(Gd<Object>);implForeignClass{/// runtime validation of function signatures. could also take a full MethodInfo.pubfnhas_method(&self,name:StringName,args:&[VariantType],ret:VariantType) -> bool{let method = self.0.get_method_list().iter_shared().find(|method| {
method
.get("name").map(|method| method.to::<StringName>() == name).unwrap_or(false)});letSome(method_args) = method.as_ref().and_then(|method| method.get("args"))else{returnfalse;};let method_args = method_args.to::<Array<Dictionary>>();let matches = args
.iter().enumerate().map(|(index, arg)| {
method_args
.try_get(index).map(|m_arg| &m_arg.get_or_nil("type").to::<VariantType>() == arg).unwrap_or(false)}).all(|item| item);let return_matches = method
.and_then(|method| method.get("return")).map(|r| r.to::<VariantType>() == ret).unwrap_or(false);
matches && return_matches
}/// pass through to Object::callpubfncall(&mutself,name:StringName,args:&[Variant]) -> Variant{self.0.call(name, args)}}/// helper trait for casting the foreign class into a trait object.pubtraitForeignClassObject{typeBase:GodotClass;fnfrom_foreign(fc:Box<ForeignClass>) -> Result<Box<Self>,ForeignClassError>;}/// extension trait for Gd, can be merged into the Gd impl.pubtraitGdExt<T:GodotClass>{fntry_to_foreign<O>(&self) -> Result<Box<O>,ForeignClassError>whereO:ForeignClassObject + ?Sized,T:Inherits<O::Base> + Inherits<Object>;}impl<T:GodotClass>GdExt<T>forGd<T>{/// cast into a duck-typed trait object. The compatibility is checked at runtime. /// This is the only way to get an instance of ForeignClass.fntry_to_foreign<O>(&self) -> Result<Box<O>,ForeignClassError>whereO:ForeignClassObject + ?Sized,T:Inherits<O::Base> + Inherits<Object>,{let obj = self.clone().upcast();let foreign = Box::new(ForeignClass(obj));/// compatebility is currently checked inside this function but could be moved into a separate call.O::from_foreign(foreign)}}#[derive(Debug)]pubenumForeignClassError{MissingMethod(StringName,Vec<VariantType>,VariantType),}// ------------- trait and impls inside the consumer gdextension -------------/// user declared foreign interfacetraitITestScript{fnhealth(&mutself) -> u8;fnhit_enemy(&mutself,enemy:Gd<Node3D>);}/// proc-macro generates an implementation of the trait for ForeignClass by calling its methods via Object::classimplITestScriptforForeignClass{fnhealth(&mutself) -> u8{self.call(StringName::from("health"),&[]).to()}fnhit_enemy(&mutself,enemy:Gd<Node3D>){self.call(StringName::from("hit_enemy"),&[enemy.to_variant()]).to()}}/// implementation of the helper trait to cast a Box<ForeignClass> into the correct trait object.implForeignClassObjectfordynITestScript{typeBase = Node3D;fnfrom_foreign(fc:Box<ForeignClass>) -> Result<Box<Self>,ForeignClassError>{// validate health method exists and is correctif !fc.has_method("health".into(),&[],VariantType::Int){returnErr(ForeignClassError::MissingMethod("health".into(),vec![],VariantType::Int,));}// validate hit_enemy method exists and is correctif !fc.has_method("hit_enemy".into(),&[VariantType::Object],VariantType::Nil){returnErr(ForeignClassError::MissingMethod("hit_enemy".into(),vec![VariantType::Object],VariantType::Nil,));}// cast once everything has been verified.Ok(fc asBox<Self>)}}
Why not Gd<ITestScript>?
Making traits work with Gd<T> would be quite hard, if not impossible, I think.
Using a concrete type, on the other hand, should be doable as it would be very similar to the generated engine classes but calls Object::call for all methods instead (from what I have seen so far).
The struct would though have to be either generated for the defined trait or defined instead of the trait in combination with an impl that contains body-less method declarations. Both sounds more cumbersome to me than defining a trait, which feels much more native to rust.
The text was updated successfully, but these errors were encountered:
I have recently been toying around with some nicer way for interfacing with Godot classes / types that are defined outside of the engine or the rust gdextension, i.e. in GDScript C# or another extension.
Currently, we can use
Object::call(...)
but it can be quite error-prone as we have to repeat and duplicate the calls to the same functions across all call sites. It would be much nicer if we could define the expected interface once and then re-use everywhere without having to resort to string-based method names and remembering the right argument orders.Idea
My idea was to allow the user to define traits for the foreign interfaces they expect and then provide a proc-macro that generates a duck-type implementation against a
Gd<T>
.Why not
Gd<ITestScript>
?Making traits work with
Gd<T>
would be quite hard, if not impossible, I think.Using a concrete type, on the other hand, should be doable as it would be very similar to the generated engine classes but calls
Object::call
for all methods instead (from what I have seen so far).The struct would though have to be either generated for the defined trait or defined instead of the trait in combination with an
impl
that contains body-less method declarations. Both sounds more cumbersome to me than defining a trait, which feels much more native to rust.The text was updated successfully, but these errors were encountered: