godot
godot copied to clipboard
Fix call with wrong arguments in Callable.callv() when passing a readonly (const) Array
Use a duplicate of the passed array when it is read-only.
Prior to this fix, when the array was marked as read-only, the temporary address of p_arguments->read_only was used for all parameters. Consequently, this caused all parameters to reference the last element of the array.
Fixes #93600.
This would really benefit from having a test added under modules/gdscript/tests/scripts/runtime/features to make sure it actually fixes the bug and makes sure it remains fixed
As this is not GDScript specific, shouldn't it be a unit test in tests/core/variant/test_callable.h ?
We do many of these tests there
~Also how did you confirm this was not script specific? I at least suspect it's specific to arguments in script methods being called, due to how the bindings and data passing works~
My bad missed some details, but I think the script side tests works best to fix this
Given some of the details here I think I have a potential alternative solution that might be better, I'll look at it along with my other changes in:
- #88905
But if you want to try it out I'll drop the alternative here, it probably would be faster and easier avoiding a full duplication with:
Variant *args = nullptr;
const Variant **argptrs = nullptr;
if (argcount) {
argptrs = (const Variant **)alloca(sizeof(Variant *) * argcount);
// Copy the arguments if read-only, otherwise the address of p_arguments->read_only will be copied.
if (p_arguments.is_read_only()) {
args = (Variant *)alloca(sizeof(Variant) * argcount);
for (int i = 0; i < argcount; i++) {
memnew_placement(&args[i], Variant(p_arguments[i]));
argptrs[i] = &args[i];
}
} else {
for (int i = 0; i < argcount; i++) {
argptrs[i] = &p_arguments[i];
}
}
}
CallError ce;
Variant ret;
callp(argptrs, argcount, ret, ce);
if (args) {
for (int i = 0; i < argcount; i++) {
args[i].~Variant();
}
}
return ret;
Edit: Tested and it works well, a bit more code but avoids unnecessary duplication
Having a second buffer allocated for a copy of the elements is what I have done initially, but I chose to use duplicate instead in the end, because I found it more readable.
I'm not sure that the call to duplicate has that much overhead since the underlying Vector uses CoW if I recall correctly. But I might be wrong.
I'm not sure that the call to duplicate has that much overhead since the underlying Vector uses CoW if I recall correctly. But I might be wrong.
I'm not sure, that's a good point and it might not matter, but i don't think the code is significantly less readable and it avoids potential weirdness
I did a quick check, and indeed the duplication is done using CoW. If it were up to me I'd keep the duplicate as it's simpler IMO, but I don't have strong opinions about it. You tell me. I'll add the test in modules/gdscript/tests/scripts/runtime/features in the meantime.
You decide, both are fine by me, and let's see what the core team says
A few thoughts:
- CoW involves dealing with the atomic refcount, with performance implications.
- This looks like an ad hoc fix for an issue due to how read-only arrays are implemented. It may be better to assess solutions to the root issue.
I think it might be difficult to handle the issue with read access on the array itself, given the nature of the reference, but I'd say those points reinforce the use of my suggestion above, it avoids the COW issues, and avoids any issues with the data access
I can't think of a reliable way to handle the read only status of array without breaking compatibility, or allocating more data somehow, but regardless I think this situation warrants at least a temporary local solution
This also needs to be done for Object::callv
Indeed this solution is not perfect, and we should consider modifying how read-only arrays are implemented. I will try to think of a nice solution when I have more time.
Made an issue tracking the broader issues of the read-only state:
- https://github.com/godotengine/godot/issues/93702
Agreed, I removed the changes.
Thanks!