Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qioseventdispatcher.mm
Go to the documentation of this file.
1// Copyright (C) 2020 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3// Qt-Security score:significant reason:default
4
7#include "qiosglobal.h"
8
9#if defined(Q_OS_VISIONOS)
10#include "qiosswiftintegration.h"
11#endif
12
13#include <QtCore/qprocessordetection.h>
14#include <QtCore/private/qcoreapplication_p.h>
15#include <QtCore/private/qsystemerror_p.h>
16#include <QtCore/private/qthread_p.h>
17
18#include <qpa/qwindowsysteminterface.h>
19
20#import <Foundation/NSArray.h>
21#import <Foundation/NSString.h>
22#import <Foundation/NSProcessInfo.h>
23#import <Foundation/NSThread.h>
24#import <Foundation/NSNotification.h>
25
26#import <UIKit/UIApplication.h>
27
28#include <setjmp.h> // Here be dragons
29
30#include <sys/mman.h>
31
32#define qAlignDown(val, align) val & ~(align - 1)
33#define qAlignUp(val, align) qAlignDown(val + (align - 1), align)
34
35static const size_t kBytesPerKiloByte = 1024;
36static const long kPageSize = sysconf(_SC_PAGESIZE);
37
38using namespace QT_PREPEND_NAMESPACE(QtPrivate);
39
40/*
41 The following diagram shows the layout of the reserved
42 stack in relation to the regular stack, and the basic
43 flow of the initial startup sequence. Note how we end
44 up back in applicationDidLaunch after the user's main
45 recurses into qApp-exec(), which allows us to return
46 from applicationDidLaunch and spin the run-loop at the
47 same level (UIApplicationMain) as iOS nativly does.
48
49 +-----------------------------+
50 | qtmn() |
51 | +--------------------+ <-- base
52 | +----> main() | |
53 | | +--------------------+ |
54 | | | ... | |
55 | | +--------------------+ |
56 | | | qApp->exec() | |
57 | | +--------------------+ |
58 | | | processEvents() | |
59 | | | | |
60 | | +--+ longjmp(a) | |
61 | | | | | |
62 | | | +--------------------+ |
63 | | | | | |
64 | | | | | |
65 | | | | unused | |
66 | | | | | |
67 | | | | | |
68 | | | +--------------------+ <-- limit
69 | | | | memory guard | |
70 | | | +--------------------+ <-- reservedStack
71 +-|-|-------------------------+
72 | | | UIApplicationMain() |
73 +-|-|-------------------------+
74 | | | applicationDidLaunch() |
75 | | | |
76 | | +--> setjmp(a) |
77 | +----+ trampoline() |
78 | |
79 +-----------------------------+
80
81 Note: the diagram does not reflect alignment issues.
82*/
83
84namespace
85{
86 rlimit stackLimit = {0, 0};
87 rlim_t originalStackSize = 0;
88
89 struct Stack
90 {
91 uintptr_t base;
92 uintptr_t limit;
93
94 static size_t computeSize(size_t requestedSize)
95 {
96 if (!requestedSize)
97 return 0;
98
99 // The stack size must be a multiple of 4 KB
100 size_t stackSize = qAlignUp(requestedSize, 4 * kBytesPerKiloByte);
101
102 // Be at least 16 KB
103 stackSize = qMax(16 * kBytesPerKiloByte, stackSize);
104
105 // Have enough extra space for our (aligned) memory guard
106 stackSize += (2 * kPageSize);
107
108 // But not exceed the 1MB maximum (adjusted to account for current stack usage)
109 stackSize = qMin(stackSize, ((1024 - 64) * kBytesPerKiloByte));
110
111 // Which we verify, just in case
112 if (Q_UNLIKELY(stackSize > originalStackSize))
113 qFatal("Unexpectedly exceeded stack limit");
114
115 return stackSize;
116 }
117
118 void adopt(void* memory, size_t size)
119 {
120 uintptr_t memoryStart = uintptr_t(memory);
121
122 // Add memory guard at the end of the reserved stack, so that any stack
123 // overflow during the user's main will trigger an exception at that point,
124 // and not when we return and find that the current stack has been smashed.
125 // We allow read though, so that garbage-collection can pass through our
126 // stack in its mark phase without triggering access violations.
127 uintptr_t memoryGuardStart = qAlignUp(memoryStart, kPageSize);
128 if (mprotect((void*)memoryGuardStart, kPageSize, PROT_READ))
129 qWarning() << "Failed to add memory guard:" << strerror(errno);
130
131 // We don't consider the memory guard part of the usable stack space
132 limit = memoryGuardStart + kPageSize;
133
134 // The stack grows downwards in memory, so the stack base is actually
135 // at the end of the reserved stack space. And, as the memory guard
136 // was page aligned, we need to align down the base as well, to
137 // keep the requirement that the stack size is a multiple of 4K.
138 base = qAlignDown(memoryStart + size, kPageSize);
139 }
140
141 bool isValid()
142 {
143 return base && limit;
144 }
145
146 size_t size()
147 {
148 return base - limit;
149 }
150
151 static const int kScribblePattern;
152
153 void scribble()
154 {
155 memset_pattern4((void*)limit, &kScribblePattern, size());
156 }
157
158 void printUsage()
159 {
160 uintptr_t highWaterMark = limit;
161 for (; highWaterMark < base; highWaterMark += 4) {
162 if (memcmp((void*)highWaterMark, &kScribblePattern, 4))
163 break;
164 }
165
166 qDebug("main() used roughly %lu bytes of stack space", (base - highWaterMark));
167 }
168 };
169
170 const int Stack::kScribblePattern = 0xfafafafa;
171
172 Stack userMainStack;
173
174 jmp_buf processEventEnterJumpPoint;
175 jmp_buf processEventExitJumpPoint;
176
177 bool applicationAboutToTerminate = false;
178 jmp_buf applicationWillTerminateJumpPoint;
179
180 bool debugStackUsage = false;
181
182 struct {
183 QAppleLogActivity UIApplicationMain;
184 QAppleLogActivity applicationDidFinishLaunching;
185 } logActivity;
186
187 static bool s_isQtApplication = false;
188
189 void updateStackLimit()
190 {
191 qCDebug(lcEventDispatcher) << "Updating RLIMIT_STACK soft limit from"
192 << originalStackSize << "to" << userMainStack.size();
193
194 stackLimit.rlim_cur = userMainStack.size();
195 if (setrlimit(RLIMIT_STACK, &stackLimit) != 0) {
196 qCWarning(lcEventDispatcher) << "Failed to update RLIMIT_STACK soft limit"
197 << QSystemError::stdString();
198 }
199 }
200
201 void restoreStackLimit()
202 {
203 qCDebug(lcEventDispatcher) << "Restoring RLIMIT_STACK soft limit from"
204 << stackLimit.rlim_cur << "back to" << originalStackSize;
205
206 stackLimit.rlim_cur = originalStackSize;
207 if (setrlimit(RLIMIT_STACK, &stackLimit) != 0) {
208 qCWarning(lcEventDispatcher) << "Failed to update RLIMIT_STACK soft limit"
209 << QSystemError::stdString();
210 }
211 }
212}
213
214extern "C" int qt_main_wrapper(int argc, char *argv[])
215{
216 s_isQtApplication = true;
217
218 @autoreleasepool {
219 if (Q_UNLIKELY(getrlimit(RLIMIT_STACK, &stackLimit) != 0))
220 qFatal("Failed to get stack limits");
221
222 originalStackSize = stackLimit.rlim_cur;
223
224 size_t defaultStackSize = 512 * kBytesPerKiloByte; // Same as secondary threads
225
226 uint requestedStackSize = qMax(0, infoPlistValue(@"QtRunLoopIntegrationStackSize", defaultStackSize));
227
228 if (infoPlistValue(@"QtRunLoopIntegrationDisableSeparateStack", false))
229 requestedStackSize = 0;
230
231 QT_WARNING_PUSH
232#if Q_CC_CLANG >= 1800
233 QT_WARNING_DISABLE_CLANG("-Wvla-cxx-extension")
234#endif
235 // The user-main stack _must_ live on the stack, so that the stack pointer
236 // during user-main is within pthread_get_stackaddr_np/pthread_get_stacksize_np.
237 char reservedStack[Stack::computeSize(requestedStackSize)];
238 QT_WARNING_POP
239
240 if (sizeof(reservedStack) > 0) {
241 userMainStack.adopt(reservedStack, sizeof(reservedStack));
242
243 if (infoPlistValue(@"QtRunLoopIntegrationDebugStackUsage", false)) {
244 debugStackUsage = true;
245 userMainStack.scribble();
246 qDebug("Effective stack size is %lu bytes", userMainStack.size());
247 }
248 }
249
250 logActivity.UIApplicationMain = QT_APPLE_LOG_ACTIVITY(
251 lcEventDispatcher().isDebugEnabled(), "UIApplicationMain").enter();
252
253#if defined(Q_OS_VISIONOS)
254 Q_UNUSED(argc);
255 Q_UNUSED(argv);
256 qCDebug(lcEventDispatcher) << "Starting Swift app";
257 QIOSIntegrationPluginSwift::runSwiftAppMain();
258 Q_UNREACHABLE();
259#else
260 qCDebug(lcEventDispatcher) << "Running UIApplicationMain";
261 return UIApplicationMain(argc, argv, nil, NSStringFromClass([QIOSApplicationDelegate class]));
262#endif
263 }
264}
265
273
274static void __attribute__((noinline, noreturn)) user_main_trampoline()
275{
276 NSArray<NSString *> *arguments = [[NSProcessInfo processInfo] arguments];
277 int argc = arguments.count;
278 char **argv = new char*[argc];
279
280 for (int i = 0; i < argc; ++i) {
281 NSString *arg = [arguments objectAtIndex:i];
282
283 NSStringEncoding cStringEncoding = [NSString defaultCStringEncoding];
284 unsigned int bufferSize = [arg lengthOfBytesUsingEncoding:cStringEncoding] + 1;
285 argv[i] = reinterpret_cast<char *>(malloc(bufferSize));
286
287 if (Q_UNLIKELY(![arg getCString:argv[i] maxLength:bufferSize encoding:cStringEncoding]))
288 qFatal("Could not convert argv[%d] to C string", i);
289 }
290
291 updateStackLimit();
292
293 int main(int argc, char *argv[]);
294
295 int exitCode = main(argc, argv);
296 delete[] argv;
297
298 logActivity.applicationDidFinishLaunching.enter();
299 qCDebug(lcEventDispatcher) << "Returned from main with exit code " << exitCode;
300
301#if TARGET_OS_SIMULATOR
302 if (qEnvironmentVariableIntegerValue("QT_RUNNING_VIA_TEST_RUNNER")) {
303 // When running on simulator via `simctl launch` the exit code
304 // is not propagated, so we manually save it to disk instead.
305 QFile exitCodeFile(QDir::tempPath() + "/qt_exit_code.txt");
306 if (exitCodeFile.open(QIODevice::WriteOnly | QIODevice::Text))
307 exitCodeFile.write(QByteArray::number(exitCode));
308 else
309 qCWarning(lcEventDispatcher) << "Failed to write exit code to" << exitCodeFile.fileName();
310 }
311#endif
312
313 if (Q_UNLIKELY(debugStackUsage))
314 userMainStack.printUsage();
315
316 restoreStackLimit();
317
318 logActivity.applicationDidFinishLaunching.leave();
319
320 if (applicationAboutToTerminate)
321 longjmp(applicationWillTerminateJumpPoint, kJumpedFromUserMainTrampoline);
322
323 // We end up here if the user's main() never calls QApplication::exec(),
324 // or if we return from exec() after quitting the application. If so we
325 // follow the expected behavior from the point of the user's main(), which
326 // is to exit with the given exit code.
327 exit(exitCode);
328}
329
330// If we don't have a stack set up, we're not running inside
331// iOS' native/root level run-loop in UIApplicationMain.
332static bool rootLevelRunLoopIntegration()
333{
334 return userMainStack.isValid();
335}
336
337@interface QIOSApplicationStateTracker : NSObject
338@end
339
340@implementation QIOSApplicationStateTracker
341
342+ (void)load
343{
344 [[NSNotificationCenter defaultCenter]
345 addObserver:self
346 selector:@selector(applicationDidFinishLaunching:)
347 name:UIApplicationDidFinishLaunchingNotification
348 object:nil];
349
350 [[NSNotificationCenter defaultCenter]
351 addObserver:self
352 selector:@selector(applicationWillTerminate)
353 name:UIApplicationWillTerminateNotification
354 object:nil];
355}
356
357#if defined(Q_PROCESSOR_X86)
358# define FUNCTION_CALL_ALIGNMENT 16
359# if defined(Q_PROCESSOR_X86_32)
360# define SET_STACK_POINTER "mov %0, %%esp"
361# elif defined(Q_PROCESSOR_X86_64)
362# define SET_STACK_POINTER "movq %0, %%rsp"
363# endif
364#elif defined(Q_PROCESSOR_ARM)
365# // Valid for both 32 and 64-bit ARM
366# define FUNCTION_CALL_ALIGNMENT 4
367# define SET_STACK_POINTER "mov sp, %0"
368#else
369# error "Unknown processor family"
370#endif
371
372+ (void)applicationDidFinishLaunching:(NSNotification *)notification
373{
374 logActivity.applicationDidFinishLaunching = QT_APPLE_LOG_ACTIVITY_WITH_PARENT(
375 lcEventDispatcher().isDebugEnabled(), "applicationDidFinishLaunching", logActivity.UIApplicationMain).enter();
376
377 qCDebug(lcEventDispatcher) << "Application launched with options" << notification.userInfo;
378
379 if (!isQtApplication())
380 return;
381
382 if (!rootLevelRunLoopIntegration()) {
383 // We schedule the main-redirection for the next run-loop pass, so that we
384 // can return from this function and let UIApplicationMain finish its job.
385 // This results in running Qt's application eventloop as a nested runloop.
386 qCDebug(lcEventDispatcher) << "Scheduling main() on next run-loop pass";
387 CFRunLoopTimerRef userMainTimer = CFRunLoopTimerCreateWithHandler(kCFAllocatorDefault,
388 CFAbsoluteTimeGetCurrent(), 0, 0, 0, ^(CFRunLoopTimerRef) { user_main_trampoline(); });
389 CFRunLoopAddTimer(CFRunLoopGetMain(), userMainTimer, kCFRunLoopCommonModes);
390 CFRelease(userMainTimer);
391 return;
392 }
393
394 switch (setjmp(processEventEnterJumpPoint)) {
395 case kJumpPointSetSuccessfully: {
396 qCDebug(lcEventDispatcher) << "Running main() on separate stack";
397 QT_APPLE_SCOPED_LOG_ACTIVITY(lcEventDispatcher().isDebugEnabled(), "main()");
398
399 // Redirect the stack pointer to the start of the reserved stack. This ensures
400 // that when we longjmp out of the event dispatcher and continue execution, the
401 // 'Qt main' call-stack will not be smashed, as it lives in a part of the stack
402 // that was allocated back in main().
403 __asm__ __volatile__(
404 SET_STACK_POINTER
405 : /* no outputs */
406 : "r" (qAlignDown(userMainStack.base, FUNCTION_CALL_ALIGNMENT))
407 );
408
409 user_main_trampoline();
410
411 Q_UNREACHABLE();
412 break;
413 }
414 case kJumpedFromEventDispatcherProcessEvents:
415 // We've returned from the longjmp in the event dispatcher,
416 // and the stack has been restored to its old self.
417 logActivity.UIApplicationMain.enter();
418 qCDebug(lcEventDispatcher) << "↳ Jumped from processEvents due to exec";
419
420 if (Q_UNLIKELY(debugStackUsage))
421 userMainStack.printUsage();
422
423 restoreStackLimit();
424
425 break;
426 default:
427 qFatal("Unexpected jump result in event loop integration");
428 }
429}
430
431// We treat applicationWillTerminate as SIGTERM, even if it can't be ignored,
432// and follow the bash convention of encoding the signal number in the upper
433// four bits of the exit code (exit(3) will only pass on the lower 8 bits).
434static const char kApplicationWillTerminateExitCode = char(SIGTERM | 0x80);
435
436+ (void)applicationWillTerminate
437{
438 QAppleLogActivity applicationWillTerminateActivity = QT_APPLE_LOG_ACTIVITY_WITH_PARENT(
439 lcEventDispatcher().isDebugEnabled(), "applicationWillTerminate", logActivity.UIApplicationMain).enter();
440 qCDebug(lcEventDispatcher) << "Application about to be terminated by iOS";
441
442 if (!isQtApplication())
443 return;
444
445 if (!rootLevelRunLoopIntegration())
446 return;
447
448 // Normally iOS just sends SIGKILL to quit applications, in which case there's
449 // no chance for us to clean up anything, but in some rare cases iOS will tell
450 // us that the application is about to be terminated.
451
452 // We try to play nice with Qt by ending the main event loop, which will result
453 // in QCoreApplication::aboutToQuit() being emitted, and main() returning to the
454 // trampoline. The trampoline then redirects us back here, so that we can return
455 // to UIApplicationMain instead of calling exit().
456
457 applicationAboutToTerminate = true;
458 switch (setjmp(applicationWillTerminateJumpPoint)) {
459 case kJumpPointSetSuccessfully:
460 qCDebug(lcEventDispatcher) << "Exiting qApp with SIGTERM exit code";
461 qApp->exit(kApplicationWillTerminateExitCode);
462
463 // The runloop will not exit when the application is about to terminate,
464 // so we'll never see the exit activity and have a chance to return from
465 // QEventLoop::exec(). We initiate the return manually as a workaround.
466 qCDebug(lcEventDispatcher) << "Manually triggering return from event loop exec";
467 applicationWillTerminateActivity.leave();
468 static_cast<QIOSJumpingEventDispatcher *>(qApp->eventDispatcher())->interruptEventLoopExec();
469 break;
470 case kJumpedFromUserMainTrampoline:
471 applicationWillTerminateActivity.enter();
472 // The user's main has returned, so we're ready to let iOS terminate the application
473 qCDebug(lcEventDispatcher) << "kJumpedFromUserMainTrampoline, allowing iOS to terminate";
474 applicationWillTerminateActivity.leave();
475 break;
476 default:
477 qFatal("Unexpected jump result in event loop integration");
478 }
479}
480
481@end
482
483QT_BEGIN_NAMESPACE
484QT_USE_NAMESPACE
485
486QIOSEventDispatcher *QIOSEventDispatcher::create()
487{
488 if (isQtApplication() && rootLevelRunLoopIntegration())
489 return new QIOSJumpingEventDispatcher;
490
491 return new QIOSEventDispatcher;
492}
493
494QIOSEventDispatcher::QIOSEventDispatcher(QObject *parent)
495 : QEventDispatcherCoreFoundation(parent)
496{
497 // We want all delivery of events from the system to be handled synchronously
498 QWindowSystemInterface::setSynchronousWindowSystemEvents(true);
499}
500
501bool QIOSEventDispatcher::isQtApplication()
502{
503 return s_isQtApplication;
504}
505
506/*!
507 Override of the CoreFoundation posted events runloop source callback
508 so that we can send window system (QPA) events in addition to sending
509 normal Qt events.
510*/
511bool QIOSEventDispatcher::processPostedEvents()
512{
513 // Don't send window system events if the base CF dispatcher has determined
514 // that events should not be sent for this pass of the runloop source.
515 if (!QEventDispatcherCoreFoundation::processPostedEvents())
516 return false;
517
518 QT_APPLE_SCOPED_LOG_ACTIVITY(lcEventDispatcher().isDebugEnabled(), "sendWindowSystemEvents");
519 QEventLoop::ProcessEventsFlags flags
520 = QEventLoop::ProcessEventsFlags(m_processEvents.flags.loadRelaxed());
521 qCDebug(lcEventDispatcher) << "Sending window system events for" << flags;
522 QWindowSystemInterface::sendWindowSystemEvents(flags);
523
524 return true;
525}
526
527QIOSJumpingEventDispatcher::QIOSJumpingEventDispatcher(QObject *parent)
528 : QIOSEventDispatcher(parent)
529 , m_processEventLevel(0)
530 , m_runLoopExitObserver(this, &QIOSJumpingEventDispatcher::handleRunLoopExit, kCFRunLoopExit)
531{
532}
533
534bool __attribute__((returns_twice)) QIOSJumpingEventDispatcher::processEvents(QEventLoop::ProcessEventsFlags flags)
535{
536 if (applicationAboutToTerminate) {
537 qCDebug(lcEventDispatcher) << "Detected QEventLoop exec after application termination";
538 // Re-issue exit, and return immediately
539 qApp->exit(kApplicationWillTerminateExitCode);
540 return false;
541 }
542
543 if (!m_processEventLevel && (flags & QEventLoop::EventLoopExec)) {
544 QT_APPLE_SCOPED_LOG_ACTIVITY(lcEventDispatcher().isDebugEnabled(), "processEvents");
545 qCDebug(lcEventDispatcher) << "Processing events with flags" << flags;
546
547 ++m_processEventLevel;
548
549 m_runLoopExitObserver.addToMode(kCFRunLoopCommonModes);
550
551 // We set a new jump point here that we can return to when the event loop
552 // is asked to exit, so that we can return from QEventLoop::exec().
553 switch (setjmp(processEventExitJumpPoint)) {
554 case kJumpPointSetSuccessfully:
555 qCDebug(lcEventDispatcher) << "QEventLoop exec detected, jumping back to system runloop ↵";
556 longjmp(processEventEnterJumpPoint, kJumpedFromEventDispatcherProcessEvents);
557 break;
558 case kJumpedFromEventLoopExecInterrupt:
559 // The event loop has exited (either by the hand of the user, or the iOS termination
560 // signal), and we jumped back though processEventExitJumpPoint. We return from processEvents,
561 // which will emit aboutToQuit if it's QApplication's event loop, and then return to the user's
562 // main, which can do whatever it wants, including calling exec() on the application again.
563 qCDebug(lcEventDispatcher) << "⇢ System runloop exited, returning with eventsProcessed = true";
564 updateStackLimit();
565 return true;
566 default:
567 qFatal("Unexpected jump result in event loop integration");
568 }
569
570 Q_UNREACHABLE();
571 }
572
573 ++m_processEventLevel;
574 bool processedEvents = QEventDispatcherCoreFoundation::processEvents(flags);
575 --m_processEventLevel;
576
577 return processedEvents;
578}
579
580void QIOSJumpingEventDispatcher::handleRunLoopExit(CFRunLoopActivity activity)
581{
582 Q_UNUSED(activity);
583 Q_ASSERT(activity == kCFRunLoopExit);
584
585 if (m_processEventLevel == 1 && !currentEventLoop()->isRunning())
586 interruptEventLoopExec();
587}
588
589void QIOSJumpingEventDispatcher::interruptEventLoopExec()
590{
591 Q_ASSERT(m_processEventLevel == 1);
592
593 --m_processEventLevel;
594
595 m_runLoopExitObserver.removeFromMode(kCFRunLoopCommonModes);
596
597 // We re-set applicationProcessEventsReturnPoint here so that future
598 // calls to QEventLoop::exec() will end up back here after entering
599 // processEvents, instead of back in didFinishLaunchingWithOptions.
600 switch (setjmp(processEventEnterJumpPoint)) {
601 case kJumpPointSetSuccessfully:
602 qCDebug(lcEventDispatcher) << "Jumping into processEvents due to system runloop exit ⇢";
603 logActivity.UIApplicationMain.leave();
604 longjmp(processEventExitJumpPoint, kJumpedFromEventLoopExecInterrupt);
605 break;
606 case kJumpedFromEventDispatcherProcessEvents:
607 // QEventLoop was re-executed
608 logActivity.UIApplicationMain.enter();
609 qCDebug(lcEventDispatcher) << "↳ Jumped from processEvents due to re-exec";
610 restoreStackLimit();
611 break;
612 default:
613 qFatal("Unexpected jump result in event loop integration");
614 }
615}
616
617QT_END_NAMESPACE
static const size_t kBytesPerKiloByte
#define qAlignUp(val, align)
#define qAlignDown(val, align)
int qt_main_wrapper(int argc, char *argv[])
static const long kPageSize
static void __attribute__((noinline, noreturn)) user_main_trampoline()
@ kJumpedFromUserMainTrampoline
@ kJumpedFromEventLoopExecInterrupt
@ kJumpedFromEventDispatcherProcessEvents
@ kJumpPointSetSuccessfully
int main(int argc, char *argv[])
[ctor_close]