macOS: Detect use of heap-allocated QMacAutoReleasePool

QMacAutoReleasePool is backed by an NSAutoreleasePool, which documents that
"you should always drain an autorelease pool in the same context (invocation
of a method or function, or body of a loop) that it was created".

This means allocating QMacAutoReleasePool on the heap is not a supported
use-case, but unfortunately we can't detect it on construction time.

Instead we detect whether or not the associated NSAutoreleasePool has been
drained, and prevent a double-drain of the pool.

Change-Id: Ifd7380a06152e9e742d2e199476ed3adab326d9c
Reviewed-by: Simon Hausmann <simon.hausmann@qt.io>
This commit is contained in:
Tor Arne Vestbø 2017-07-22 16:41:44 +02:00 committed by Simon Hausmann
parent ff9080e740
commit d2a988512e
5 changed files with 185 additions and 1 deletions

View File

@ -84,19 +84,83 @@ QT_FOR_EACH_MUTABLE_CORE_GRAPHICS_TYPE(QT_DECLARE_WEAK_QDEBUG_OPERATOR_FOR_CF_TY
// -------------------------------------------------------------------------
QT_END_NAMESPACE
QT_USE_NAMESPACE
@interface QT_MANGLE_NAMESPACE(QMacAutoReleasePoolTracker) : NSObject
{
NSAutoreleasePool **m_pool;
}
-(id)initWithPool:(NSAutoreleasePool**)pool;
@end
@implementation QT_MANGLE_NAMESPACE(QMacAutoReleasePoolTracker)
-(id)initWithPool:(NSAutoreleasePool**)pool
{
if (self = [super init])
m_pool = pool;
return self;
}
-(void)dealloc
{
if (*m_pool) {
// The pool is still valid, which means we're not being drained from
// the corresponding QMacAutoReleasePool (see below).
// QMacAutoReleasePool has only a single member, the NSAutoreleasePool*
// so the address of that member is also the QMacAutoReleasePool itself.
QMacAutoReleasePool *pool = reinterpret_cast<QMacAutoReleasePool *>(m_pool);
qWarning() << "Premature drain of" << pool << "This can happen if you've allocated"
<< "the pool on the heap, or as a member of a heap-allocated object. This is not a"
<< "supported use of QMacAutoReleasePool, and might result in crashes when objects"
<< "in the pool are deallocated and then used later on under the assumption they"
<< "will be valid until" << pool << "has been drained.";
// Reset the pool so that it's not drained again later on
*m_pool = nullptr;
}
[super dealloc];
}
@end
QT_NAMESPACE_ALIAS_OBJC_CLASS(QMacAutoReleasePoolTracker);
QT_BEGIN_NAMESPACE
QMacAutoReleasePool::QMacAutoReleasePool()
: pool([[NSAutoreleasePool alloc] init])
{
[[[QMacAutoReleasePoolTracker alloc] initWithPool:
reinterpret_cast<NSAutoreleasePool **>(&pool)] autorelease];
}
QMacAutoReleasePool::~QMacAutoReleasePool()
{
if (!pool) {
qWarning() << "Prematurely drained pool" << this << "finally drained. Any objects belonging"
<< "to this pool have already been released, and have potentially been invalid since the"
<< "premature drain earlier on.";
return;
}
// Save and reset pool before draining, so that the pool tracker can know
// that it's being drained by its owning pool.
NSAutoreleasePool *savedPool = static_cast<NSAutoreleasePool*>(pool);
pool = nullptr;
// Drain behaves the same as release, with the advantage that
// if we're ever used in a garbage-collected environment, the
// drain acts as a hint to the garbage collector to collect.
[static_cast<NSAutoreleasePool*>(pool) drain];
[savedPool drain];
}
#ifndef QT_NO_DEBUG_STREAM
QDebug operator<<(QDebug debug, const QMacAutoReleasePool *pool)
{
QDebugStateSaver saver(debug);
debug.nospace();
debug << "QMacAutoReleasePool(" << (const void *)pool << ')';
return debug;
}
#endif // !QT_NO_DEBUG_STREAM
#ifdef Q_OS_MACOS
/*
Ensure that Objective-C objects auto-released in main(), directly or indirectly,

View File

@ -153,6 +153,10 @@ Q_CORE_EXPORT QChar qt_mac_qtKey2CocoaKey(Qt::Key key);
Q_CORE_EXPORT Qt::Key qt_mac_cocoaKey2QtKey(QChar keyCode);
#endif
#ifndef QT_NO_DEBUG_STREAM
QDebug operator<<(QDebug debug, const QMacAutoReleasePool *pool);
#endif
Q_CORE_EXPORT void qt_apple_check_os_version();
QT_END_NAMESPACE

View File

@ -0,0 +1,4 @@
CONFIG += testcase
TARGET = tst_qmacautoreleasepool
QT = core testlib
SOURCES = tst_qmacautoreleasepool.mm

View File

@ -0,0 +1,111 @@
/****************************************************************************
**
** Copyright (C) 2017 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of the test suite of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:GPL-EXCEPT$
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 3 as published by the Free Software
** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-3.0.html.
**
** $QT_END_LICENSE$
**
****************************************************************************/
#include <QtTest/QtTest>
#include <Foundation/Foundation.h>
class tst_QMacAutoreleasePool : public QObject
{
Q_OBJECT
private slots:
void noPool();
void rootLevelPool();
void stackAllocatedPool();
void heapAllocatedPool();
};
static id lastDeallocedObject = nil;
@interface DeallocTracker : NSObject @end
@implementation DeallocTracker
-(void)dealloc
{
lastDeallocedObject = self;
[super dealloc];
}
@end
void tst_QMacAutoreleasePool::noPool()
{
// No pool, will not be released, but should not crash
[[[DeallocTracker alloc] init] autorelease];
}
void tst_QMacAutoreleasePool::rootLevelPool()
{
// The root level case, no NSAutoreleasePool since we're not in the main
// runloop, and objects autoreleased as part of main.
NSObject *allocedObject = nil;
{
QMacAutoReleasePool qtPool;
allocedObject = [[[DeallocTracker alloc] init] autorelease];
}
QCOMPARE(lastDeallocedObject, allocedObject);
}
void tst_QMacAutoreleasePool::stackAllocatedPool()
{
// The normal case, other pools surrounding our pool, draining
// our pool before any other pool.
NSObject *allocedObject = nil;
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
{
QMacAutoReleasePool qtPool;
allocedObject = [[[DeallocTracker alloc] init] autorelease];
}
QCOMPARE(lastDeallocedObject, allocedObject);
[pool drain];
}
void tst_QMacAutoreleasePool::heapAllocatedPool()
{
// The special case, a pool allocated on the heap, or as a member of a
// heap allocated object. This is not a supported use of QMacAutoReleasePool,
// and will result in warnings if the pool is prematurely drained.
NSObject *allocedObject = nil;
{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
QMacAutoReleasePool *qtPool = nullptr;
{
qtPool = new QMacAutoReleasePool;
allocedObject = [[[DeallocTracker alloc] init] autorelease];
}
[pool drain];
delete qtPool;
}
QCOMPARE(lastDeallocedObject, allocedObject);
}
QTEST_APPLESS_MAIN(tst_QMacAutoreleasePool)
#include "tst_qmacautoreleasepool.moc"

View File

@ -67,3 +67,4 @@ SUBDIRS=\
qvector_strictiterators \
qversionnumber
darwin: SUBDIRS += qmacautoreleasepool