-
-
Notifications
You must be signed in to change notification settings - Fork 23.5k
Add RequiredParam<T> and RequiredResult<T> to mark Object * arguments and return values as required
#86079
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
base: master
Are you sure you want to change the base?
Conversation
8c39e62 to
f86e10a
Compare
|
One option would be to have this requirement be strict and crash on failure to fulfil it, I feel this is the area where that'd be appropriate, as if it's not so strict an error print should be used At least I think such an option for this decoration feels appropriate, if not for the complete case |
f86e10a to
26443b8
Compare
Actually, I think I may be wrong about this part! With gcc/clang there are the EDIT: Heh, and we're even using them in Godot for the crash handler already. :-) |
|
Very cool, and that was fast! 😎
Could these metadata flags potentially overlap (maybe not now with the ints, but as we add more over time)?
I'm probably missing something here, but if the expected usage is to always abort the function (i.e. print error + return null) after a "required" precondition fails, do we actually need to create a default instance? Not too worried about performance in the error case, but some custom classes might have side effects in their constructors? (Edit: since we're talking about the engine API here, there are probably no custom classes involved; side effects might still apply to some Godot classes though.) |
I really have no idea what metadata could be added in the future. But like I said in the description, something more extensible like the the options you described in godotengine/godot-proposals#2550 would probably be better.
Nope, you're not missing anything, we don't need to create a default instance. I added that because I was worried that engine contributors could mistakenly believe that I'm really not sure what the right behavior is, so that's what I'm hoping to discuss here :-) |
|
I'd say it depends on how we interpret the argument limitation, is it shouldn't or can't be null? From the proposal:
One use for this would be to remove checks, at least in non-debug builds, the annotation states that "the responsibility of ensuring this is non-null is on the caller" and no checks needs to be done on the callee side In the same sense the check can be used before calling any extension methods or before calling anything from an extension, and exiting there, acting as a barrier I'd prefer it to be a guarantee for the method implementer, a "I can safely skip doing any null checks in this method, someone else handles it for me", or "this can safely be optimized in some sense", akin to the LLVM I'm also not familiar with what the other languages here do for this, so matching some norm there would make sense |
It's hard to plan ahead here. Most flexible would probably be a dictionary (JSON object), but it might also be overkill? {
// ...
"metadata": {
"exact_type": "float64",
"angle": "radian",
"nullable": true
}
}Also, we will need to keep the What I'm a bit worried is that (in a few years), this might devolve into: {
// ...
"meta": "float64|angle=radian|nullable"
}That is, we have a "protocol inside a protocol" -- which would also be breaking btw, since existing code likely checks for the entire value string and would not expect any separators. In other words, we have multiple options:
I tend to favor 1) or 2), but maybe you have more thoughts on this? |
There are many examples of languages that didn't have a concept of nullability/optionality and retrofitted it late. Some I know:
|
|
An idea I mentioned during today's GDExtension meeting is the type-state pattern, meaning that multiple types are used to represent different states of an object. Concretely, we could split
The core idea here is that you cannot obtain |
f9d00d3 to
0cd5ba3
Compare
|
@Bromeon I finally got around to reworking this PR based on your suggestion of using the "type-state pattern" from several months ago This requires using new error macros to unpack the underlying pointer, which I think would also lead to encouraging more consistent use of the error macros for pointer parameters, which I think is something @AThousandShips would like per her earlier comments. Please let me know what you think! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks so much for this! ❤️
Added some first feedback.
It's also convenient that required meta is mutually exclusive with other metas, since it's an object and others are numbers 🙂
0cd5ba3 to
928b024
Compare
821baf1 to
ae96d1b
Compare
6bab68d to
8fcff03
Compare
8fcff03 to
a6ec85f
Compare
RequiredPtr<T> to mark Object * arguments and return values as requiredRequiredParam<T> and RequiredValue<T> to mark Object * arguments and return values as required
|
Alright, I think I've got these changes where I want them to be! I'd still like to do some more testing by marking more APIs with required and making sure everything still works, but I think this can come out of DRAFT and get some wider review :-) Regarding the two CI failures:
@Bromeon Can you do some testing with this using the Rust bindings? |
62caca6 to
eada2e6
Compare
|
CI is passing now! |
09c21c6 to
9ccb329
Compare
|
Thanks a lot for the great work so far! Codegen works nicely! 👍 When testing the invocation, I ran into UB when calling handle_crash: Program crashed with signal 11
Engine version: Godot Engine v4.6.dev.custom_build (9ccb329effa3aaac6d0e0ab3b157b6f02b099926)
Dumping the backtrace. Please include this when reporting the bug on: https://github.com/godotengine/godot/issues
[1] /lib64/libc.so.6(+0x1a070) [0x7f88079ef070] (??:0)
[2] List<Ref<Tweener>, DefaultAllocator>::front() (/.../godot/core/templates/list.h:269)
[3] List<Ref<Tweener>, DefaultAllocator>::clear() (/.../godot/core/templates/list.h:468 (discriminator 1))
[4] List<Ref<Tweener>, DefaultAllocator>::~List() (/.../godot/core/templates/list.h:702)
[5] void LocalVector<List<Ref<Tweener>, DefaultAllocator>, unsigned int, false, false>::_resize<true>(unsigned int) (/.../godot/core/templates/local_vector.h:57 (discriminator 3))
[6] LocalVector<List<Ref<Tweener>, DefaultAllocator>, unsigned int, false, false>::resize(unsigned int) (/.../godot/core/templates/local_vector.h:191)
[7] LocalVector<List<Ref<Tweener>, DefaultAllocator>, unsigned int, false, false>::clear() (/.../godot/core/templates/local_vector.h:153)
[8] Tween::clear() (/.../godot/scene/animation/tween.cpp:222)
[9] SceneTree::finalize() (/.../godot/scene/main/scene_tree.cpp:867 (discriminator 8))
[10] OS_LinuxBSD::run() (/.../godot/platform/linuxbsd/os_linuxbsd.cpp:996)
[11] ...I could also reproduce this when calling godot-rust ERROR: Trying to unreference a SafeRefCount which is already zero is wrong and a symptom of it being misused.
Upon a SafeRefCount reaching zero any object whose lifetime is tied to it, as well as the ref count itself, must be destroyed.
Moreover, to guarantee that, no multiple threads should be racing to do the final unreferencing to zero.
at: _check_unref_safety (./core/templates/safe_refcount.h:186)
================================================================
handle_crash: Program crashed with signal 4
Engine version: Godot Engine v4.6.dev.custom_build (9ccb329effa3aaac6d0e0ab3b157b6f02b099926)
Dumping the backtrace. Please include this when reporting the bug on: https://github.com/godotengine/godot/issues
[1] /lib64/libc.so.6(+0x1a070) [0x7f8e42ff3070] (??:0)
[2] RefCounted::unreference() (/.../godot/core/templates/safe_refcount.h:186 (discriminator 5))
[3] Ref<Tween>::unref() (/.../godot/core/object/ref_counted.h:207 (discriminator 1))
[4] Ref<Tween>::~Ref() (/.../godot/core/object/ref_counted.h:225)
[5] List<Ref<Tween>, DefaultAllocator>::Element::~Element() (/.../godot/core/templates/list.h:52)
[6] void memdelete_allocator<List<Ref<Tween>, DefaultAllocator>::Element, DefaultAllocator>(List<Ref<Tween>, DefaultAllocator>::Element*) (/.../godot/core/os/memory.h:154)
[7] List<Ref<Tween>, DefaultAllocator>::_Data::erase(List<Ref<Tween>, DefaultAllocator>::Element*) (/.../godot/core/templates/list.h:249)
[8] List<Ref<Tween>, DefaultAllocator>::erase(List<Ref<Tween>, DefaultAllocator>::Element*) (/.../godot/core/templates/list.h:436)
[9] SceneTree::process_tweens(double, bool) (/.../godot/scene/main/scene_tree.cpp:835)
[10] SceneTree::process(double) (/.../godot/scene/main/scene_tree.cpp:719)
[11] Main::iteration() (/.../godot/main/main.cpp:4748 (discriminator 3))
[12] OS_LinuxBSD::run() (/.../godot/platform/linuxbsd/os_linuxbsd.cpp:991 (discriminator 1))
[13] ...
Back to this: I would vote for calling it |
9ccb329 to
9b1f42d
Compare
Thanks for testing and finding this issue! I'm not entirely sure how I didn't notice it in my testing originally :-/ I just pushed some changes that I think should fix it - please let me know if it works for you.
Maybe we could call it Personally, I just don't like But, yeah, this sort of thing is always super subjective - I wouldn't be surprised to find another native English speaker from somewhere else that may feel differently about this :-) so perhaps it doesn't matter too much |
I think it's easy to miss, but I wonder why CI didn't catch it. Maybe it makes sense to add some smoke tests for known APIs? Either in core code (if that can trigger this sort of issue) or the godot-cpp CI. In Rust I simply used this: let mut parent = Node::new_alloc();
let child = Node::new_alloc();
parent.add_child(&child);
let tween: Gd<Tween> = parent.create_tween();
assert!(tween.is_instance_valid());
assert!(tween.to_string().contains("Tween"));
parent.free();
|
Makes sense to me! I've made a PR to add such a test to godot-cpp's tests:
That could work! Let's discuss at the GDExtension team meeting before I update this PR, though |
9b1f42d to
17028c6
Compare
RequiredParam<T> and RequiredValue<T> to mark Object * arguments and return values as requiredRequiredParam<T> and RequiredResult<T> to mark Object * arguments and return values as required
|
Per our discussion at today's GDExtension meeting, I've renamed |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
✅ Re-tested 17028c6 with godot-rust, all our tests pass.
This looks good from my side!
…ments and return values as required Co-authored-by: Thaddeus Crews <repiteo@outlook.com>
17028c6 to
659ef86
Compare
Implements proposal godotengine/godot-proposals#2241
The core problem that this is solving is that in some languages, different language constructs/features are used with nullable and non-nullable values, where the non-nullable ones are usually friendlier for the developer to use. This has come up numerous times specifically in the context of Rust, Swift and C#, but would also apply to Kotlin and probably others.
The goal of this PR is to mark various arguments and return values in
extension_api.jsonas "required", which means:How this is accomplished:
Object *orconst Ref<T> &in method arguments, you can useconst RequiredParam<T> &, which will get the parameter marked as "required" inClassDB.EXTRACT_REQUIRED_PARAM_OR_FAIL(m_name, m_param)which acts just likeERR_FAILED_NULL(m_param), except it also assigns a local variable with the object, if it wasn't null. Using the macro is required in order to get the underlying value out of the parameter, which has two advantages:RequiredParam<T>when they don't really mean it, so (hopefully) these markers are less likely to become out-of-dateERR_*macros) which is beneficial to engine development, even outside the context of GDExtensionRequiredParam<T>there isRequiredResult<T>. This separate object is used because return values are more of a "soft" requirement and don't need a special process in order to extract the valueClassDB, the meta for any of required parameters or return values will beMETADATA_OBJECT_IS_REQUIRED, which will look like this in theextension_api.json:rp_*(rather thanp_*) in order to minimize the diff when switching toRequiredParam<T>. This makes it so each change is just +/- 2 lines in the diff, and it's easy to review and see that there were no other functional changesRequiredParam<T>andRequiredResult<T>to be optimized away, and generate basically the same binary as using a plainObject *orRef<T>.UPDATE (2024-12-10): The original version of this had this whole auto-vivifying thing that was terrible, and was replaced with an excellent suggestion from @Bromeon in this comment.
UPDATE (2025-10-17): The semantics of parameters and return values turned out to be different enough that this now has two templates (
RequiredParam<T>andRequiredResult<T>instead of the singleRequiredPtr<T>).