Skip to content

Allow to extend given class by re-usable structs (class extensions) #1215

Open
@Yarwin

Description

@Yarwin

Long story short, currently if one has some fields/methods shared between multiple classes of very different bases they have two sane ways to deal with repeating code:

  1. Use custom Godot's resource (Godot's way)
  2. Create some kind of trait with blanket implementation combined with tool and little boilerplate
    example:
example of trait & blanket approach
pub trait WithAttributes {
    fn get_attribute(&self, property: &str) -> Option<Variant> {
        match property {
            "OrderAndForce" => Some(self.get_attributes().get(0)?.to_variant()),
            "LoyaltyAndSelfishness" => Some(self.get_attributes().get(1)?.to_variant()),
            "GreedAndKindness" => Some(self.get_attributes().get(2)?.to_variant()),
            "PietyAndSecular" => Some(self.get_attributes().get(3)?.to_variant()),
            _ => None,
        }
    }

    fn set_attribute(&mut self, property: &str, value: Variant) -> bool {
        let Ok(int) = value.try_to::<i32>() else {
            return false;
        };

        match property {
            "OrderAndForce" => {
                self.get_attributes_mut().set(0, int);
            }
            "LoyaltyAndSelfishness" => {
                self.get_attributes_mut().set(1, int);
            }
            "GreedAndKindness" => {
                self.get_attributes_mut().set(2, int);
            }
            "PietyAndSecular" => {
                self.get_attributes_mut().set(3, int);
            }
            _ => return false,
        };
        true
    }

    fn attributes(&self) -> impl Iterator<Item = Attribute> {
        self.get_attributes()
            .iter_shared()
            .enumerate()
            .map(|(idx, str)| Attribute {
                attribute_type: AttributeTag::from(idx + 1),
                strength: str,
            })
    }

    fn get_attributes_property_list() -> [PropertyInfo; AMOUNT_OF_ATTRIBUTES + 1] {
        [
            PropertyInfo::new_group("Attributes", ""),
            PropertyInfo::new_export::<i32>("OrderAndForce"),
            PropertyInfo::new_export::<i32>("LoyaltyAndSelfishness"),
            PropertyInfo::new_export::<i32>("GreedAndKindness"),
            PropertyInfo::new_export::<i32>("PietyAndSecular"),
        ]
    }

    fn get_attributes(&self) -> &Array<i32>;

    fn get_attributes_mut(&mut self) -> &mut Array<i32>;
}

// Example usage:

#[derive(GodotClass)]
#[class(init, tool, base = Resource)]
pub struct AddAttributeStrength {
    #[init(val = {
        let mut arr = Array::new();
        arr.resize(AMOUNT_OF_ATTRIBUTES, 0);
        arr
    })]
    #[var]
    attributes: Array<i32>,
    base: Base<Resource>,
}

impl WithAttributes for AddAttributeStrength {
    fn get_attributes(&self) -> &Array<i32> {
        &self.attributes
    }

    fn get_attributes_mut(&mut self) -> &mut Array<i32> {
        &mut self.attributes
    }
}

#[godot_api]
impl IResource for AddAttributeStrength {
    fn get_property(&self, property: StringName) -> Option<Variant> {
        self.get_attribute(property.to_string().as_str())
    }

    fn set_property(&mut self, property: StringName, value: Variant) -> bool {
        self.set_attribute(property.to_string().as_str(), value.clone())
    }

    fn get_property_list(&mut self) -> Vec<PropertyInfo> {
        Vec::from(Self::get_attributes_property_list())
    }
}

This proposal suggest adding the third way – Class Extensions. It would be derive macro which would allow users to use #[var], #[export] and traits on non-class structs which later can be appended as a field to some struct.

For example, following declaration:

#[derive(GodotClass)]
#[class(init, base=Node)]
struct Initializer {
    #[class_extension]
    smth: SomeExtension
}

#[derive(ClassExtension)]
pub struct SomeExtension {
    #[var]
    #[init( val = 4)]
    field: i32,
}

Would generate, more or less, following code:

wall of code
// In initializer registration:
<SomeExtension as ::godot::obj::cap::ImplementsClassExtensionGodotExports<Initializer>>::__register_exports();

impl WithClassExtension<SomeExtension> for Initializer
    where
        Self: GodotClass,
    {
        #[doc(hidden)]
        fn __get_extension(&self) -> &SomeExtension {&self.smth}
        #[doc(hidden)]
        fn __get_extension_mut(&mut self) -> &mut SomeExtension {&mut self.smth} 
    }


// More-or-less, it has some non-generic leftovers – should be enough to convey the idea.

impl<C> ::godot::obj::cap::ImplementsClassExtensionGodotExports<C>
for SomeExtension
where C: GodotClass + ::godot::obj::cap::WithClassExtension<SomeExtension>
{
    fn __register_exports() {
        use ::godot::builtin::{StringName, Variant};
        use ::godot::obj::GodotClass;
        use ::godot::register::private::method::ClassMethodInfo;
        use ::godot::sys;
        type CallParams = ();
        type CallRet = <i32 as ::godot::meta::GodotConvert>::Via;
        let method_name = StringName::from("get_field");
        unsafe extern "C" fn varcall_fn<C: ::godot::obj::cap::WithClassExtension<SomeExtension>>(
            _method_data: *mut std::ffi::c_void,
            instance_ptr: sys::GDExtensionClassInstancePtr,
            args_ptr: *const sys::GDExtensionConstVariantPtr,
            arg_count: sys::GDExtensionInt,
            ret: sys::GDExtensionVariantPtr,
            err: *mut sys::GDExtensionCallError,
        ) {
            let call_ctx = ::godot::meta::CallContext::func("Initializer", "get_field");
            ::godot::private::handle_varcall_panic(&call_ctx, &mut *err, || {
                ::godot::meta::Signature::<CallParams, CallRet>::in_varcall(
                    instance_ptr,
                    &call_ctx,
                    args_ptr,
                    arg_count,
                    ret,
                    err,
                    |instance_ptr, params| {
                        let () = params;
                        let storage = unsafe { ::godot::private::as_storage::<C>(instance_ptr) };
                        let instance = ::godot::private::Storage::get(storage);
                        instance.__get_extension().get_field()
                    },
                )
            });
        };
        unsafe extern "C" fn ptrcall_fn<C: ::godot::obj::cap::WithClassExtension<SomeExtension>>(
            _method_data: *mut std::ffi::c_void,
            instance_ptr: sys::GDExtensionClassInstancePtr,
            args_ptr: *const sys::GDExtensionConstTypePtr,
            ret: sys::GDExtensionTypePtr,
        ) {
            let class_name = C::class_name().to_string();
            let call_ctx = ::godot::meta::CallContext::func(&class_name, "get_field");
            let _success = ::godot::private::handle_panic(
                || format_args!("{0}", call_ctx).to_string(),
                || {
                    ::godot::meta::Signature::<CallParams, CallRet>::in_ptrcall(
                        instance_ptr,
                        &call_ctx,
                        args_ptr,
                        ret,
                        |instance_ptr, params| {
                            let () = params;
                            let storage =
                                unsafe { ::godot::private::as_storage::<C>(instance_ptr) };
                            let instance = ::godot::private::Storage::get(storage);
                            instance.__get_extension().get_field()
                        },
                        sys::PtrcallType::Standard,
                    )
                },
            );
        };
        let method_info = unsafe {
            ClassMethodInfo::from_signature::<C, CallParams, CallRet>(
                method_name,
                Some(varcall_fn::<C>),
                Some(ptrcall_fn::<C>),
                ::godot::global::MethodFlags::NORMAL | ::godot::global::MethodFlags::CONST,
                &[],
            )
        };
        {
            if false {
                format_args!("   Register fn:   {0}::{1}", "Initializer", "get_field");
            }
        };
        method_info.register_extension_class_method();
        {
            use ::godot::builtin::{StringName, Variant};
            use ::godot::obj::GodotClass;
            use ::godot::register::private::method::ClassMethodInfo;
            use ::godot::sys;
            type CallParams = (<i32 as ::godot::meta::GodotConvert>::Via,);
            type CallRet = ();
            let method_name = StringName::from("set_field");
            unsafe extern "C" fn varcall_fn<C: ::godot::obj::cap::WithClassExtension<SomeExtension>>(
                _method_data: *mut std::ffi::c_void,
                instance_ptr: sys::GDExtensionClassInstancePtr,
                args_ptr: *const sys::GDExtensionConstVariantPtr,
                arg_count: sys::GDExtensionInt,
                ret: sys::GDExtensionVariantPtr,
                err: *mut sys::GDExtensionCallError,
            ) {
                let call_ctx = ::godot::meta::CallContext::func("Initializer", "set_field");
                ::godot::private::handle_varcall_panic(&call_ctx, &mut *err, || {
                    ::godot::meta::Signature::<CallParams, CallRet>::in_varcall(
                        instance_ptr,
                        &call_ctx,
                        args_ptr,
                        arg_count,
                        ret,
                        err,
                        |instance_ptr, params| {
                            let (field,) = params;
                            let storage =
                                unsafe { ::godot::private::as_storage::<C>(instance_ptr) };
                            let mut instance = ::godot::private::Storage::get_mut(storage);
                            instance.__get_extension_mut().set_field(field)
                        },
                    )
                });
            };
            unsafe extern "C" fn ptrcall_fn<C: ::godot::obj::cap::WithClassExtension<SomeExtension>>(
                _method_data: *mut std::ffi::c_void,
                instance_ptr: sys::GDExtensionClassInstancePtr,
                args_ptr: *const sys::GDExtensionConstTypePtr,
                ret: sys::GDExtensionTypePtr,
            ) {
                let call_ctx = ::godot::meta::CallContext::func("Initializer", "set_field");
                let _success = ::godot::private::handle_panic(
                    || format_args!("{0}", call_ctx).to_string(),
                    || {
                        ::godot::meta::Signature::<CallParams, CallRet>::in_ptrcall(
                            instance_ptr,
                            &call_ctx,
                            args_ptr,
                            ret,
                            |instance_ptr, params| {
                                let (field,) = params;
                                let storage =
                                    unsafe { ::godot::private::as_storage::<C>(instance_ptr) };
                                let mut instance = ::godot::private::Storage::get_mut(storage);
                                instance.__get_extension_mut().set_field(field)
                            },
                            sys::PtrcallType::Standard,
                        )
                    },
                );
            };
            let method_info = unsafe {
                ClassMethodInfo::from_signature::<C, CallParams, CallRet>(
                    method_name,
                    Some(varcall_fn::<C>),
                    Some(ptrcall_fn::<C>),
                    ::godot::global::MethodFlags::NORMAL,
                    &["field"],
                )
            };
            {
                if false {
                    format_args!("   Register fn:   {0}::{1}", "Initializer", "set_field");
                }
            };
            method_info.register_extension_class_method();
        }
        type FieldType = i32;
        ::godot::register::private::register_export::<C, FieldType>(
            "field",
            __godot_SomeExtension_Funcs::get_field,
            __godot_SomeExtension_Funcs::set_field,
            <i32 as ::godot::register::property::Export>::export_hint(),
            ::godot::global::PropertyUsageFlags::DEFAULT,
        );
    }
}

example in editor:

Image

For what purpose?

It would help with repeating code and would also allow to fracture class into smaller pieces – for example it would make possible to categorize struct by "thread safe" parts and "single threaded Gd part". It would also make working with views and whatnot easier.

It could also be used as some kind of inheritance-by-composition mechanism, albeit I'm not fully sold on that.

Constraints, limits etc.

  • Should class extensions be repeatable, or should we allow only one-class-extension-of-given-type-per-class? The latter one is much easier to handle and less janky.
  • IMO OnReady<T> should not be supported for class extensions. It is way too messy and makes it WAY too easy to miss something (in other words – it generates much more problems than solves). Not to say that OnReady works only for Nodes (not a huge issue, but another thing to remember about)
  • Should we support #[godot_api] for class extensions? IMO it is not necessary with #[godot_api(secondary)] and macros 🤔… but it also wouldn't be too hard?
  • Init should be supported - both for class extension itself (#[class_extension]#[init( val = T::VARIANTA)] field: T) and fields on it.
  • Docs should be supported.
  • Class extension fields should be able to declare groups & subgroups; one should be allowed to declare groups&subgroups as #[class_extension(group = ..., subgroup = ...)].
  • Appending prefixes with proper key&value on attribute macro should be supported. Runtime warning should be emitted if class & class extension fields overlap.

Metadata

Metadata

Assignees

No one assigned

    Labels

    c: registerRegister classes, functions and other symbols to GDScriptfeatureAdds functionality to the libraryhardOpposite of "good first issue": needs deeper know-how and significant design work.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions