-
-
Notifications
You must be signed in to change notification settings - Fork 582
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
Set instance and instance binding in Wrapped
constructor.
#1446
Set instance and instance binding in Wrapped
constructor.
#1446
Conversation
76100c1
to
0230024
Compare
0230024
to
a57118d
Compare
Wrapped
constructor.
As pointed out in a comment on a similar issue, moving this setup to the I haven't had a chance to really review the code here, but I think we need automated tests that ensure both paths work correctly. This should either be part of this PR or in a PR that's merged before it, since this is one of the things that has gotten broken and fixed in godot-cpp a couple of times. |
I don't know if there are other ways to create extension class objects that I have neglected.
About test, I think the content in "test" folder already done this thing. Both creating extensions node( If there have other situations need to be tested, please point them out. |
Yeah, we're already creating objects in both ways, but we aren't really testing it, ie. the tests won't necessarily fail if we get this wrong. Looking over old issues/PRs where we broke this previously, we commonly got particular error messages, for example:
In the current code, I think that message would actually be different, but perhaps making the CI fail if we got those sort of error messages would help here? Anyway, given that this is tricky, and we have a history of breaking and fixing this multiple times in the past, I'd really like to have some support from the automated tests to help us get this right. :-) |
a57118d
to
acd8e4c
Compare
I found the reson of this error
In my example, We can draw a conclusion, the key point of the new test is to check whether the binding is set many times unexpectedly, instead of creating extension class in godot-cpp and godot. But I can't find a elegant way to do this check currently, maybe we need some new things which provided by godot.
So I added a new test to check whether the binding was set before the constructor of extension class. |
Thanks for your work on this PR! I haven't had a chance to test this yet, but skimming the code I think this could work. I worry a bit about the extra complexity, and if this is going to lead to more function calls during object construction. A few of the new functions are marked as Anyway, I still need to spend some more time to try this out. |
acd8e4c
to
fbaf79a
Compare
Yes, |
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 again for working on this!
I've finally gotten around to doing some testing and deeper review - I have a couple of notes.
However, overall, I think I'm personally in favor of going forward with this. While C++ doesn't make this easy for us, I feel like the added complexity isn't too bad, and this does fix a big compatibility issue between what developers can do in modules vs GDExtensions.
2c6ef7a
to
3b4ff60
Compare
de0b3d0
to
c233430
Compare
Out of curiosity, why not? And why is a stack allocated instance ref counted behind the scenes? The object instance isn’t going into the scene; I’m only creating the instance because CylinderMesh:: create_mesh_array() isn’t exposed. Generally, is there a place I can read more about what is going on here? I would expect to be able to declare classes on the stack and have the initializer do the proper thing. I’d like to learn more so I can avoid any other footguns. |
One of godot-cpp's core design pillars is that code should match (as closely as possible) code in the engine itself, such that (ideally) you can copy C++ code between Godot and godot-cpp, and have it work the same. In Godot, objects aren't fully setup just by their constructor, you need to use
If we weren't trying to match Godot's internal API, we could perhaps bake the incrementing/decrementing of the reference count into the classes constructor/destructor.
Unfortunately, there isn't a lot of documentation on how Godot's internal C++ API are designed. Personally, my primary reference is Godot's source code itself, but I know that isn't ideal. :-) |
@dsnopek It seems that it is not uncommon to instantiate a godot object without using In most cases, a stack allocated godot object like |
Yeah, it's interesting how many people were apparently doing things that should never have worked! And, even if they did sort of work, they were probably not hitting other issues by luck, and/or causing memory leaks they just weren't aware of.
Personally, I think we should keep it as-is. We do need to improve our documentation to better educate developers in the first place, but I think this is primarily a transition thing, and after a couple weeks folks will have adjusted.
I don't think signals are the only thing we have to worry about. There's potentially a lot of things that could go wrong if the Godot objects aren't fully setup, since everything in Godot is going to assume that all objects are.
Having a loud error with a clue, is better than silently "succeeding" with future hidden traps. :-) |
Could we find out all situations which need instance binding and add some check for them? |
Is your thought that we could allow users to use not-fully-setup objects, but then add checks to the situations where we know the result would be particularly bad? Aside from that being tricky to do (I don't know how we'd know an instance binding should exist to check for it being missing), I don't think that's a very sustainable solution. I personally think it would be better to focus on how we can guide users to use the API correctly. However, just to for clarity (for those who don't know the technical details), I think the main situation we need an instance binding is any time an object is passed from GDExtension to Godot and back to GDExtension. Otherwise, multiple redundant wrapper objects could be created. Those won't necessarily cause crashes (although, they could if you delete more than one of the wrappers), but it would leak memory, and cause unexpected situations, for example, where you pass an object to Godot, but get a different one when it passes it back to you (even if its the same object on the Godot side), making |
If we can find and add check for all situations which need instance binding, we can add a method to setup binding manually. I think some simple godot objects to be allocated at stack is reasonable, like |
I disagree. Setting the instance binding when creating an object is part of correctly using the GDExtension interface. I don't think we should be selectively saying that sometimes for some classes we don't do the complete process of creating an object. |
The requirement is to support allocating godot objects at stack. I had a sudden inspiration, to add a macro, for example, defiene it as But this plan will break the code style which similar to godot source (because godot side support allocate objects at stack naturally ). I think I have exhausted my all solutions on this issue, no one is perfect😌. |
Even within Godot itself, classes that descend from |
Almost |
I'm going to document a knock-on effect of this, and propose an option. I'm working on a GDExtension conversion of godotengine/godot#85890 to implement a GridMap with hex-shaped cells. One of the big changes is converting from Internally the C++ code uses // pick a point on the middle of an edge to find the closest edge of the cells
float max = CellId(GRID_RADIUS, -(int)GRID_RADIUS / 2, 0)
.unit_center()
.length_squared(); The CellId *edge_cell = memnew(CellId(GRID_RADIUS, -(int)GRID_RADIUS / 2, 0));
float max = edge_cell->unit_center().length_squared();
memdelete(edge_cell); I'm not doing this for both performance reasons and it gets ungodly ugly in the more complex places like cell iteration. (ugly) Work-aroundThe work-around I'm using right now is that for any type I use natively in C++, that also needs to be accessible in GDScript, I create a wrapper class such as ProposalWould it be possible to add a compiler flag to turn off this guard rail? There are cases such as for I understand the guardrail was added to catch a common problem of passing uninitialized |
If your purpose is to specifically expose such types to the scripting API, then it would be an extremely bad idea to make those types incomplete. Instead, consider exposing setters and getters on the parent class for a proxied access to the properties of this data structure that you want to expose. This would also be a more conventional way to design the API in Godot. PS. You don't actually need the set method in your example. You can pass arguments to the constructor in memnew. Although with so many arguments it may look too heavy on the reader. |
@YuriSizov I called out in my comment that I was not exposing incomplete types to the GDScript layer. The classes will be properly bound when they reach the scripting api because they can only reach GDScript by being returned via The purpose of this is to use the types in C++, because they’re safe to use in C++ without setting up the engine bindings. There are a sufficient number of instances where these types are created, used, and discarded without crossing the boundary into the core engine to consider allowing the behavior.
EDIT: I found the proper syntax; it's not an argument to This still leaves the performance hit associated with dynamically allocating and releasing the type when it is safe to use on the stack. ORIGINAL (incorrect): godot-cpp/include/godot_cpp/core/memory.hpp Line 100 in 4131b7f
|
@dmlary In my opinion, only some native godot objects worth to be used without If you don't want to expose a class to scripting, the class don't need to inherit from godot classes. In some case, you can consider to combine them by using multiple Inheritance.
As my previous comment show, some classes in godot source code are used in stack allocated. To force require users use |
@Daylily-Zeleen I can open a PR with this change. |
The guard in `Wrapped::Wrapped(const StringName)` detects the partial initialization of `godot::Object` subclasses. This is needed because using `new` to allocate a `godot::Object` subclass will result in unexpected crashes because the bindings for the instance have not been set. The side effect of this guard is that it also prevents any use of a `godot::Object` subclass on the stack. There are godot native types such as `godot::RegEx` that are safe to use on the stack, and are used that way throughout the godot source. Similarly, custom classes that subclass `godot::Object` may also be used safely on the stack[^1]. In this commit, I add a compiler flag to disable this guard. This will allow godot-cpp users who understand the safety implications to use `godot::Object` subclasses on the stack. [^1]: godotengine#1446 (comment)
Okay, I did not understand that initially. Then using two different types, plain one and a wrapper, makes sense for how Godot is designed. |
@YuriSizov the problem with using two different types is that you end up having to write a lot of boilerplate for that wrapper class. Any function you expose that doesn't require wrapping or unwrapping a type in I've opened PR #1585 to allow GDExtension creators to use a compiler flag to disable the guard that prevents the use of godot::Object subclasses from being used on the stack. The default is to leave the guard in place so people learn about the need for @YuriSizov on a separate note, I found Full example using a wrapper classI've been going down this path in my repo, and I wanted to provide a concrete example of what using a separate wrapper class looks like. I've cleaned up one of the C++ classes I'm working with. This class class CellId {
public:
// axial hex coordinates, and y coordinate
int q, r, y;
CellId() : q(0), r(0), y(0){};
CellId(int q, int r, int y) : q(q), r(r), y(y){};
// get the position of the center of this cell, assuming cell radius=1,
// height=1
Vector3 unit_center() const;
// check if a point falls within this cell
bool contains_point(Vector3 &point) const;
// calculate the distance from another hex cell
unsigned distance(const CellId &) const;
// get the CellId of the neighbor in the specified direction
CellId get_neighbor(int direction) const;
}; I use this class in the following ways:
For the
The wrapper class for class CellIdWrapper : public RefCounted {
GDCLASS(CellIdWrapper, RefCounted)
CellId cell_id;
public:
CellIdWrapper(const CellId &cell_id) : cell_id(cell_id){};
CellIdWrapper(){};
Vector3 unit_center() const { return cell_id.unit_center(); };
bool contains_point(Vector3 point) const {
return cell_id.contains_point(point);
}
unsigned distance(const Ref<CellIdWrapper> other) const {
return cell_id.distance(other->cell_id);
}
Ref<CellIdWrapper> get_neighbor(int direction) const {
Ref<CellIdWrapper> ref;
ref.instantiate();
ref->cell_id = cell_id.get_neighbor(direction);
return ref;
};
protected:
static void _bind_methods();
}; Of the four types of functions, only two of them require GDScript specific code. Example with guard disabled, allowing stack allocationWhen the guard is disabled, we gain the ability to use class CellId : public RefCounted {
GDCLASS(CellId, RefCounted)
public:
// axial hex coordinates, and y coordinate
int q, r, y;
CellId() : q(0), r(0), y(0){};
CellId(int q, int r, int y) : q(q), r(r), y(y){};
// get the position of the center of this cell, assuming cell radius=1,
// height=1
Vector3 unit_center() const;
// check if a point falls within this cell
bool contains_point(Vector3 &point) const;
// calculate the distance from another hex cell
unsigned distance(const CellId &) const;
unsigned _distance(const Ref<CellId> other) const {
return distance(**other);
}
// get the CellId of the neighbor in the specified direction
CellId get_neighbor(int direction) const;
Ref<CellId> _get_neighbor(int direction) const {
CellId neighbor = get_neighbor(direction);
Ref<CellId> ref;
ref.instantiate();
// this part is ugly, but it's the most direct method I could find to keep comparison clear
ref->q = neighbor.q;
ref->r = neighbor.r;
ref->y = neighbor.y;
return ref;
};
protected:
static void _bind_methods();
}; The key change is that the GDScript-specific functions get renamed to Overall the difference lies in that we can bind the existing Another possible solutionIf This approach works well for my case, but doesn't help anyone who wants to use native godot types like If this path is taken, it would be nice if we could also override the name of the class used in GDScript. I'm already getting tired of typing |
Sorry, didn't have a chance to read through the whole thing, but wanted to quickly point out that you are allowed to assign Ref<SiOPMWavePCMData> pcm_data = memnew(SiOPMWavePCMData(p_data, (int)(p_sampling_note * 64), p_src_channel_count, p_channel_count)); I don't think this introduces any performance differences, as it should simply use the assigned instance in place of the internally initialized one. And also, yes, I understand the boilerplate argument. I am of an opinion that the style of the But for the official project I'd prefer if it would stick to an authoritative approach rather than flexibility to accommodate different styles, approaches, and targets. I understand that your main argument is performance, but the engine itself seems to exist fine within the same limitations, so it doesn't strike me as such a critical problem as to address it in the general purpose binding that is |
@YuriSizov oh, that's nice! Thank you for the tip.
@YuriSizov But it actually doesn't. Godot explicitly made |
I agree with a lot of what @YuriSizov wrote above:
I agree, that this would be a good way to handle this. You can have plain C++ classes that you use internally, which you can use however you want. But if you want to bind a class with Godot via godot-cpp, then there are some rules that need to be followed. Separating those into separate classes (internal ones vs bound Godot wrappers) makes a lot of sense to me.
I agree with this as well. One of godot-cpp's design goals is to mimick Godot's internal APIs as much as possible, and that includes the rules for how to use classes that descend from I understand that there are a few places in Godot where it isn't presently following these rules. However, as I've said previously, I'm of the opinion that those are bugs that should be fixed in Godot, not something we should be attempting to allow from godot-cpp.
Yes, godot-cpp isn't the only possible way to build a C++ binding for GDExtension. It has a particular set of goals and that has influenced its design choices. |
Ok, it seems y'all are pretty fixed on preserving the guard. I can accept that. It seems like the accepted solution for performance is a C++-only class, and a wrapper class for sharing values to GDScript. To reduce the maintenance overhead on the wrapper class, is there any way to add the ability to bind a GDScript method to an instance variable in the godot object? In concrete terms from the example1 binding Footnotes |
Had to add a class for wrapping HexMapCellId to pass to GDScript. There's a whole series of problems here. First of all, ClassDB::bind_method() can only bind methods that return a type that can be cast to a Variant. From some resources the only two viable options were Ref<T> or a pointer to an Object subclass[^1]. Pointer means we need to somehow manage memory that we've entirely passed up to GDScript. Instead we use Ref<T>. Second problem, you cannot use Object subclasses on the stack in godot-cpp[^2]. This means if we make HexMapCellId a subclass of godot::Object, we cannot use it on the stack, or easily construct it using an initializer. Insteat we'd have to use `memnew()` all over the place. So we're using a wrapper class `HexMapCellId < public godot::RefCounted`. It's a boiler-plate heavy approach, but it works for now. Did propose to godot-cpp to add a compiler flag to disable the guard-rail that keeps us from using `godot::Object` subclasses on the stack[^3]. [^1]: https://forum.godotengine.org/t/returning-custom-classes-from-c-gdextension/65920/18 [^2]: godotengine/godot-cpp#1446 (comment) [^3]: godotengine/godot-cpp#1446 (comment)
Have two approches, C++ class and GDScript wrapper class `HexMapCellId` and `HexMapCellIdRef`, or unified class in `HexMapIterAxial`. The unified approach requires an upstream change[^1] to allow stack allocation of `godot::Object` classes. `HexMapIterAxial` being an Object means all the doctest tests are now broken, as it cannot find the `godot::String` implementation pointers. The tests crash currently because of this. We do have tests in the godot demo project, using GUT. They confirm that `HexMapCellId.get_neighbors()` is properly working from GDScript. I'm not sure which direction I'll go in the long term. I'd like to be able to unit test in C++ and GDScript, but I don't want to have to maintain a pile of pass-through proxy methods in the wrapper class. I made one suggestion[^2] in godot-cpp to allow binding methods to class instance methods, but I don't even know if that's possible. [^1]: godotengine/godot-cpp#1585 [^2]: godotengine/godot-cpp#1446 (comment)
Fixes #1039
Refer to this comment. This is my solution to advance the timing of setting instance and instance binding.
There may be some potential problems that I haven't found, please review it carefully.
This is my test class:
Before this pr, it will push an error: