Re-organize type-level attributes in NativeClass
derive macro · Issue #848 · godot-rust/gdnative (original) (raw)
The NativeClass
macro currently utilizes a large number of type-level attributes, making it tricky to add further expansions to its functionality. The current attributes are:
inherit
user_data
register_with
no_constructor
... while in the future, we might also like to add:
- Custom type parameter bounds for generics types (Derive NativeClass for known monomorphizations of generic types #601)
- Custom static type name (Rename #[derive(NativeClass)] types #603)
Proposal
My proposal is as follows:
inherit
is to be left as a top-level attribute, because of its significance.- All other attributes are to be bundled into a single
#[nativescript]
, as key-value pairs when applicable:#[nativescript(user_data = "path::to::Type<Self>", register_with = "path::to::function", no_constructor)]
Under this new arrangement, generic bounds and static renames can easily be introduced as keys under the #[nativescript]
attribute: #[nativescript(bound = "T: SomeBound", rename = "SomeNewName")]
. Prior art for this could be found in the ecosystem, for example, in Serde.
Unresolved questions
It's a common complaint that the signature of the mandatory new
is too verbose, the owner
argument being seldom used. However, it isn't obvious how this is best dealt with. Our problem here is the need to distinguish three different semantics at the syntax level:
- Calling a function, possibly a custom one, with no arguments.
- Calling a function, possibly a custom one, with a single
owner
argument. - Having no default constructor.
I can see two alternatives to the current situation:
Changing the default constructor to the nullary Default::default
, requiring a constructor
key for unary constructors
Under this arrangement, the possible annotations are:
- Nothing.
Default::default
is called to produce a new instance by default. #[nativescript(constructor = "Self::new")]
.Self::new
is called with theowner
argument to produce a new instance. This is consistent with the current behavior.#[nativescript(no_constructor)]
. The NativeScript type would have no default constructor.- All other cases would become compile errors.
This makes it much less verbose to declare types with constructors that do not use the owner argument, and frees up the new
identifier unless requested specifically by the user. The downside is that extra annotation is required to reproduce the current default.
Replacing no_constructor
with a nested key-value pair under the constructor
meta
Under this arrangement, the possible annotations are:
#[nativescript(constructor(nullary = "Self::default"))]
.Self::default
is called with no arguments to produce a new instance.#[nativescript(constructor(unary = "Self::new"))]
.Self::new
is called with theowner
argument to produce a new instance.#[nativescript(constructor(none))]
. The NativeScript type would have no default constructor.- All other cases would become compile errors.
This has the benefit of allowing custom functions for both the nullary case and the unary case, but is very verbose. It should be noted that Default
can always be implemented for types with nullary constructors, even though it could be undesirable if the constructor in question is expensive, or if there are multiple such functions.