Fix crash when unbinding event handlers from other handlers

Calling Unbind() on another handler from the currently executing handler which
had been bound after (and hence executed before) the handler being unbound
resulted in a crash previously as the iterators used in the loop over all
dynamic event handlers became invalid.

Fix this by storing the dynamic event table entries in a vector instead of a
list (which is also more memory and speed efficient anyhow) and null the
deleted entries instead of removing them to avoid invalidating the iterators
and only really remove them once we finish iterating.

Closes #17229.
This commit is contained in:
Vadim Zeitlin
2015-11-17 15:46:38 +01:00
parent 929b077894
commit 99d9a81e28
4 changed files with 175 additions and 56 deletions

View File

@@ -1122,13 +1122,11 @@ wxEvtHandler::~wxEvtHandler()
if (m_dynamicEvents)
{
for ( wxList::iterator it = m_dynamicEvents->begin(),
end = m_dynamicEvents->end();
it != end;
++it )
size_t cookie;
for ( wxDynamicEventTableEntry* entry = GetFirstDynamicEntry(cookie);
entry;
entry = GetNextDynamicEntry(cookie) )
{
wxDynamicEventTableEntry *entry = (wxDynamicEventTableEntry*)*it;
// Remove ourselves from sink destructor notifications
// (this has usually been done, in wxTrackable destructor)
wxEvtHandler *eventSink = entry->m_fn->GetEvtHandler();
@@ -1696,10 +1694,12 @@ void wxEvtHandler::DoBind(int id,
new wxDynamicEventTableEntry(eventType, id, lastId, func, userData);
if (!m_dynamicEvents)
m_dynamicEvents = new wxList;
m_dynamicEvents = new DynamicEvents;
// Insert at the front of the list so most recent additions are found first
m_dynamicEvents->Insert( (wxObject*) entry );
// We prefer to push back the entry here and then iterate over the vector
// in reverse direction in GetNextDynamicEntry() as it's more efficient
// than inserting the element at the front.
m_dynamicEvents->push_back(entry);
// Make sure we get to know when a sink is destroyed
wxEvtHandler *eventSink = func->GetEvtHandler();
@@ -1723,11 +1723,11 @@ wxEvtHandler::DoUnbind(int id,
if (!m_dynamicEvents)
return false;
wxList::compatibility_iterator node = m_dynamicEvents->GetFirst();
while (node)
size_t cookie;
for ( wxDynamicEventTableEntry* entry = GetFirstDynamicEntry(cookie);
entry;
entry = GetNextDynamicEntry(cookie) )
{
wxDynamicEventTableEntry *entry = (wxDynamicEventTableEntry*)node->GetData();
if ((entry->m_id == id) &&
((entry->m_lastId == lastId) || (lastId == wxID_ANY)) &&
((entry->m_eventType == eventType) || (eventType == wxEVT_NULL)) &&
@@ -1744,28 +1744,78 @@ wxEvtHandler::DoUnbind(int id,
}
delete entry->m_callbackUserData;
m_dynamicEvents->Erase( node );
// We can't delete the entry from the vector if we're currently
// iterating over it. As we don't know whether we're or not, just
// null it for now and we will really erase it when we do finish
// iterating over it the next time.
//
// Notice that we rely on "cookie" being just the index into the
// vector, which is not guaranteed by our API, but here we can use
// this implementation detail.
(*m_dynamicEvents)[cookie] = NULL;
delete entry;
return true;
}
node = node->GetNext();
}
return false;
}
wxDynamicEventTableEntry*
wxEvtHandler::GetFirstDynamicEntry(size_t& cookie) const
{
if ( !m_dynamicEvents )
return NULL;
// The handlers are in LIFO order, so we must start at the end.
cookie = m_dynamicEvents->size();
return GetNextDynamicEntry(cookie);
}
wxDynamicEventTableEntry*
wxEvtHandler::GetNextDynamicEntry(size_t& cookie) const
{
// On entry here cookie is one greater than the index of the entry to
// return, so if it is 0, it means that there are no more entries.
while ( cookie )
{
// Otherwise return the element at the previous index, skipping any
// null elements which indicate removed entries.
wxDynamicEventTableEntry* const entry = m_dynamicEvents->at(--cookie);
if ( entry )
return entry;
}
return NULL;
}
bool wxEvtHandler::SearchDynamicEventTable( wxEvent& event )
{
wxCHECK_MSG( m_dynamicEvents, false,
wxT("caller should check that we have dynamic events") );
wxList::compatibility_iterator node = m_dynamicEvents->GetFirst();
while (node)
{
wxDynamicEventTableEntry *entry = (wxDynamicEventTableEntry*)node->GetData();
DynamicEvents& dynamicEvents = *m_dynamicEvents;
// get next node before (maybe) calling the event handler as it could
// call Disconnect() invalidating the current node
node = node->GetNext();
bool processed = false;
bool needToPruneDeleted = false;
// We can't use Get{First,Next}DynamicEntry() here as they hide the deleted
// but not yet pruned entries from the caller, but here we do want to know
// about them, so iterate directly. Remember to do it in the reverse order
// to honour the order of handlers connection.
for ( size_t n = dynamicEvents.size(); n; n-- )
{
wxDynamicEventTableEntry* const entry = dynamicEvents[n - 1];
if ( !entry )
{
// This entry must have been unbound at some time in the past, so
// skip it now and really remove it from the vector below, once we
// finish iterating.
needToPruneDeleted = true;
continue;
}
if ( event.GetEventType() == entry->m_eventType )
{
@@ -1773,11 +1823,27 @@ bool wxEvtHandler::SearchDynamicEventTable( wxEvent& event )
if ( !handler )
handler = this;
if ( ProcessEventIfMatchesId(*entry, handler, event) )
return true;
{
processed = true;
break;
}
}
}
return false;
if ( needToPruneDeleted )
{
size_t nNew = 0;
for ( size_t n = 0; n != dynamicEvents.size(); n++ )
{
if ( dynamicEvents[n] )
dynamicEvents[nNew++] = dynamicEvents[n];
}
wxASSERT( nNew != dynamicEvents.size() );
dynamicEvents.resize(nNew);
}
return processed;
}
void wxEvtHandler::DoSetClientObject( wxClientData *data )
@@ -1844,19 +1910,20 @@ void wxEvtHandler::OnSinkDestroyed( wxEvtHandler *sink )
wxASSERT(m_dynamicEvents);
// remove all connections with this sink
wxList::compatibility_iterator node = m_dynamicEvents->GetFirst(), node_nxt;
while (node)
size_t cookie;
for ( wxDynamicEventTableEntry* entry = GetFirstDynamicEntry(cookie);
entry;
entry = GetNextDynamicEntry(cookie) )
{
wxDynamicEventTableEntry *entry = (wxDynamicEventTableEntry*)node->GetData();
node_nxt = node->GetNext();
if ( entry->m_fn->GetEvtHandler() == sink )
{
delete entry->m_callbackUserData;
m_dynamicEvents->Erase( node );
delete entry;
// Just as in DoUnbind(), we use our knowledge of
// GetNextDynamicEntry() implementation here.
(*m_dynamicEvents)[cookie] = NULL;
}
node = node_nxt;
}
}