Description
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:
- Use custom Godot's resource (Godot's way)
- 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:
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.