I share your surprise. It should not be dangerous to switch views from the button’s OnClick handler. Alas, you found a serious bug in CGE in this thread, that somehow was unnoticed for a long time → I confirmed now it’s a problem. I will fix it ASAP – I have some new ideas how to do this.
The TCastleView.InternalStop, which is called when view stops,
- calls virtual
Stop
- and then does
UnLoadDesign which, by doing FreeAndNil(FDesignLoadedOwner), frees all the design loaded components.
See castle-engine/src/ui/castleuicontrols_view.inc at bbbfc016ed179b2009cf8194dd692b81adf1f6e3 · castle-engine/castle-engine · GitHub .
I just confirmed it is a potential problem even in our platformer example code, that has OnClick handler doing trivial
procedure TViewMenu.ClickPlay(Sender: TObject);
begin
Container.View := ViewPlay;
end;
Adding 3 Writelns to the TCastleButton code, I can unfortunately confirm that when DoClick finishes, this button was destroyed.
→ results in log like this:
DoClick begin: 134962968364080
Destroying TCastleButton: 134962968367248
Destroying TCastleButton: 134962968366192
Destroying TCastleButton: 134962968365136
Destroying TCastleButton: 134962968364080 // our button is destroyed here
... (various game loading logs)
DoClick end: 134962968364080 // but here we are still in this button's method code
So the button 134962968364080 is really destroyed in the middle of it’s OnClick handler. If the DoClick would do anything more that is accessing the fields → then it’s an undefined behavior. E.g. this is undefined behavior (right now, I mean before the fix; we will fix it to make it OK):
procedure TCastleButton.DoClick;
begin
Writeln('DoClick begin: ', PtrUInt(Self));
Writeln(' DoClick begin says: Name is ', Name);
if Assigned(OnClick) then
OnClick(Self);
Writeln('DoClick end: ', PtrUInt(Self));
Writeln(' DoClick end says: Name is ', Name); // <-- this accesses Self which is now a dangling pointer
end;
Log for above shows, for my system and compiler now (FPC 3.2.2, Linux/x86_64):
DoClick begin: 139979378930736
DoClick begin says: Name is ButtonPlay
Destroying TCastleButton: 139979378933904
Destroying TCastleButton: 139979378932848
Destroying TCastleButton: 139979378931792
Destroying TCastleButton: 139979378930736
...
DoClick end: 139979378930736
DoClick end says: Name is
So it seems like the Name changed to '' . Why? By accident, this memory piece is nil now. But this is an accident, it could as well be a crash, and some memory garbage. Different OS, different program workflow, different compiler / optimization level → could cause a crash.
And even if we would not even do anything from DoClick after calling OnClick, it’s still dangerous because DoClick is done from Release which is done by TCastleContainer.EventRelease ( castle-engine/src/ui/castleuicontrols_container.inc at bbbfc016ed179b2009cf8194dd692b81adf1f6e3 · castle-engine/castle-engine · GitHub ) doing loop for I := C.ControlsCount - 1 downto 0 do if RecursiveRelease(C.Controls[I]) then . So it may work by accident (it did work by accident in all tests until this thread!
), esp. if the list count drops only by 1, but when C has been freed → we are undefined, and we can get crashes.
If you create / destroy the timer in view’s Start / Stop methods, and OnTimer will change views (stopping the view owning it) → then you should be able to confirm my grim conclusions. I mean, things inside view’s Stop will effectively happen in the middle of OnTimer.
Whether the timer is part of the design file (.castle-user-interface) or it’s created explicitly → will not matter.
The only way you could avoid this problem is by destroying timer later, e.g. in view’s Destroy. (So one idea for the fix is to move destroying loaded components later; alas, since you may also create/destroy stuff explicitly in Start/Stop, we should also introduce the “schedule view change” mentioned in earlier post; I’ll experiment today what is best to do).
Do be clear, right now
TCastleTimer.DoTimer does nothing after calling OnTimer.
- And the caller of
DoTimer, which is TCastleTimer.Update, also does nothing after calling DoTimer.
- By accident, other
OnXxx calls also don’t do anything more – I checked TCastleButton.OnClick, TCastleCheckbox.OnChange, TCastleEdit.OnChange.
So at least we don’t get crash in timer… but
- We want to have, in the future, ability to place some Pascal code to be done after
OnXxx on that instance is called.
- Even if not, they are inside a loop inside a container. That they don’t crash so far → is a lucky accident
