diff --git a/src/platformsupport/eventdispatchers/qeventdispatcher_cf_p.h b/src/platformsupport/eventdispatchers/qeventdispatcher_cf_p.h index 0b9dda2b7f..1742a9b939 100644 --- a/src/platformsupport/eventdispatchers/qeventdispatcher_cf_p.h +++ b/src/platformsupport/eventdispatchers/qeventdispatcher_cf_p.h @@ -168,7 +168,17 @@ public: if (!runLoop) runLoop = CFRunLoopGetCurrent(); - CFRunLoopAddObserver(runLoop, m_observer, mode); + if (!CFRunLoopContainsObserver(runLoop, m_observer, mode)) + CFRunLoopAddObserver(runLoop, m_observer, mode); + } + + void removeFromMode(CFStringRef mode, CFRunLoopRef runLoop = 0) + { + if (!runLoop) + runLoop = CFRunLoopGetCurrent(); + + if (CFRunLoopContainsObserver(runLoop, m_observer, mode)) + CFRunLoopRemoveObserver(runLoop, m_observer, mode); } private: diff --git a/src/plugins/platforms/ios/ios.pro b/src/plugins/platforms/ios/ios.pro index 263577d43f..9b53974998 100644 --- a/src/plugins/platforms/ios/ios.pro +++ b/src/plugins/platforms/ios/ios.pro @@ -9,8 +9,8 @@ LIBS += -framework Foundation -framework UIKit -framework QuartzCore OBJECTIVE_SOURCES = \ plugin.mm \ - qiosmain_wrapper.mm \ qiosintegration.mm \ + qioseventdispatcher.mm \ qioswindow.mm \ qiosscreen.mm \ qiosbackingstore.mm \ @@ -24,6 +24,7 @@ OBJECTIVE_SOURCES = \ HEADERS = \ qiosintegration.h \ + qioseventdispatcher.h \ qioswindow.h \ qiosscreen.h \ qiosbackingstore.h \ diff --git a/src/plugins/platforms/ios/qiosapplicationdelegate.mm b/src/plugins/platforms/ios/qiosapplicationdelegate.mm index d4fd613ae3..e06d2b8840 100644 --- a/src/plugins/platforms/ios/qiosapplicationdelegate.mm +++ b/src/plugins/platforms/ios/qiosapplicationdelegate.mm @@ -46,8 +46,6 @@ #include -extern "C" int main(int argc, char *argv[]); - @implementation QIOSApplicationDelegate @synthesize window; @@ -68,30 +66,9 @@ extern "C" int main(int argc, char *argv[]); [self.window makeKeyAndVisible]; - // We schedule the main-redirection for the next eventloop pass so that we - // can return from this function and let UIApplicationMain finish its job. - [NSTimer scheduledTimerWithTimeInterval:.01f target:self - selector:@selector(runUserMain) userInfo:nil repeats:NO]; - return YES; } -- (void)runUserMain -{ - NSArray *arguments = [[NSProcessInfo processInfo] arguments]; - int argc = arguments.count; - char **argv = new char*[argc]; - for (int i = 0; i < argc; ++i) { - NSString *arg = [arguments objectAtIndex:i]; - argv[i] = reinterpret_cast(malloc([arg lengthOfBytesUsingEncoding:[NSString defaultCStringEncoding]])); - strcpy(argv[i], [arg cStringUsingEncoding:[NSString defaultCStringEncoding]]); - } - - main(argc, argv); - delete[] argv; -} - - - (void)dealloc { [qiosViewController release]; diff --git a/src/plugins/platforms/ios/qiosmain_wrapper.mm b/src/plugins/platforms/ios/qioseventdispatcher.h similarity index 73% rename from src/plugins/platforms/ios/qiosmain_wrapper.mm rename to src/plugins/platforms/ios/qioseventdispatcher.h index cb9a2c161e..83267e80ea 100644 --- a/src/plugins/platforms/ios/qiosmain_wrapper.mm +++ b/src/plugins/platforms/ios/qioseventdispatcher.h @@ -39,11 +39,33 @@ ** ****************************************************************************/ -#include "qiosapplicationdelegate.h" +#ifndef QIOSEVENTDISPATCHER_H +#define QIOSEVENTDISPATCHER_H -extern "C" int qtmn(int argc, char *argv[]) +#include + +QT_BEGIN_NAMESPACE + +class QIOSEventDispatcher : public QEventDispatcherCoreFoundation { - @autoreleasepool { - return UIApplicationMain(argc, argv, nil, NSStringFromClass([QIOSApplicationDelegate class])); - } -} + Q_OBJECT + +public: + explicit QIOSEventDispatcher(QObject *parent = 0); + + bool processEvents(QEventLoop::ProcessEventsFlags flags) Q_DECL_OVERRIDE; + void interrupt() Q_DECL_OVERRIDE; + + void handleRunLoopExit(CFRunLoopActivity activity); + + void checkIfApplicationShouldQuit(); + void interruptQApplicationExec(); + +private: + uint m_processEventCallsAfterAppExec; + RunLoopObserver m_runLoopExitObserver; +}; + +QT_END_NAMESPACE + +#endif // QIOSEVENTDISPATCHER_H \ No newline at end of file diff --git a/src/plugins/platforms/ios/qioseventdispatcher.mm b/src/plugins/platforms/ios/qioseventdispatcher.mm new file mode 100644 index 0000000000..6148338000 --- /dev/null +++ b/src/plugins/platforms/ios/qioseventdispatcher.mm @@ -0,0 +1,522 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/legal +** +** This file is part of the plugins of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** 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 Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.html. +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "qioseventdispatcher.h" +#include "qiosapplicationdelegate.h" +#include "qiosglobal.h" + +#include +#include +#include + +#import +#import +#import +#import +#import + +#import + +#include // Here be dragons + +#include + +#define qAlignDown(val, align) val & ~(align - 1) +#define qAlignUp(val, align) qAlignDown(val + (align - 1), align) + +static const size_t kBytesPerKiloByte = 1024; +static const long kPageSize = sysconf(_SC_PAGESIZE); + +/* + The following diagram shows the layout of the reserved + stack in relation to the regular stack, and the basic + flow of the initial startup sequence. Note how we end + up back in applicationDidLaunch after the user's main + recurses into qApp-exec(), which allows us to return + from applicationDidLaunch and spin the run-loop at the + same level (UIApplicationMain) as iOS nativly does. + + +-----------------------------+ + | qtmn() | + | +--------------------+ <-- base + | +----> main() | | + | | +--------------------+ | + | | | ... | | + | | +--------------------+ | + | | | qApp->exec() | | + | | +--------------------+ | + | | | processEvents() | | + | | | | | + | | +--+ longjmp(a) | | + | | | | | | + | | | +--------------------+ | + | | | | | | + | | | | | | + | | | | unused | | + | | | | | | + | | | | | | + | | | +--------------------+ <-- limit + | | | | memory guard | | + | | | +--------------------+ <-- reservedStack + +-|-|-------------------------+ + | | | UIApplicationMain() | + +-|-|-------------------------+ + | | | applicationDidLaunch() | + | | | | + | | +--> setjmp(a) | + | +----+ trampoline() | + | | + +-----------------------------+ + + Note: the diagram does not reflect alignment issues. +*/ + +namespace +{ + struct Stack + { + uintptr_t base; + uintptr_t limit; + + static size_t computeSize(size_t requestedSize) + { + if (!requestedSize) + return 0; + + // The stack size must be a multiple of 4 KB + size_t stackSize = qAlignUp(requestedSize, 4 * kBytesPerKiloByte); + + // Be at least 16 KB + stackSize = qMax(16 * kBytesPerKiloByte, stackSize); + + // Have enough extra space for our (aligned) memory guard + stackSize += (2 * kPageSize); + + // But not exceed the 1MB maximum (adjusted to account for current stack usage) + stackSize = qMin(stackSize, ((1024 - 64) * kBytesPerKiloByte)); + + // Which we verify, just in case + struct rlimit stackLimit = {0, 0}; + if (getrlimit(RLIMIT_STACK, &stackLimit) == 0 && stackSize > stackLimit.rlim_cur) + qFatal("Unexpectedly exceeded stack limit"); + + return stackSize; + } + + void adopt(void* memory, size_t size) + { + uintptr_t memoryStart = uintptr_t(memory); + + // Add memory guard at the end of the reserved stack, so that any stack + // overflow during the user's main will trigger an exception at that point, + // and not when we return and find that the current stack has been smashed. + uintptr_t memoryGuardStart = qAlignUp(memoryStart, kPageSize); + if (mprotect((void*)memoryGuardStart, kPageSize, PROT_NONE)) + qWarning() << "Failed to add memory guard:" << strerror(errno); + + // We don't consider the memory guard part of the usable stack space + limit = memoryGuardStart + kPageSize; + + // The stack grows downwards in memory, so the stack base is actually + // at the end of the reserved stack space. And, as the memory guard + // was page aligned, we need to align down the base as well, to + // keep the requirement that the stack size is a multiple of 4K. + base = qAlignDown(memoryStart + size, kPageSize); + } + + bool isValid() + { + return base && limit; + } + + size_t size() + { + return base - limit; + } + + static const int kScribblePattern; + + void scribble() + { + memset_pattern4((void*)limit, &kScribblePattern, size()); + } + + void printUsage() + { + uintptr_t highWaterMark = limit; + for (; highWaterMark < base; highWaterMark += 4) { + if (memcmp((void*)highWaterMark, &kScribblePattern, 4)) + break; + } + + qDebug("main() used roughly %lu bytes of stack space", (base - highWaterMark)); + } + }; + + const int Stack::kScribblePattern = 0xfafafafa; + + Stack userMainStack; + + jmp_buf processEventEnterJumpPoint; + jmp_buf processEventExitJumpPoint; + + bool applicationAboutToTerminate = false; + jmp_buf applicationWillTerminateJumpPoint; + + bool debugStackUsage = false; +} + +static int infoPlistValue(NSString* key, int defaultValue) +{ + static NSBundle *bundle = [NSBundle mainBundle]; + NSNumber* value = [bundle objectForInfoDictionaryKey:key]; + return value ? [value intValue] : defaultValue; +} + +extern "C" int qtmn(int argc, char *argv[]) +{ + @autoreleasepool { + size_t defaultStackSize = 512 * kBytesPerKiloByte; // Same as secondary threads + + uint requestedStackSize = qMax(0, infoPlistValue(@"QtRunLoopIntegrationStackSize", defaultStackSize)); + + if (infoPlistValue(@"QtRunLoopIntegrationDisableSeparateStack", false)) + requestedStackSize = 0; + + char reservedStack[Stack::computeSize(requestedStackSize)]; + + if (sizeof(reservedStack) > 0) { + userMainStack.adopt(reservedStack, sizeof(reservedStack)); + + if (infoPlistValue(@"QtRunLoopIntegrationDebugStackUsage", false)) { + debugStackUsage = true; + userMainStack.scribble(); + qDebug("Effective stack size is %lu bytes", userMainStack.size()); + } + } + + qEventDispatcherDebug() << "Running UIApplicationMain"; qIndent(); + return UIApplicationMain(argc, argv, nil, NSStringFromClass([QIOSApplicationDelegate class])); + } +} + +enum SetJumpResult +{ + kJumpPointSetSuccessfully = 0, + kJumpedFromEventDispatcherProcessEvents, + kJumpedFromQApplicationExecInterrupt, + kJumpedFromUserMainTrampoline, +}; + +extern "C" int main(int argc, char *argv[]); + +static void __attribute__((noinline, noreturn)) user_main_trampoline() +{ + NSArray *arguments = [[NSProcessInfo processInfo] arguments]; + int argc = arguments.count; + char **argv = new char*[argc]; + for (int i = 0; i < argc; ++i) { + NSString *arg = [arguments objectAtIndex:i]; + argv[i] = reinterpret_cast(malloc([arg lengthOfBytesUsingEncoding:[NSString defaultCStringEncoding]])); + strcpy(argv[i], [arg cStringUsingEncoding:[NSString defaultCStringEncoding]]); + } + + int exitCode = main(argc, argv); + delete[] argv; + + qEventDispatcherDebug() << "Returned from main with exit code " << exitCode; + + if (Q_UNLIKELY(debugStackUsage)) + userMainStack.printUsage(); + + if (applicationAboutToTerminate) + longjmp(applicationWillTerminateJumpPoint, kJumpedFromUserMainTrampoline); + + // We end up here if the user's main() never calls QApplication::exec(), + // or if we return from exec() after quitting the application. If so we + // follow the expected behavior from the point of the user's main(), which + // is to exit with the given exit code. + exit(exitCode); +} + +// If we don't have a stack set up, we're not running inside +// iOS' native/root level run-loop in UIApplicationMain. +static bool rootLevelRunLoopIntegration() +{ + return userMainStack.isValid(); +} + +@interface QIOSApplicationStateTracker : NSObject +@end + +@implementation QIOSApplicationStateTracker + ++ (void) load +{ + [[NSNotificationCenter defaultCenter] + addObserver:self + selector:@selector(applicationDidFinishLaunching) + name:UIApplicationDidFinishLaunchingNotification + object:nil]; + + [[NSNotificationCenter defaultCenter] + addObserver:self + selector:@selector(applicationWillTerminate) + name:UIApplicationWillTerminateNotification + object:nil]; +} + +#if defined(Q_PROCESSOR_X86) +# define SET_STACK_POINTER "mov %0, %%esp" +# define FUNCTION_CALL_ALIGNMENT 16 +#elif defined(Q_PROCESSOR_ARM) +# define SET_STACK_POINTER "mov sp, %0" +# define FUNCTION_CALL_ALIGNMENT 4 +#else +# error "Unknown processor family" +#endif + ++ (void) applicationDidFinishLaunching +{ + if (!isQtApplication()) + return; + + if (!rootLevelRunLoopIntegration()) { + // We schedule the main-redirection for the next run-loop pass, so that we + // can return from this function and let UIApplicationMain finish its job. + // This results in running Qt's application eventloop as a nested runloop. + qEventDispatcherDebug() << "Scheduling main() on next run-loop pass"; + CFRunLoopTimerRef userMainTimer = CFRunLoopTimerCreateWithHandler(kCFAllocatorDefault, + CFAbsoluteTimeGetCurrent(), 0, 0, 0, ^(CFRunLoopTimerRef) { user_main_trampoline(); }); + CFRunLoopAddTimer(CFRunLoopGetMain(), userMainTimer, kCFRunLoopCommonModes); + CFRelease(userMainTimer); + return; + } + + switch (setjmp(processEventEnterJumpPoint)) { + case kJumpPointSetSuccessfully: + qEventDispatcherDebug() << "Running main() on separate stack"; qIndent(); + + // Redirect the stack pointer to the start of the reserved stack. This ensures + // that when we longjmp out of the event dispatcher and continue execution, the + // 'Qt main' call-stack will not be smashed, as it lives in a part of the stack + // that was allocated back in main(). + __asm__ __volatile__( + SET_STACK_POINTER + : /* no outputs */ + : "r" (qAlignDown(userMainStack.base, FUNCTION_CALL_ALIGNMENT)) + ); + + user_main_trampoline(); + + Q_UNREACHABLE(); + break; + case kJumpedFromEventDispatcherProcessEvents: + // We've returned from the longjmp in the event dispatcher, + // and the stack has been restored to its old self. + qUnIndent(); qEventDispatcherDebug() << "Returned from processEvents"; + + if (Q_UNLIKELY(debugStackUsage)) + userMainStack.printUsage(); + + break; + default: + qFatal("Unexpected jump result in event loop integration"); + } +} + ++ (void) applicationWillTerminate +{ + if (!isQtApplication()) + return; + + if (!rootLevelRunLoopIntegration()) + return; + + // Normally iOS just sends SIGKILL to quit applications, in which case there's + // no chance for us to clean up anything, but in some rare cases iOS will tell + // us that the application is about to be terminated. + + // We try to play nice with Qt by ending the main event loop, which will result + // in QCoreApplication::aboutToQuit() being emitted, and main() returning to the + // trampoline. The trampoline then redirects us back here, so that we can return + // to UIApplicationMain instead of calling exit(). + + applicationAboutToTerminate = true; + switch (setjmp(applicationWillTerminateJumpPoint)) { + case kJumpPointSetSuccessfully: + qEventDispatcherDebug() << "Exiting qApp with SIGTERM exit code"; qIndent(); + // We treat applicationWillTerminate as SIGTERM, even if it can't be ignored + qApp->exit(128 + SIGTERM); + + // The runloop will not exit when the application is about to terminate, + // so we'll never see the exit activity and have a chance to return from + // QApplication::exec(). We initiate the return manually as a workaround. + qEventDispatcherDebug() << "Manually triggering return from QApp exec"; + static_cast(qApp->eventDispatcher())->interruptQApplicationExec(); + break; + case kJumpedFromUserMainTrampoline: + // The user's main has returned, so we're ready to let iOS terminate the application + qUnIndent(); qEventDispatcherDebug() << "kJumpedFromUserMainTrampoline, allowing iOS to terminate"; + break; + default: + qFatal("Unexpected jump result in event loop integration"); + } +} + +@end + +QT_BEGIN_NAMESPACE +QT_USE_NAMESPACE + +QIOSEventDispatcher::QIOSEventDispatcher(QObject *parent) + : QEventDispatcherCoreFoundation(parent) + , m_processEventCallsAfterAppExec(0) + , m_runLoopExitObserver(this, &QIOSEventDispatcher::handleRunLoopExit, kCFRunLoopExit) +{ +} + +bool __attribute__((returns_twice)) QIOSEventDispatcher::processEvents(QEventLoop::ProcessEventsFlags flags) +{ + if (!rootLevelRunLoopIntegration()) + return QEventDispatcherCoreFoundation::processEvents(flags); + + QCoreApplicationPrivate *qApplication = static_cast(QObjectPrivate::get(qApp)); + if (!m_processEventCallsAfterAppExec && qApplication->in_exec) { + Q_ASSERT(flags & QEventLoop::EventLoopExec); + + // We know that app->in_exec is set just before executing the main event loop, + // so the first processEvents call after that will be the main event loop. + ++m_processEventCallsAfterAppExec; + + // We set a new jump point here that we can return to when the Qt application + // is asked to exit, so that we can return from QCoreApplication::exec(). + switch (setjmp(processEventExitJumpPoint)) { + case kJumpPointSetSuccessfully: + qEventDispatcherDebug() << "QApplication exec detected, jumping back to native runloop"; + longjmp(processEventEnterJumpPoint, kJumpedFromEventDispatcherProcessEvents); + break; + case kJumpedFromQApplicationExecInterrupt: + // QCoreApplication has quit (either by the hand of the user, or the iOS termination + // signal), and we jumped back though processEventExitJumpPoint. We return from processEvents, + // which will emit aboutToQuit and then return to the user's main, which can do + // whatever it wants, including calling exec() on the application again. + qEventDispatcherDebug() << "kJumpedFromQApplicationExecInterrupt, returning with eventsProcessed = true"; + return true; + default: + qFatal("Unexpected jump result in event loop integration"); + } + + Q_UNREACHABLE(); + } + + if (m_processEventCallsAfterAppExec) + ++m_processEventCallsAfterAppExec; + + bool processedEvents = QEventDispatcherCoreFoundation::processEvents(flags); + + if (m_processEventCallsAfterAppExec) + --m_processEventCallsAfterAppExec; + + // If we're running with nested event loops and the application is quit, + // then the forwarded interrupt call will happen while our processEvent + // counter is still 2, and we won't detect that we're about to fall down + // to the root iOS run-loop. We do an extra check here to catch that case. + checkIfApplicationShouldQuit(); + + return processedEvents; +} + +void QIOSEventDispatcher::interrupt() +{ + QEventDispatcherCoreFoundation::interrupt(); + + if (!rootLevelRunLoopIntegration()) + return; + + // If an interrupt happens as part of a non-nested event loop, that is, + // by processing an event or timer in the root iOS run-loop, we'll be + // able to detect it here. + checkIfApplicationShouldQuit(); +} + +void QIOSEventDispatcher::checkIfApplicationShouldQuit() +{ + if (QThreadData::current()->quitNow && m_processEventCallsAfterAppExec == 1) { + qEventDispatcherDebug() << "Hit root runloop level, watching for runloop exit"; + m_runLoopExitObserver.addToMode(kCFRunLoopCommonModes); + } +} + +void QIOSEventDispatcher::handleRunLoopExit(CFRunLoopActivity activity) +{ + Q_ASSERT(activity == kCFRunLoopExit); + + m_runLoopExitObserver.removeFromMode(kCFRunLoopCommonModes); + + interruptQApplicationExec(); +} + +void QIOSEventDispatcher::interruptQApplicationExec() +{ + Q_ASSERT(QThreadData::current()->quitNow); + Q_ASSERT(m_processEventCallsAfterAppExec == 1); + + --m_processEventCallsAfterAppExec; + + // We re-set applicationProcessEventsReturnPoint here so that future + // calls to QApplication::exec() will end up back here after entering + // processEvents, instead of back in didFinishLaunchingWithOptions. + switch (setjmp(processEventEnterJumpPoint)) { + case kJumpPointSetSuccessfully: + qEventDispatcherDebug() << "Jumping back to processEvents"; + longjmp(processEventExitJumpPoint, kJumpedFromQApplicationExecInterrupt); + break; + case kJumpedFromEventDispatcherProcessEvents: + // QCoreApplication was re-executed + qEventDispatcherDebug() << "kJumpedFromEventDispatcherProcessEvents"; + break; + default: + qFatal("Unexpected jump result in event loop integration"); + } +} + +QT_END_NAMESPACE diff --git a/src/plugins/platforms/ios/qiosintegration.mm b/src/plugins/platforms/ios/qiosintegration.mm index cbf9dba862..d854bf7723 100644 --- a/src/plugins/platforms/ios/qiosintegration.mm +++ b/src/plugins/platforms/ios/qiosintegration.mm @@ -40,6 +40,8 @@ ****************************************************************************/ #include "qiosintegration.h" +#include "qioseventdispatcher.h" +#include "qiosglobal.h" #include "qioswindow.h" #include "qiosbackingstore.h" #include "qiosscreen.h" @@ -47,7 +49,6 @@ #include "qiosinputcontext.h" #include "qiostheme.h" -#include #include #include @@ -114,7 +115,10 @@ QPlatformOpenGLContext *QIOSIntegration::createPlatformOpenGLContext(QOpenGLCont QAbstractEventDispatcher *QIOSIntegration::guiThreadEventDispatcher() const { - return new QEventDispatcherCoreFoundation; + if (isQtApplication()) + return new QIOSEventDispatcher; + else + return new QEventDispatcherCoreFoundation; } QPlatformFontDatabase * QIOSIntegration::fontDatabase() const