Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow custom GDScript annotations which can be read at runtime #1316

Open
Wavesonics opened this issue Aug 5, 2020 · 33 comments
Open

Allow custom GDScript annotations which can be read at runtime #1316

Wavesonics opened this issue Aug 5, 2020 · 33 comments

Comments

@Wavesonics
Copy link

Describe the project you are working on:
Multiplayer game

Describe the problem or limitation you are having in your project:
Automaticly serializing custom data classes

Describe the feature / enhancement and how it helps to overcome the problem or limitation:
In 4.0 GDScript now has annotations. What I propose is two fold:

  1. User's can declare their own custom annotations much like how Java allows you to (override a built in Annotation class)
  2. User's can query a classes properties and methods for their associated annotations at runtime

Describe how your proposal will work, with code, pseudocode, mockups, and/or diagrams:

extends Reference
class_name MyDataClass

@Serialize(name="json_foo")
var foo: int

@Serialize
var bar: String

var x: Boolean

Then I can wrote some code to introspect MyDataClass and find it's properties that it wants serialized, and produce something like:

{
    "json_foo": 0,
    "bar": "test"
}

If this enhancement will not be used often, can it be worked around with a few lines of script?:
It can be worked around, it will just be less flexible and elegant.

Is there a reason why this should be core and not an add-on in the asset library?:
At least this particular approach would not be possible without support from GDScript it's self. But other approaches are possible, such as just listing all of the properties using the current inspection methods, and serializing all of them.

@KoBeWi
Copy link
Member

KoBeWi commented Aug 6, 2020

You could also make your variables exported and save the data as .tres. It's a text format too.

@jonbonazza
Copy link

jonbonazza commented Aug 6, 2020

You could also make your variables exported and save the data as .tres. It's a text format too.

AFAIK, this doesn't work for embedded classes.

@jordo
Copy link

jordo commented Oct 30, 2020

+1 for this. So I'm basically working on something similar. exposing an annotation property would be nice. My work around is to use some naming prefix convention and yes iterate the object properties. Example:

@export net_serialized_counter : int   // gets serialized and synced
@export net_serialized_state : int  // gets serialized and synced
@export particles : Node // not serialized or synced

Then a net synchronizer just iterates the properties and checks for that prefix. But it's not ideal. I'd prefer not to rely on a naming convention here, as a custom annotation would be much less hacky and easier to use.

@vnen
Copy link
Member

vnen commented Dec 7, 2020

My main question about this is how do you expect to gather this information? Can you mock up a sample code with the functions you would call to retrieve the annotations?

I also want to make annotations explicitly registered, so a typo won't break the functionality without the user noticing.

@AndreaCatania
Copy link

AndreaCatania commented Dec 7, 2020

Define

To register it would be nice an API like this:

@tool

func _ready():
    Engine.add_annotation("custom_annotation")

C++ version:

void register_custom_module() {
    Engine::get_singleton()->add_annotation("custom_annotation")
}

Fetch

I think that a good way to retrieve the annotations would the following:

GDscript version:

# Returns the scripts path 
var annotated_scripts = Engine.get_annotated_scripts("custom_annotation")

for script in annotated_scripts:
    print("Script: ", script)

    var annotated_functions = Engine.get_annotated_functions("custom_annotation", script)
    var annotated_variables = Engine.get_annotated_variables("custom_annotation", script)

    func f in annotated_functions:
        print("-- Function: ", f)
        var arguments = Engine.get_annotated_functions_arguments("custom_annotation", script, f)
        for key in arguments.key():
                print("---- ", key, " = ", arguments[key])

    func v in annotated_variables:
        print("-- Variable: ", v)
        var arguments = Engine.get_annotated_variables_arguments("custom_annotation", script, v)
        for key in arguments.key():
                print("---- Annotation: ", key, " = ", arguments[key])

C++ version:

struct ScriptAnnotationData {
    OHashMap<StringName, Dictionary> function_data;
    OHashMap<StringName, Dictionary> variable_data;
};

const OAHashMap<String, ScriptAnnotationData> &annotated_scripts = Engine::get_singleton()->get_annotated_scripts("custom_annotation")
// Returns the stored annotations for each script. The key is the script path.
[.....]

Usage

This kind of syntax allows the annotation to contains any kind of data. So that it's possible to define dynamic annotations; allowing:

@Serialize(name="json_foo")
var foo: int

@Serialize
var bar: String

@Serialize(max_length=20)
var long_string: String

@Serialize(networked=true)
var long_string_2: String

@Serialize(networked=false, max_length=20, name="json_foo")
var long_string_3: String

@AndreaCatania
Copy link

We also need a way to take the script level annotations like @tool. This function var annotated_scripts = Engine.get_annotated_scripts("tool_like") still seems valid. Though, we need a way to take the arguments.

@jordo
Copy link

jordo commented Dec 7, 2020

+1 for this. Like @AndreaCatania we have a client-predicted, server-auth, (rollback re-sim) network module.

Right now our net 'state' is explicity defined under list of exported properties of our "NetworkGameObject"s. GameData, Player1, and Player2 in below screenshot:

Screen Shot 2020-12-07 at 11 19 09 AM

We inspect this list at runtime to serialize the game's state, compare, and do any rollback and resims.. The downside to using just this list we can't add any other properties to these objects without them getting serialized along with whats currently exported.

Likewise some properties we will want to predict client-side, and some properties we will want to render lag and show/draw in different timestream. And even like the above comment by @AndreaCatania perhaps serialize in different ways, etc.

From the c++/native side, it would be great to access any custom annotations at runtime say in here somewhere:
Screen Shot 2020-12-07 at 11 24 20 AM

Right now we rely and default storage attribute flags for processing, but access to custom annotations here in the object runtime would be quite powerful and flexible.

@AndreaCatania
Copy link

AndreaCatania commented Dec 7, 2020

OFFTOPIC: @jordo this may interest you: godotengine/godot#37200

@jordo
Copy link

jordo commented Dec 7, 2020

But ya, for sure +1 for what @AndreaCatania proposing below, this is great:

@Serialize(networked=false, max_length=20, name="json_foo")

OFFTOPIC: @AndreaCatania yes I've followed what you've been doing here, godotengine/godot#37200 It's quite an effort! But we needed more explicit control and a more data-orientated approached with our module. Simpler and not tied into as many different godot systems.

@Killfrra
Copy link

Killfrra commented Jun 6, 2021

+1 for this proposal. Because that's how I solve the simular problem now. Perhaps this suggestion will allow me to get rid of the initialization of var_attrs, and in combination with the #2837, and the explicit call to sync_set and setters with emit_stat.
P.S. I'm sorry for the large amount of code. It's here to demonstrate what we can get rid of

# CHILD CLASS ON SERVER

var mana
var mana_max: float = 418.0
var mana_regen: float = 8.0
var ability_power := 9.0
var ability_haste := 0.0
var level_int := 1
var level_frac
var exp_to_next_level := 280
var gold := 500.0
var creep_score := 0
var kills := 0
var deaths := 0
var assists := 0

func generate_variable_attributes():
    .generate_variable_attributes()
    var_attrs["mana"] = Attrs.UNRELIABLE | Attrs.VISIBLE_TO_EVERYONE
    var_attrs["mana_max"] = Attrs.VISIBLE_TO_EVERYONE
    var_attrs["mana_regen"] = Attrs.VISIBLE_TO_OWNER_AND_SPEC
    var_attrs["ability_power"] = Attrs.VISIBLE_TO_EVERYONE
    var_attrs["ability_haste"] = Attrs.VISIBLE_TO_EVERYONE
    var_attrs["strike_chance"] = Attrs.VISIBLE_TO_EVERYONE
    var_attrs["level_int"] = Attrs.VISIBLE_TO_EVERYONE
    var_attrs["level_frac"] = Attrs.VISIBLE_TO_OWNER_AND_SPEC
    var_attrs["gold"] = Attrs.VISIBLE_TO_OWNER_AND_SPEC
    var_attrs["kills"] = Attrs.VISIBLE_TO_EVERYONE
    var_attrs["deaths"] = Attrs.VISIBLE_TO_EVERYONE
    var_attrs["assists"] = Attrs.VISIBLE_TO_EVERYONE
    var_attrs["creep_score"] = Attrs.VISIBLE_TO_EVERYONE

func level_up_stats():
    # damagable
    var health_change = 92 * (0.65 + 0.035 * level_int)
    sync_set("health_max", health_max + health_change)
    sync_set("health_regen", health_regen + 0.06 * (0.65 + 0.035 * level_int))
    sync_set("health", min(health + health_change, health_max))
    
    sync_set("armor", armor + 3.5 * (0.65 + 0.035 * level_int))
    sync_set("mr", mr + 0.5 * (0.65 + 0.035 * level_int))
    # autoattacking
    sync_set("attack_damage", attack_damage + 3 * (0.65 + 0.035 * level_int))
    # champion
    var mana_change = 92 * (0.65 + 0.035 * level_int)
    sync_set("mana_max", mana_max + 25 * (0.65 + 0.035 * level_int))
    sync_set("mana_regen", mana_regen + 0.08 * (0.65 + 0.035 * level_int))
    sync_set("mana", min(mana + mana_change, mana_max))

# BASE CLASS ON SERVER

enum Attrs {
    RELIABLE = 0, # default
    UNRELIABLE = 1,
    CONST = 2,
    VISIBLE_TO_EVERYONE = 4,
    VISIBLE_TO_TEAM_AND_SPEC = 8,
    VISIBLE_TO_OWNER_AND_SPEC = 16,
}

var var_attrs := {}
func generate_variable_attributes():
    pass

func _init():
    generate_variable_attributes()

func sync_set(key, value = null):
    if value == null:
        value = get(key)
    else:
        set(key, value)

    var attrs = var_attrs.get(key) if var_attrs else null
    if attrs:
        
        var func_name = "rpc_unreliable_id" if attrs | Attrs.UNRELIABLE else "rpc_id"
        
        if attrs & Attrs.VISIBLE_TO_EVERYONE:
            for client in Game.clients.values():
                if should_sync_to_client(client):
                    avatar.call(func_name, client.id, "set_remote", key, value)
        
        else:
            
            if attrs & Attrs.VISIBLE_TO_TEAM_AND_SPEC:
                for player in Game.lists[team].values():
                    if should_sync_to_client(player):
                        avatar.call(func_name, player.id, "set_remote", key, value)
            elif attrs & Attrs.VISIBLE_TO_OWNER_AND_SPEC:
                avatar.call(func_name, id, "set_remote", key, value)
            
            for spectator in Game.spectators.values():
                if should_sync_to_client(spectator):
                    avatar.call(func_name, spectator.id, "set_remote", key, value)

        return true
    return false

func should_sync_to_client(client):
    return client.team == team || client.team == Types.Team.Spectators || seen_by_teams[client.team]

# BASE CLASS ON CLIENT (avatar)

puppetsync func set_remote(key, value):
    self[key] = value
    
# CHILD CLASS ON CLIENT

var health := 0.0 setget set_health
var health_max := 0.0 setget set_health_max
var health_regen := 0.0 setget set_health_regen
var armor := 0.0 setget set_armor
var mr := 0.0 setget set_mr

func set_health(to):
    emit_stat("health", to)
    health = to

func set_health_max(to):
    emit_stat("health_max", to)
    health_max = to

func set_health_regen(to):
    emit_stat("health_regen", to)
    health_regen = to
    
func set_armor(to):
    emit_stat("armor", to)
    armor = to

func set_mr(to):
    emit_stat("mr", to)
    mr = to

@sairam4123
Copy link

sairam4123 commented Jun 18, 2021

Define

To register it would be nice an API like this:

@tool

func _ready():
    Engine.add_annotation("custom_annotation")

C++ version:

void register_custom_module() {
    Engine::get_singleton()->add_annotation("custom_annotation")
}

Define

We could use annotations for defining annotations:

# Console.gd
var commands = {}


@annotation
func register_command(function: Callable):
    commands[function.name] = function

This is similar to decorators in Python, a annotation could possibly call back the function it got annotated with. The function will have one argument, function, extra arguments could be added, extra arguments must be passed using the decorator itself.
Then we can split up annotations to functions and variables, because who wants function annotations to be used in variables or vice versa.

So the above code could be written this way:

var commands = {}


@function_annotation
func register_command(function: Callable):
    commands[function.name] = function

I have a mockup for variables too.

# Settings.gd
var settings = {}

@variable_annotation
func setting(name, type, value):
    settings[name] = {name, type, value}

As we don't have a class for variables we'd be passing name, type and the value, if type is not defined, we'd pass inferred type.
And I think, instead of having to pass 3 arguments, we could compile them into one class named Property, this could be discussed further in a separate proposal.

Fetch

As for fetching functions, we can possibly callback the function that has been annotated as annotation, with the function or variable that got annotated in the first argument and the arguments that got passed with it.
We can also use Engine.get_annotated_scripts to get annotated script as mentioned in #1316 (comment)

@function_annotation and @variable_annotation

As said previously, this comment introduces two new annotations, function and variable annotations to declare custom annotations.

These are built in annotations and will work like other built-in annotation.

Usage

Variable/Property Annotation

@Settings.setting()
var speed: int = 50

@Settings.setting()
var start_money := 10.0

@Settings.setting() # I'm yet to decide how we should override variable names that get passed.
var this_name_will_be_overriden := "Hi!"

@Settings.setting("range", 10, 11, 1.0) # Extra arguments
var extra_args = 10 # will be inferred at runtime

Function Annotation

@Console.command()
func this_is_a_console_command(arg: int, arg_2: int) -> bool:
    return arg > arg_2

@Console.command("test")
func set_start_money(money: int) -> bool:
    start_money = money
    return true

This comment is kinda of a new proposal but still I'm commenting it here.

@JuerGenie

This comment has been minimized.

@Calinou
Copy link
Member

Calinou commented Jan 2, 2022

@JuerGenie Please don't bump issues without contributing significant new information. Use the 👍 reaction button on the first post instead.

@Calinou Calinou changed the title GDScript 4.0: Allow custom annotations which can be read at runtime Allow custom GDScript annotations which can be read at runtime Jun 2, 2022
@Frontrider
Copy link

I'll probably go "out of bounds" a bit, but imo most annotation requests we see here are either these, implementation wise:

  • "decorator", run a function on a thing (or as a part of a thing), like use the existing signal api to connect things
  • "type information", add more data to the things we wrote.

"Type information" heavily overlaps with the current meta data field imo, and in the userspace could be implemented as a decorator that is a function writing metadata. This would also give room for other languages to interact with this information through that interface.
Not a complete overlap, as this information might still belong to the type and is redundant on instances, but could still be a reasonable implementation that may not require a new language version, as it probably does not need to change current underlying behavior.

I also do not fully understand how decorators would work in Godot.

@awardell
Copy link

"Type information" heavily overlaps with the current meta data field imo, and in the userspace could be implemented as a decorator that is a function writing metadata. This would also give room for other languages to interact with this information through that interface.

Something like @meta(data:{String:Variant}, ...} vararg to annotate with? (Forgive me if I've butchered that syntax)

@Frontrider
Copy link

Frontrider commented Oct 26, 2022

"Type information" heavily overlaps with the current meta data field imo, and in the userspace could be implemented as a decorator that is a function writing metadata. This would also give room for other languages to interact with this information through that interface.

Something like @meta(data:{String:Variant}, ...} vararg to annotate with? (Forgive me if I've butchered that syntax)

https://github.com/tc39/proposal-decorator-metadata
No, the way javascript decorators are set up, is that you can wrap the function call in a method/call a method on that thing you marked.

So, the "connect signal to" decorator is implementable in gdscript, by calling the connect method inside the decorator. Same as metadata can be set by calling the set_meta method, the same apis remain, we just hide most of that complexity.

If there is a setter for something, then decorators can and will be able to interact with it. I think this could be a quite elegant solution for gdscript, but again, I'm not quite certain exactly how it would interact with the rest of the system as in the current version it can not use @ as a marker.

IF there was a GDScript api to set export parameters one by one then all problems are solved with it imo.

@me2beats
Copy link

Is this like Python decorators or something different?

@dalexeev
Copy link
Member

Is this like Python decorators or something different?

See #6750 (comment) and #6750 (comment).

@ibe-denaux
Copy link

This feature would be greatly appreciated. I'm not aware of any annotations for methods, but having custom ones for both methods and properties would be very useful. Just being able to filter results using annotations with the object's class get_property_list and get_method_list would make things like serializing data or exposing certain properties/methods to designer tools a lot easier.

Functionally, just them being tags for other modules to process would be enough imo.

@mstarongithub
Copy link

mstarongithub commented Nov 28, 2023

An imo decent approach for implementing custom annotations could be copied from how Rust handles macros (see (https://doc.rust-lang.org/book/ch19-06-macros.html)).

In pseudo-code this could look something like the following:

var token := read_new_token()
if token is custom_annotation:
    annotation := token.to_annotation
    annotated_tokens := read_next_segment()
    ast.append(annotation.execute(annotated_tokens)

Where read_next_segment is potentially recursive (due to stacked annotations) and reads in:

  • a full variable definition
    • Including Getter and Setter (should this only be the function reference or include the full function)
  • a function definition
  • a class definition
  • an enum
  • another annotation
    • Would start the whole procedure on that annotation first and then operate on the tokens that annotation generated

@nlupugla
Copy link

nlupugla commented Jan 2, 2024

Fully custom annotations could potentially have a huge scope, so I think it's worth considering whether there is some feature with smaller scope that could cover a large percentage of the use cases for custom annotations. Godot's development mantra is simple problem -> simple solution.

For example, I've been thinking about an @tag annotation that would allow you to tag properties with metadata. The OP's @serialize annotation could just as easily be done with @tag("serializable").

#8752

@Morgul
Copy link

Morgul commented Jan 9, 2024

Fully custom annotations could potentially have a huge scope, so I think it's worth considering whether there is some feature with smaller scope that could cover a large percentage of the use cases for custom annotations. Godot's development mantra is simple problem -> simple solution.

For example, I've been thinking about an @tag annotation that would allow you to tag properties with metadata. The OP's @serialize annotation could just as easily be done with @tag("serializable").

#8752

Tags solve one aspect, certainly. But I'd also like to use annotations to remove boilerplate, ex: a @player_command annotation for functions that under the hood call a register_command function on the class. This way I can have a base class that knows how to handle player commands, and everything that can handle commands can just inherit from it, and just annotate their functions with @player_command.

To me, it's a lot like the justification for the @onready annotation; sure, we could overload the _ready function and fill it with variable initialization... but it's much cleaner to use the annotation. Same for doing any sort of custom logic registration of handler functions or behavior building.

Personally, I like the elegance of python decorators, and think they would have good user ergonomics for gdscript. They're just a function, and they take what they decorate, do some work on it, and return a replacement for it (often just the original item). If a decorator takes arguments, it's called as a function with just those two arguments, and then expected to return a standard decorator function that just takes what to decorate. (i.e. if a normal decorator is the equivalent of dec(thing_to_decorate), then one with arguments would be dec(arg1, arg2)(thing_to_decorate).) (A good overview of them can be found here.)

Check out the Python decorator proposal, they have some good rationale for why they made the decisions they did, the problems they ran into, and the community reception since. It's useful to see other people thinking through the same problem, IMHO.

@daihaminkey
Copy link

Python approach may be too complicated and beginner unfriendly. When you put more than one decorator over a function, nested execution becomes harder to read and predict.

Since C# is a first-party language now, I would suggest attribute approach.

User defines custom annotation as a data class, that extends special Annotation class.
Custom annotation class can have properties, that populated by constructor (and the @Serialize(name="json_foo") part is a call of that constructor).

Constructor executed once per build and can’t have side effects, so it’s a class metadata, not instance one.

In the runtime it should be possible to access this metadata, by running a function, that retrieves all annotations of entity. In case of properties, I suggest, that it should return an array of annotations for a given property name. Same for annotation funcs and classes.

Like get_property_list(), but get_property_annotations("my_property").

This approach does not have a big scope.
It can be done without implementing complex reflection system: to populate a dictionary with data for every class should be enough.
It can be also achieved by populating data at runtime, with lazy initialization triggered by first run of get_property_annotations function of a class.

Other big advantage is similarity with C# approach. It’s not only ensures feature parity, but also makes possible to retrieve annotated metadata of GDScropt class from C# code.

Than it can be handled the same as C# attribute, making possible creation C# plugins, that work with both languages metadata. Example here is potential plugin, that adds button to the inspector, based on [Button] attribute and @Button annotation simultaneously.

@mikerp99
Copy link

I was hoping to be able to do some dependency injection by defining custom annotations. This feature would be great!

@MarouaneMebarki

This comment was marked as off-topic.

@Calinou
Copy link
Member

Calinou commented Apr 1, 2024

@MarouaneMebarki Please don't bump issues without contributing significant new information. Use the 👍 reaction button on the first post instead.

@nlupugla
Copy link

nlupugla commented Apr 4, 2024

@daihaminkey I like your arguments in favor of the attribute approach. However, I'm not familiar with it first-hand. Could you give a rough idea of what it would look like in GDScript? For example, how would I create an @serialize annotation using the attribute approach?

@Mitten-O
Copy link

@daihaminkey I like your arguments in favor of the attribute approach. However, I'm not familiar with it first-hand. Could you give a rough idea of what it would look like in GDScript? For example, how would I create an @serialize annotation using the attribute approach?

@nlupugla I'm not @daihaminkey, but I have experience with C# attributes and want them in Godot, so I'll give a shot at showing what that could look like.

# File: Serialize.gds
extends Annotation # All annotations must extend this.
class_name Serialize

# Defines which elements the annotation is valid on.
@annotation_target AnnotationTarget.Property

# The name the field should have in the serialized format.
@export var name: String

# If set, truncate the length of the string or array to this length.
@export var max_length: int
# File: MyDataClass.gds
extends Reference
class_name MyDataClass

@Serialize(name="json_foo")
var foo: int

@Serialize
var bar: String

var x: Boolean
# File: Serializer.gds

func convert_to_json( obj: Object ) -> String:
  # Here I'm imagining the annotations added as another field in the existing property metadata dictionary.
  # Seems natural to me, but I don't have expertise here.
  var properties: Array[Dictionary] = obj._get_property_list()
  for property in properties:
     var annotations: Annotations = property["annotations"]
     var serialize: Serialize = annotations.get(Serialize)
     # Proceed to serialize according to the settings

PS: For me the main use case for annotations is a custom inspector plugin. Annotations would be the ideal way to enable one for a field and configure it. That's what most of the built-in annotations are doing for the built-in editors.

@nlupugla
Copy link

Ah, I see, thanks for the demonstration! This seems like a pretty well-fleshed out approach. Perhaps someone like @dalexeev or @vnen with a better understanding of the language internals can comment on how feasible this would be.

My only issue that I can think of at the moment is that GDScript, being a relatively to-the-point, beginner-friendly scripting language, currently doesn't use any patterns like this (that I can think of) where the user is required to extend a particular class in order to get certain language functionality. While you are meant to extend nodes to benefit from certain functionality, I feel that is more of an engine feature than a language feature. Maybe the distinction isn't all that important, but I can't help but feel the approach might feel overly complicated for newcomers. Then again, implementing your own annotations isn't exactly "Hello World", so maybe my concern newcomer friendliness isn't super relevant here.

Thinking about this some more, I wonder if instead of

extends Annotation
class_name Serialize

we could just do

annotation Serialize

@Morgul
Copy link

Morgul commented Apr 23, 2024

@nlupugla I'm not @daihaminkey, but I have experience with C# attributes and want them in Godot, so I'll give a shot at showing what that could look like.

I could get behind this approach. It gives me what I'd be looking for out of my use cases, and it "feels" like it fits with the rest of GDScript, for the most part.

My only issue that I can think of at the moment is that GDScript, being a relatively to-the-point, beginner-friendly scripting language, currently doesn't use any patterns like this (that I can think of) where the user is required to extend a particular class in order to get certain language functionality. While you are meant to extend nodes to benefit from certain functionality, I feel that is more of an engine feature than a language feature. Maybe the distinction isn't all that important, but I can't help but feel the approach might feel overly complicated for newcomers. Then again, implementing your own annotations isn't exactly "Hello World", so maybe my concern newcomer friendliness isn't super relevant here.

I think custom Annotations are an intermediate to advanced use case. I'm imagining some libraries providing common ones, and that being most beginner's interactions with them. I would focus on users with experience with meta programming in other languages and a moderate level of Godot experience as the target audience. imho.

we could just do

annotation Serialize

I really like annotation Serialize as syntactic sugar for extends Annotation class_name Serialize. I would think we support both; the annotation Foo syntax is just a short-hand for the class extension.

@dalexeev
Copy link
Member

1. What capabilities should custom annotations have?

Or "What are custom annotations?" Standard annotations are more than just metadata. For example, @onready changes when the variable initializer is evaluated. This is a language feature that cannot be implemented in GDScript itself. I believe that we are not talking about something like that. User annotations are only expected to be able to associate some metadata with class members (variables, functions, constants, etc.)1. In some ways, this is already possible:

const _METADATA = {
    foo = {name = "json_foo"},
    bar = {},
}

var foo: int
var bar: String

But this is non-standardized and less convenient since the metadata is separated from the members. There is also no static analysis, which is potentially possible for custom annotations.

Also, custom annotations as metadata are not relevant to Python decorators. This is a separate language feature.

2. How to declare custom annotations and avoid conflicts with standard ones?

One of the reasons we added annotations to the language is to reduce the number of keywords and potential conflicts with user identifiers. Now we can add any annotation and not worry about conflicts, unlike keywords. If we want to add support for custom annotations, then we must make sure that they do not conflict with the standard ones.

For example, we can require that all custom annotations begin with @data_23. Or we could not allow custom annotations, but instead introduce one standard one that allows you to associate any metadata with any member. For example @data(key: String, value: Variant) or @data(params: Variant, ...)4. I think you will agree that there is not much difference between @serialize("my_name") and @data("serialize", "my_name"). It seems to me that this makes sense, since annotations are already quite hard-coded. If we want to add custom annotations for specific purposes, then it makes sense to use specific annotations. For example, @data for metadata, @decorator for Python-like decorators, etc.

The option above does not require any action to declare annotations. Any annotation with a valid name and arbitrary arguments are allowed. GDScript doesn't do any validation, but you can implement this yourself (only in runtime, without static checks).

Another option is the ability to explicitly declare a custom annotation: specify valid arguments and targets (what class member types the annotation can be applied to). Something like:

annotation @data_serialize(name: String) {targets=var,func}

In this case, GDScript will check that the passed arguments match to the declared signature and that the target is correct at compile time. However, once there is a declaration, the question of the scope of the declaration arises. Is the declared annotation available globally in the entire project? Or does it only affect the file? Or does it operate within the class and its descendants?

3. How to use custom annotations?

Depending on the previous point. An important limitation of the current implementation: annotation arguments must be constant expressions. Object5, Callable, and some other types are not constant expressions. Annotation arguments are evaluated once when the script is compiled and are the same for all instances of the class.

4. How to get information from user annotations at runtime?

It is suggested above to use get_property_list(), however this is a core method and is not directly related to GDScript. Instead, we could introduce a different way of reflection. For example, add new methods to the GDScript or Script class.

@data("serialize", {name = "json_foo"})
var foo: int

@data("serialize")
var bar: String

func convert_to_json(obj: Object) -> String:
    var script = obj.get_script()
    if script is not GDScript:
        return

    var properties: Array[Dictionary] = obj.get_property_list()
    var annotations: Dictionary = script.get_annotations()
    for property in properties:
        if annotations.has(property.name):
            var property_annotations: Array = annotations[property.name]
            # ...

In theory, we could add a @meta annotation that would add the same object metadata to each instance. It would be more convenient to use, but I think duplicating data is bad.

Footnotes

  1. Similar to attributes in PHP.

  2. Similar to data- attributes in HTML.

  3. Or @my_, @custom_, @user_, etc.

  4. Or @meta, @tag, etc.

  5. Except for preload() and a few other special cases.

@Mitten-O
Copy link

Thoughts on what is sufficient

It is suggested above to use get_property_list(), however this is a core method and is not directly related to GDScript.

Yes, it may be infeasible to add these to the PropertyInfo underlying get_property_list. Adding annotations there would fatten the structure and it is used in so many places that the cost to people not using the feature may be unjustifiable. But I would argue that we specifically want the annotations to exist on the core level, so that other languages can expose them. Consider the use case of annotations as configuration for custom editors. It would be intolerable for C# Script's fields to not be configurable to use the custom editor.

Or we could not allow custom annotations, but instead introduce one standard one that allows you to associate any metadata with any member. @data(key: String, value: Variant)

This would be sufficient for the custom editor use case, so I'd be happy with it. 👍

Thoughts on what is elegant

But I'll be ambition's advocate: What about reimplementing the currently hard coded annotations on top of the generic annotation system? It would decrease the coupling between gdscript and the editor. It's a bit silly for something like @export_flags_2d_navigation to be hard coded in the language; it would be far more flexible for the editor subsystem to register such things dynamically. There is more language complexity in having a long list of hard coded ad-hoc annotations, than in simply having a generic annotation system.

Also, even though the initial sensible implementation should probably just be simple metadata, modeling the annotations as fully fledged classes would enable future extensions if they are deemed worth it. For example, a virtual _on_the_annotated_function_being_called( target_object, function_name, arguments ) handler could be added to satisfy the needs of the people dreaming of python decorators. I don't love the idea, as I see annotations as passive data, not things that should themselves act, but the design space is there to be explored.

For example, @onready changes when the variable initializer is evaluated. This is a language feature that cannot be implemented in GDScript itself.

True, but annotations are available at compile time, so the compiler is free to pay attention to them if it wishes. Thereby even the most low-level magical things could be triggered just as well by built-in generic annotations as by hard-coded token names.

@Frontrider
Copy link

Frontrider commented Apr 25, 2024

But I'll be ambition's advocate: What about reimplementing the currently hard coded annotations on top of the generic annotation system? It would decrease the coupling between gdscript and the editor. It's a bit silly for something like @export_flags_2d_navigation to be hard coded in the language; it would be far more flexible for the editor subsystem to register such things dynamically. There is more language complexity in having a long list of hard coded ad-hoc annotations, than in simply having a generic annotation system.

Honestly. This.

Even if we never get custom,"build/export time" annotation processing for GDScript it's still a useful API for language support, and I don't see any architectural reason for why the code for the existing annotations could not just use this API as their data source.

One could argue that it would put it in a half-finished state, but I think that would be a problem.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests