-
-
Notifications
You must be signed in to change notification settings - Fork 198
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
Implicit binds in virtual calls cause borrow errors #338
Comments
I guess you mean panic?
Yep, exactly. It's very hard to know if the But the big problem is that we lose information about local borrows. How would we prevent scenarios like this? #[derive(GodotClass)]
#[class(init, base = Node)]
pub struct Bar {
#[base]
base: Base<Node>,
vec: Vec<i32>,
}
#[godot_api]
impl NodeVirtual for Bar {
fn ready(&mut self) {
self.vec = vec![1, 2, 3];
for _ in self.vec.iter() {
self.base.add_child(Node::new_alloc());
}
}
fn on_notification(&mut self, what: NodeNotification) {
self.vec.clear(); // unsound
}
} Maybe it needs a completely different design, where such methods don't implicitly borrow The same problem applies to signals. I don't see how we can allow safe reborrows. And requiring |
One spontaneous idea: explicit partial borrows. #[derive(GodotClass)]
#[class(init, base = Node)]
pub struct Bar {
#[base]
base: Base<Node>,
// Let's say we have 2 data fields:
vec: Vec<i32>,
notify_count: usize,
} And now, the user would need to explicitly request access to certain fields: #[godot_api]
impl NodeVirtual for Bar {
fn ready(&mut self) {
self.vec = vec![1, 2, 3];
// Tell gdext that we're only accessing these two fields
// (it should limit the scope if possible, e.g. through a local function).
// Closure syntax is just an example for familiarity; up to bikeshedding and technical limits.
partial_borrow!(self => |vec, mut base| {
for _ in vec.iter() {
base.add_child(Node::new_alloc());
}
});
}
fn on_notification(this: Gd<Self>, what: NodeNotification) {
// This would panic:
partial_borrow!(this => |mut vec| {
vec.clear();
});
// However this would work:
partial_borrow!(this => |mut notify_count| {
*notify_count += 1;
});
}
} It's of course clumsy, but Rust itself is very pedantic about not calling |
This is not related to the issue title, but commenting here as it is relevant to the rest of the comments.
Same is also true for rpcs. Using rpcs right now infest the rust with "string method" calls. I tried limiting rpc calls like so: pub fn set_tool(&mut self, mut item: Item) {
self.rpc("r_set_tool".into(), &[item.to_variant()]);
} But it doesn't work for the same reason. Only workaround that user can do is either using |
Also one important thing to note is that deferring is not really a workaround here: It explicitly changes behavior by introducing delays. For some applications that is unacceptable |
Another option could be to only ever use a |
@Soremwar mentioned a closely related issue in #383, which is a more general case of this: calling Rust -> GDScript -> Rust, which causes double borrows. That one is probably even harder to solve than |
Allowing #[func]
fn foo(_self: Gd<Self>) {
{
let _self = _self.bind_mut();
// do stuff with self
}
// some function call that would potentially grab a mutable reference to self
_self.call("some_func", ...);
{
let _self = _self.bind_mut();
// do other stuff with self
}
} |
This is an interesting challenge, and probably far more general than the specific example in the issue title. Working with notifications or signals in general has the potential to "backfire", leading to a panic. Adding another data point: I encountered this issue while implementing a custom item list, which basically started as a port of item_list.cpp of Godot. The pattern used in that implementation doesn't work out-of-the box in Rust, because it involves backfiring signals, even in a relatively subtle way:
Some thoughts about more general solutions: The idea in #359 looks quite helpful. For instance, instead of disconnecting and reconnecting the signal, the drawing logic could have set #[func]
fn on_scroll_changed(_self: Gd<Self>) {
// Only try to mutable bind & trigger a redraw if we are not already drawing...
if self.is_within_redraw {
let _self = self.bind_mut();
_self.base.queue_redraw()
}
} But the manual maintenance of #[func]
fn on_scroll_changed(_self: Gd<Self>) {
if let Some(_self) = _self.try_bind_mut() {
// No other mut binding exists, so we know we have been triggered from "externally".
// In this case we want to trigger a redraw.
_self.base.queue_redraw()
} else {
// Apparently we cannot get a mut binding. This indicates that we have triggered ourselves!
// And assuming this can only come from the drawing/layouting logic itself, we actually don't
// have to do anything here in this particular example...
}
} The interesting question is what to do in the #[func]
fn on_scroll_changed(&self) { // due to interior mutability we don't need &mut
// The idea of `run_or_enqueue` would be to either execute the code block synchronously
// if a mutable binding is possible, or to push the FnOnce into a queue with interior mutability.
// If desirable, a plain `.enqueue()` that is consistently lazy could be offered a well.
self.deferred_queue.run_or_enqueue(|&mut this| {
// here we have a &mut binding
});
}
#[func]
fn process(&mut self, delta: f64) {
// This executes all the accumulated actions. Since the `process` function runs once per frame, the amount
// of delay can be okay-ish. To reduce the delay the user could also place the `run_all()` strategically in
// certain other methods.
self.deferred_queue.run_all();
} Not sure if this makes sense, but I feel that offering a simple way to "defer" signals/notifications could help a lot to mitigate this issue, because it breaks the cyclic nature of the problem. In many use cases the slight delay may be a reasonable price to pay for being guaranteed not to run into "signal cycle panics". |
In this particular case, you could just do #[func]
fn on_scroll_changed(_self: Gd<Self>) {
_self.upcast().queue_redraw()
} and in general i think the solution is gonna have to be that people need to be a bit more fine-grained with when they do |
|
Working on #396, the constant across calling a method with either So maybe we want the user to write something like:
Then internally when registering We're not doing any runtime branching, and you can still call The one issue is that |
Actually, hold on, The cleaner option would be to have a generic |
Is the common denominator not simply I don't see why you need |
I'm assuming |
Fair point. But is |
If I understand correctly, we currently have (with other methods left out):
We can't do something like:
because in whatever code we generate in That said, it sounds like it's efficient enough to do:
And then users either implement
into
|
add_child
in a &mut self
function if Self
overrides on_notification
I have an idea for how to mostly fix these issues. BackgroundThe basic issue is that we have an indirect call of a rust method through ffi, but this shouldn't theoretically be an issue as you are allowed to do this in rust. For instance, let's say you have a signal fn process(&mut self, delta: f64) {
self.base.emit("foo")
}
fn bar(&mut self) {
// code
} Is the same as doing fn process(&mut self, delta: f64) {
self.bar()
}
fn bar(&mut self) {
// code
} which is fine, but the original code would fail. The reason this is fine is because rust knows that calling So if we can somehow create an API that allows a user to call methods which may potentially call back into self (such as Basic IdeaThe idea has two major components (all names used are up for bikeshedding):
Current Idea For ImplementationMy current idea for implementing this is: We add a counter to
Additionally we create a drop-guard which When a rust-method is called from Godot (that takes So as some code-examples we'd take this: #[derive(GodotClass)]
#[class(base = Node, init)]
struct MyClass {
#[base]
base: Base<Node>
}
#[godot_api]
impl INode for MyClass {
fn ready(&mut self) {
// base() here returns the drop guard
self.base().call("some_func".into(), &[]);
}
}
#[godot_api]
impl MyClass {
#[func]
fn some_func(&mut self) {
// code
}
} And we generate this (very abbreviated) glue-code: fn generated_ready(storage: &InstanceStorage<MyClass>) {
if storage.get_possibly_aliased() == 0 {
let mut instance = unsafe { storage.bind_self() };
instance.ready()
} else {
let mut instance = storage.bind_mut();
instance.ready()
}
}
fn generated_some_func(storage: &InstanceStorage<MyClass>) {
if storage.get_possibly_aliased() == 0 {
let mut instance = unsafe { storage.bind_self() };
instance.some_func()
} else {
let mut instance = storage.bind_mut();
instance.some_func()
}
} One thing to consider is adding an immutable/mutable pair of these, as it'd be safe to call a method that takes a old contentsi wonder if we can add another kind of unsafe bind, like `bind_self`. which returns a `GdSelf` that derefs to `&mut self`. where the safety invariant is that it is only used to call a method on it.it fails if there exists a this needs one modification, accessing This should avoid the unsoundness with allowing reentrancy @Bromeon identified above. This is additionally also semantically more correct imo, since a Footnotes
|
My rough idea from above, a bit modified, has been implemented in #501. I will write a more detailed description in the PR of how it works before marking the PR as ready for review. I could also split some stuff out of that PR to make it more manageable. |
Edit bromeon -- title was "Cannot
add_child
in a&mut self
function ifSelf
overrideson_notification
"add_child
will trigger a notification (https://docs.godotengine.org/en/stable/classes/class_node.html#class-node-constant-notification-child-order-changed), and that will in turn trigger a call toon_notification
, which will try to get a mutable borrow ofself
. This causes a crash.It seems like this is a big issue tbh because it basically makes
self.add_child
almost unusable wheneveron_notification
is overridden.I'm not sure what a good solution to this is, but it's related to the common issue of calling a signal that will trigger on self.
The text was updated successfully, but these errors were encountered: