Skip to content

Re-organize type-level attributes in NativeClass derive macro #848

Open
@chitoyuu

Description

@chitoyuu

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:

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 the owner 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 the owner 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    breaking-changeIssues and PRs that are breaking to fix/merge.c: exportComponent: export (mod export, derive)quality-of-lifeNo new functionality, but improves ergonomics/internals

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions