Mike Lischke:
> just got around fixing a nasty crash in my application that uses Scintilla in modal dialogs on OSX (10.11). What turned out is that Scintilla uses timers in certain places and they are not working well in modal dialogs. So, here is a patch that makes it working nicely:
It appears to me there may be 2 bugs here: (1) no ticking in modal dialogs (not fatal) and (2) a crash due to object lifetimes. Adding the modal panel run mode fixes (1) and decreases the scope for (2) but does not eliminate it.
Everything discussed here is on the main thread.
The sequence goes like this:
1) The ScintillaCocoa object is created. It is a C++ object, not an NSObject so does not have a reference count and can not directly receive timer events and notifications.
2) In the ScintillaCocoa constructor a TimerTarget object is created to receive events with a pointer back to its ScintillaCocoa ‘owner’. It now has a ref-count of 1.
3) The TimerTarget registers as an observer with the default notification center. This does not change its ref-count.
4) ScintillaCocoa needs some idle events so creates a repeating timer whose target is the TimerTarget. The timer holds a strong reference to the TimerTarget which now has a ref-count of 2.
5) The idle timer times out and calls idleTimerFired: on the TimerTarget. An NSNotification is queued back to the TimerTarget with NSPostWhenIdle so it should only be received when the application is truly idle. The notification stores a reference to the TimerTarget which bumps the ref-count up to 3.
6) The application decides to destroy the ScintillaView and thus the ScintillaCocoa destructor is called. SetIdle(false) is called which calls invalidate on the repeating timer from step 4 which releases the TimerTarget so its ref-count is down to 2. The TimerTarget is released explicitly at the end of the ScintillaCocoa destructor so its ref-count is down to 1.
7) The ScintillaCocoa object is now dead but the TimerTarget is still alive and has a pointer to the ScintillaCocoa object.
8) The NSNotification arrives at the idleTriggered: method of the TimerTarget which calls the IdleTimerFired method on the dead ScintillaCocoa object causing unpleasantness.
9) The NSNotification is finished so is deallocated and releases the TimerTarget which finally has a ref-count of 0 so is deallocated.
This may occur statistically whenever an idling Scintilla is destroyed. Mike’s example with the modal dialog just ensures that any notification will be delayed until after the destruction.
The TimerTarget is shared between idle timers and period timers so the ref-count will not be quite as shown but the period timers are simpler since they never involve the notification queue. All the period timers are cancelled by calling FineTickerCancel in ScintillaCocoa::Finalise in the ScintillaCocoa destructor. Its only the extra references to the TimerTarget in the notification queue which lead to problems.
To fix this correctly, the outstanding notifications should be discarded inside the destructor for the ScintillaCocoa object. This is actually a little tricky as the call for deleting queue elements takes a pointer to the notification object. The notification object is not remembered by the TimerTarget and is ‘slippy’ due to the coalescing nature of the queue: is the new notification dropped or the one already in the queue? While the previous notification could be remembered and dequeued explicitly before enqueuing the new notification, it should be sufficient to release the queue object at shutdown. Further, the pointer back to ScintillaCocoa should be NULLed in the ScintillaCocoa destructor so that there is no possibility of calling back into ScintillaCocoa.
I think the following change should be sufficient for fixing the crashes with modal dialogs without changing the run loop modes. It would help eliminate the possibility of crashes to confirm this in Mike’s code before changing the run loop modes.
diff -r b5b60d746cda cocoa/ScintillaCocoa.mm
--- a/cocoa/ScintillaCocoa.mm Wed Oct 14 14:55:30 2015 +1100
+++ b/cocoa/ScintillaCocoa.mm Fri Oct 16 16:17:20 2015 +1100
@@ -334,6 +334,18 @@
//--------------------------------------------------------------------------------------------------
/**
+ * Method called by owning ScintillaCocoa object when it is destroyed.
+ */
+- (void) ownerDestroyed
+{
+ mTarget = NULL;
+ [notificationQueue release];
+ notificationQueue = nil;
+}
+
+//--------------------------------------------------------------------------------------------------
+
+/**
* Method called by a timer installed by ScintillaCocoa. This two step approach is needed because
* a native Obj-C class is required as target for the timer.
*/
@@ -408,6 +420,7 @@
ScintillaCocoa::~ScintillaCocoa()
{
Finalise();
+ [timerTarget ownerDestroyed];
[timerTarget release];
}
As a further issue, the TimerTarget registers for “Idle” notifications with object nil. This implies that all Scintilla objects receive idle events meant not just for themselves but for any other Scintilla instances in the application. This should be tightened up if that is the case.
Neil