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

[Windows] Improve handling of window destruction #1798

Merged

Conversation

maroider
Copy link
Member

  • Tested on all platforms changed
  • Compilation warnings were addressed
  • cargo fmt has been run on this branch
  • cargo doc builds successfully
  • Added an entry to CHANGELOG.md if knowledge of this change could be valuable to users
  • Updated documentation to reflect any user-facing changes, including notes of platform-specific behavior
  • Created or updated an example program if it would help users understand this functionality
  • Updated feature matrix, if new features were added or implemented

This is a follow-up to #1780 and #1746.

Fixes #1795.

Comment on lines 842 to 846
drop(subclass_input);
Box::from_raw(subclass_input_ptr as *mut SubclassInput<T>);
remove_window_subclass(
window,
Box::from_raw(subclass_input_ptr as *mut SubclassInput<T>),
);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dropping subclass input first and then trying to do something with a pointer to it seems strange to me.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The subclass_input that's dropped has a type of &SubclassInput<T> (introduced here). Dropping it is necessary to avoid UB.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm I don't believe drop will do provide anything in this case as references are copy types, but I'm not sure about the subtile aspects of lifetimes in this case. It's even a bit trickier as we are in a callback executed on the runner inside the subclass input. Explicit calls to Box::from_raw and Box::into_raw should lead to a safer approach in this case without relying on the dereference+borrow from outside the callback.

Copy link
Member Author

@maroider maroider Dec 12, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

references are copy types

Ah, I had completely forgotten, but that then makes me wonder why it was ever there in the first place.

It's even a bit trickier as we are in a callback executed on the runner inside the subclass input

Yeah, I'm kind of at a loss here. The EventLoopRunner<T> needs to be kept alive until the catch_unwind method returns, but the Rc we have to it is dropped before we return. This is admittedly worse in thread_event_target_callback, since that function's subclass_input is seemlingly the last thing that will hold on to an Rc to the EventLoopRunner<T>.

EDIT: Actually, it may be fine since thread_event_target_callback clones the Rc before calling catch_unwind and returning. Windows which use public_window_callback seem to then rely on thread_event_target_callback to not drop its Rc to the EventLoopRunner<T> before the windows get destroyed.

Copy link
Member Author

@maroider maroider Dec 13, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've dug into the code some more, and it seems like there isn't anything guaranteeing that the event target window gets destroyed after all other windows. This means that catch_unwind will try to access the contents of the EventLoopRunner<T> after its destructor has been run. Fixing this would require temporarily cloning and holding onto the Rc until we don't need it.
The slightly more ambiguous issue is to do with pointer aliasing. It feels like having a & reference to something while also having a *mut pointer to it is insta-UB, but I'll have to consult with someone more knowledgeable on this before reaching a conclusion.

Copy link
Member Author

@maroider maroider Dec 19, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The slightly more ambiguous issue is to do with pointer aliasing. It feels like having a & reference to something while also having a *mut pointer to it is insta-UB, but I'll have to consult with someone more knowledgeable on this before reaching a conclusion.

I talked with someone on the Rust community discord, and it seems like this is indeed UB.

WINDOW_SUBCLASS_ID,
)
};
assert_eq!(removal_result, 1);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure how I feel about these assertions. I feel like the most common way for this to fail is if the subclass is already gone and I'm not sure if we should tear down the entire application in that case.

With applications running multiple windows this could lead to a crash of the entire application, assuming this subclass info is added separately for each window (I'm not sure about that?).

Copy link
Member Author

@maroider maroider Dec 11, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The subclass is registered individually for every window, so I don't see how it could be "already gone" when event_loop is the only module with enough information (the callback function pointer and subclass ID) to remove it.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Additionally, it seems like the only "always safe" place to deregister subclasses is during window destruction.

@maroider
Copy link
Member Author

The code in question now relies on Box::from_raw and Box::into_raw rather than casting to references, as previously suggested.
Dropping subclass_input has been moved to the very end of the callback functions.

I realized as I was writing this that the potential panics due to the added asserts is no longer covered by catch_unwind. Admittedly, if a panic does occur, then that means that one of the assumptions we're making to maintain memory safety has been violated.

@maroider
Copy link
Member Author

Panicky bits are now inside catch_unwind again.

.catch_unwind(callback)
.unwrap_or(-1);
if ncdestroy {
remove_event_target_window_subclass(window, subclass_input);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not really happy with that solution tbh - unfortunately the topic is quite complex to get right due to the recursive call of the window proc (e.g DestroyWindow will invoke the callback again for th NCDESTROY case etc) and this approach makes it harder to maintain imo.
To be fair I currently don't have a clear opinion on the best way either. I played a bit around with local dereferencing but it doesn't feel robust either: msiglreith@9fd2cee

Copy link
Member Author

@maroider maroider Dec 15, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would returning (LRESULT, Option<Box<SubclassInput<T>>>) from the callback closure and calling Box::into_raw in the (_, Some(_)) case be better? WM_NCDESTROY would then get to take ownership of the box and deregister the subclass.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or (assuming that I've understood the recursive nature of WndProc correctly): The subclass_input: SubclassInput<T> could be changed to be Rc<SubclassInput<T>> which always has one "fake" reference. At the beginning of each call to *_callback we'd deref the subclass_input_ptr and clone the Rc, which would ensure that we never drop the subclass_input before all calls to the callback are done. WM_NCDESTROY would then deregister the subclass and remove the "fake" reference, in that order.

@maroider maroider force-pushed the remove-subclass-on-ncdestroy branch 3 times, most recently from 8b89c94 to c44fb62 Compare December 19, 2020 17:17
@maroider
Copy link
Member Author

public_window_callback now tracks how deep the recursion is, and only drops the subclass_input when we're certain that there is no recursion. thread_event_target_callback does not seem to get called recursively at the moment, so I've omitted tracking it for now. Tracking recursion in thread_event_target_callback may be advisable, but I'm not 100% certain of the necessity of it.

The window subclass removal happens immediately after matching WM_NCDESTROY, since doing it later causes it to unconditionally fail in public_window_callback.

Copy link
Member

@msiglreith msiglreith left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

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

Successfully merging this pull request may close these issues.

Windows: Improve application teardown
3 participants