diff options
Diffstat (limited to 'src')
21 files changed, 905 insertions, 61 deletions
diff --git a/src/corelib/CMakeLists.txt b/src/corelib/CMakeLists.txt index 337703bda3a..1147205b79f 100644 --- a/src/corelib/CMakeLists.txt +++ b/src/corelib/CMakeLists.txt @@ -577,10 +577,15 @@ if(QT_FEATURE_async_io) io/qrandomaccessasyncfile.cpp io/qrandomaccessasyncfile_p.h io/qrandomaccessasyncfile_p_p.h ) - # TODO: This should become the last (fallback) condition later. - # We migth also want to rewrite it so that it does not depend on - # QT_FEATURE_future. - if(QT_FEATURE_thread AND QT_FEATURE_future) + if(APPLE) + qt_internal_extend_target(Core + SOURCES + io/qrandomaccessasyncfile_darwin.mm + ) + elseif(QT_FEATURE_thread AND QT_FEATURE_future) + # TODO: This should become the last (fallback) condition later. + # We migth also want to rewrite it so that it does not depend on + # QT_FEATURE_future. qt_internal_extend_target(Core SOURCES io/qrandomaccessasyncfile_threadpool.cpp diff --git a/src/corelib/configure.cmake b/src/corelib/configure.cmake index 535e3742cd2..d951b85c147 100644 --- a/src/corelib/configure.cmake +++ b/src/corelib/configure.cmake @@ -1233,7 +1233,7 @@ qt_feature("openssl-hash" PRIVATE qt_feature("async-io" PRIVATE LABEL "Async File I/O" PURPOSE "Provides support for asynchronous file I/O." - CONDITION QT_FEATURE_thread AND QT_FEATURE_future + CONDITION (QT_FEATURE_thread AND QT_FEATURE_future) OR APPLE ) qt_configure_add_summary_section(NAME "Qt Core") diff --git a/src/corelib/io/qrandomaccessasyncfile_darwin.mm b/src/corelib/io/qrandomaccessasyncfile_darwin.mm new file mode 100644 index 00000000000..2d7d3b196b2 --- /dev/null +++ b/src/corelib/io/qrandomaccessasyncfile_darwin.mm @@ -0,0 +1,728 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +// Qt-Security score:significant reason:default + +#include "qrandomaccessasyncfile_p_p.h" + +#include "qiooperation_p.h" +#include "qiooperation_p_p.h" +#include "qplatformdefs.h" + +#include <QtCore/qdir.h> +#include <QtCore/qfile.h> +#include <QtCore/private/qfilesystemengine_p.h> + +QT_BEGIN_NAMESPACE + +namespace { + +static bool isBarrierOperation(QIOOperation::Type type) +{ + return type == QIOOperation::Type::Flush || type == QIOOperation::Type::Open; +} + +} // anonymous namespace + +// Fine to provide the definition here, because all the usages are in this file +// only! +template <typename Operation, typename ...Args> +Operation * +QRandomAccessAsyncFilePrivate::addOperation(QIOOperation::Type type, qint64 offset, Args &&...args) +{ + auto dataStorage = new QtPrivate::QIOOperationDataStorage(std::forward<Args>(args)...); + auto *priv = new QIOOperationPrivate(dataStorage); + priv->offset = offset; + priv->type = type; + + Operation *op = new Operation(*priv, q_ptr); + auto opId = getNextId(); + m_operations.push_back(OperationInfo(opId, op)); + startOperationsUntilBarrier(); + + return op; +} + +QRandomAccessAsyncFilePrivate::QRandomAccessAsyncFilePrivate() + : QObjectPrivate() +{ +} + +QRandomAccessAsyncFilePrivate::~QRandomAccessAsyncFilePrivate() + = default; + +void QRandomAccessAsyncFilePrivate::init() +{ +} + +void QRandomAccessAsyncFilePrivate::cancelAndWait(QIOOperation *op) +{ + auto it = std::find_if(m_operations.cbegin(), m_operations.cend(), + [op](const auto &opInfo) { + return opInfo.operation.get() == op; + }); + // not found + if (it == m_operations.cend()) + return; + + const auto opInfo = m_operations.takeAt(std::distance(m_operations.cbegin(), it)); + + if (opInfo.state == OpState::Running) { + // cancel this operation + m_mutex.lock(); + if (m_runningOps.contains(opInfo.opId)) { + m_opToCancel = opInfo.opId; + closeIoChannel(opInfo.channel); + m_cancellationCondition.wait(&m_mutex); + m_opToCancel = kInvalidOperationId; // reset + } + m_mutex.unlock(); + } // otherwise it was not started yet + + // clean up the operation + releaseIoChannel(opInfo.channel); + auto *priv = QIOOperationPrivate::get(opInfo.operation); + priv->setError(QIOOperation::Error::Aborted); + + // we could cancel a barrier operation, so try to execute next operations + startOperationsUntilBarrier(); +} + +void QRandomAccessAsyncFilePrivate::close() +{ + if (m_fileState == FileState::Closed) + return; + + // cancel all operations + m_mutex.lock(); + m_opToCancel = kAllOperationIds; + for (const auto &op : m_operations) + closeIoChannel(op.channel); + closeIoChannel(m_ioChannel); + // we're not interested in any results anymore + if (!m_runningOps.isEmpty() || m_ioChannel) + m_cancellationCondition.wait(&m_mutex); + m_opToCancel = kInvalidOperationId; // reset + m_mutex.unlock(); + + // clean up all operations + for (auto &opInfo : m_operations) { + releaseIoChannel(opInfo.channel); + auto *priv = QIOOperationPrivate::get(opInfo.operation); + priv->setError(QIOOperation::Error::Aborted); + } + m_operations.clear(); + + releaseIoChannel(m_ioChannel); + + if (m_fd >= 0) { + ::close(m_fd); + m_fd = -1; + } + + m_fileState = FileState::Closed; +} + +qint64 QRandomAccessAsyncFilePrivate::size() const +{ + if (m_fileState != FileState::Opened) + return -1; + + QFileSystemMetaData metaData; + if (QFileSystemEngine::fillMetaData(m_fd, metaData)) + return metaData.size(); + + return -1; +} + +QIOOperation * +QRandomAccessAsyncFilePrivate::open(const QString &path, QIODeviceBase::OpenMode mode) +{ + if (m_fileState == FileState::Closed) { + m_filePath = path; + m_openMode = mode; + // Open is a barrier, so we won't have two open() operations running + // in parallel + m_fileState = FileState::OpenPending; + } + + return addOperation<QIOOperation>(QIOOperation::Type::Open, 0); +} + +QIOOperation *QRandomAccessAsyncFilePrivate::flush() +{ + return addOperation<QIOOperation>(QIOOperation::Type::Flush, 0); +} + +QIOReadOperation *QRandomAccessAsyncFilePrivate::read(qint64 offset, qint64 maxSize) +{ + QByteArray array(maxSize, Qt::Uninitialized); + return addOperation<QIOReadOperation>(QIOOperation::Type::Read, offset, std::move(array)); +} + +QIOWriteOperation *QRandomAccessAsyncFilePrivate::write(qint64 offset, const QByteArray &data) +{ + QByteArray copy = data; + return write(offset, std::move(copy)); +} + +QIOWriteOperation *QRandomAccessAsyncFilePrivate::write(qint64 offset, QByteArray &&data) +{ + return addOperation<QIOWriteOperation>(QIOOperation::Type::Write, offset, std::move(data)); +} + +QIOVectoredReadOperation * +QRandomAccessAsyncFilePrivate::readInto(qint64 offset, QSpan<std::byte> buffer) +{ + return addOperation<QIOVectoredReadOperation>(QIOOperation::Type::Read, offset, + QSpan<const QSpan<std::byte>>{buffer}); +} + +QIOVectoredWriteOperation * +QRandomAccessAsyncFilePrivate::writeFrom(qint64 offset, QSpan<const std::byte> buffer) +{ + return addOperation<QIOVectoredWriteOperation>(QIOOperation::Type::Write, offset, + QSpan<const QSpan<const std::byte>>{buffer}); +} + +QIOVectoredReadOperation * +QRandomAccessAsyncFilePrivate::readInto(qint64 offset, QSpan<const QSpan<std::byte>> buffers) +{ + // GCD implementation does not have vectored read. Spawning several read + // operations (each with an updated offset), is not ideal, because some + // of them could fail, and it wouldn't be clear what would be the return + // value in such case. + // So, we'll just execute several reads one-after-another, and complete the + // whole operation only when they all finish (or when an operation fails + // at some point). + + return addOperation<QIOVectoredReadOperation>(QIOOperation::Type::Read, offset, buffers); +} + +QIOVectoredWriteOperation * +QRandomAccessAsyncFilePrivate::writeFrom(qint64 offset, QSpan<const QSpan<const std::byte>> buffers) +{ + return addOperation<QIOVectoredWriteOperation>(QIOOperation::Type::Write, offset, buffers); +} + +dispatch_io_t QRandomAccessAsyncFilePrivate::createMainChannel(int fd) +{ + auto sharedThis = this; + return dispatch_io_create(DISPATCH_IO_RANDOM, fd, + dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0), + ^(int /*error*/) { + // Notify that the file descriptor can be closed + QMutexLocker locker(&sharedThis->m_mutex); + sharedThis->m_cancellationCondition.wakeOne(); + }); +} + +dispatch_io_t QRandomAccessAsyncFilePrivate::duplicateIoChannel(OperationId opId) +{ + if (!m_ioChannel) + return nullptr; + // We need to create a new channel for each operation, because the only way + // to cancel an operation is to call dispatch_io_close() with + // DISPATCH_IO_STOP flag. + // We do not care about the callback in this case, because we have the + // callback from the "main" io channel to do all the proper cleanup + auto channel = + dispatch_io_create_with_io(DISPATCH_IO_RANDOM, m_ioChannel, + dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0), + ^(int){ /* empty callback */ }); + + if (channel) { + QMutexLocker locker(&m_mutex); + m_runningOps.insert(opId); + } + return channel; +} + +void QRandomAccessAsyncFilePrivate::closeIoChannel(dispatch_io_t channel) +{ + if (channel) + dispatch_io_close(channel, DISPATCH_IO_STOP); +} + +void QRandomAccessAsyncFilePrivate::releaseIoChannel(dispatch_io_t channel) +{ + if (channel) { + dispatch_release(channel); + channel = nullptr; + } +} + +void QRandomAccessAsyncFilePrivate::handleOperationComplete(const OperationResult &opResult) +{ + // try to start next operations on return + auto onReturn = qScopeGuard([this] { + startOperationsUntilBarrier(); + }); + + auto it = std::find_if(m_operations.cbegin(), m_operations.cend(), + [opId = opResult.opId](const auto &opInfo) { + return opInfo.opId == opId; + }); + if (it == m_operations.cend()) + return; + qsizetype idx = std::distance(m_operations.cbegin(), it); + + const OperationInfo info = m_operations.takeAt(idx); + closeIoChannel(info.channel); + releaseIoChannel(info.channel); + + if (!info.operation) + return; + + auto convertError = [](int error, QIOOperation::Type type) { + if (error == 0) { + return QIOOperation::Error::None; + } else if (error == ECANCELED) { + return QIOOperation::Error::Aborted; + } else if (error == EBADF) { + return QIOOperation::Error::FileNotOpen; + } else if (error == EINVAL) { + switch (type) { + case QIOOperation::Type::Read: + case QIOOperation::Type::Write: + return QIOOperation::Error::IncorrectOffset; + case QIOOperation::Type::Flush: + return QIOOperation::Error::Flush; + case QIOOperation::Type::Open: + return QIOOperation::Error::Open; + case QIOOperation::Type::Unknown: + Q_UNREACHABLE_RETURN(QIOOperation::Error::FileNotOpen); + } + } else { + switch (type) { + case QIOOperation::Type::Read: + return QIOOperation::Error::Read; + case QIOOperation::Type::Write: + return QIOOperation::Error::Write; + case QIOOperation::Type::Flush: + return QIOOperation::Error::Flush; + case QIOOperation::Type::Open: + return QIOOperation::Error::Open; + case QIOOperation::Type::Unknown: + Q_UNREACHABLE_RETURN(QIOOperation::Error::FileNotOpen); + } + } + }; + + auto *priv = QIOOperationPrivate::get(info.operation); + switch (priv->type) { + case QIOOperation::Type::Read: + case QIOOperation::Type::Write: + priv->appendBytesProcessed(opResult.result); + // make sure that read buffers are truncated to the actual amount of + // bytes read + if (priv->type == QIOOperation::Type::Read) { + auto dataStorage = priv->dataStorage.get(); + auto processed = priv->processed; + if (dataStorage->containsByteArray()) { + QByteArray &array = dataStorage->getByteArray(); + array.truncate(processed); + } else if (dataStorage->containsReadSpans()) { + qint64 left = processed; + auto &readBuffers = dataStorage->getReadSpans(); + for (auto &s : readBuffers) { + const qint64 spanSize = qint64(s.size_bytes()); + const qint64 newSize = (std::min)(left, spanSize); + if (newSize < spanSize) + s.chop(spanSize - newSize); + left -= newSize; + } + } + } + priv->operationComplete(convertError(opResult.error, priv->type)); + break; + case QIOOperation::Type::Flush: { + const QIOOperation::Error error = convertError(opResult.error, priv->type); + priv->operationComplete(error); + break; + } + case QIOOperation::Type::Open: { + const QIOOperation::Error error = convertError(opResult.error, priv->type); + if (opResult.result >= 0 && error == QIOOperation::Error::None) { + m_fd = (int)opResult.result; + m_ioChannel = createMainChannel(m_fd); + m_fileState = FileState::Opened; + } else { + m_fileState = FileState::Closed; + } + priv->operationComplete(error); + break; + } + case QIOOperation::Type::Unknown: + Q_UNREACHABLE(); + break; + } +} + +void QRandomAccessAsyncFilePrivate::queueCompletion(OperationId opId, int error) +{ + const OperationResult res = { opId, 0LL, error }; + QMetaObject::invokeMethod(q_ptr, [this, res] { + handleOperationComplete(res); + }, Qt::QueuedConnection); +} + +void QRandomAccessAsyncFilePrivate::startOperationsUntilBarrier() +{ + // starts all operations until barrier, or a barrier operation if it's the + // first one + bool first = true; + for (auto &opInfo : m_operations) { + const bool isBarrier = isBarrierOperation(opInfo.operation->type()); + const bool shouldExecute = (opInfo.state == OpState::Pending) && (!isBarrier || first); + first = false; + if (shouldExecute) { + opInfo.state = OpState::Running; + switch (opInfo.operation->type()) { + case QIOOperation::Type::Read: + executeRead(opInfo); + break; + case QIOOperation::Type::Write: + executeWrite(opInfo); + break; + case QIOOperation::Type::Flush: + executeFlush(opInfo); + break; + case QIOOperation::Type::Open: + executeOpen(opInfo); + break; + case QIOOperation::Type::Unknown: + Q_UNREACHABLE(); + break; + } + } + if (isBarrier) + break; + } +} + +void QRandomAccessAsyncFilePrivate::executeRead(OperationInfo &opInfo) +{ + opInfo.channel = duplicateIoChannel(opInfo.opId); + if (!opInfo.channel) { + queueCompletion(opInfo.opId, EBADF); + return; + } + auto priv = QIOOperationPrivate::get(opInfo.operation); + auto dataStorage = priv->dataStorage.get(); + if (dataStorage->containsByteArray()) { + auto &array = dataStorage->getByteArray(); + char *bytesPtr = array.data(); + qint64 maxSize = array.size(); + readOneBufferHelper(opInfo.opId, opInfo.channel, priv->offset, + bytesPtr, maxSize, + 0, 1, 0); + } else { + Q_ASSERT(dataStorage->containsReadSpans()); + auto &readBuffers = dataStorage->getReadSpans(); + const auto totalBuffers = readBuffers.size(); + if (totalBuffers == 0) { + queueCompletion(opInfo.opId, 0); + return; + } + auto buf = readBuffers[0]; + readOneBufferHelper(opInfo.opId, opInfo.channel, priv->offset, + buf.data(), buf.size(), + 0, totalBuffers, 0); + } +} + +void QRandomAccessAsyncFilePrivate::executeWrite(OperationInfo &opInfo) +{ + opInfo.channel = duplicateIoChannel(opInfo.opId); + if (!opInfo.channel) { + queueCompletion(opInfo.opId, EBADF); + return; + } + auto priv = QIOOperationPrivate::get(opInfo.operation); + auto dataStorage = priv->dataStorage.get(); + if (dataStorage->containsByteArray()) { + const auto &array = dataStorage->getByteArray(); + const char *dataPtr = array.constData(); + const qint64 dataSize = array.size(); + + dispatch_queue_t queue = dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0); + // We handle the bytes on our own, so we need to specify an empty block as + // a destructor. + // dataToWrite is retained, so should be properly cleaned up. We always do + // it in the callback. + dispatch_data_t dataToWrite = dispatch_data_create(dataPtr, dataSize, queue, ^{}); + + writeHelper(opInfo.opId, opInfo.channel, priv->offset, dataToWrite, dataSize); + } else { + Q_ASSERT(dataStorage->containsWriteSpans()); + + const auto &writeBuffers = dataStorage->getWriteSpans(); + const auto totalBuffers = writeBuffers.size(); + if (totalBuffers == 0) { + queueCompletion(opInfo.opId, 0); + return; + } + + dispatch_queue_t queue = dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0); + qsizetype idx = 0; + dispatch_data_t dataToWrite = dispatch_data_empty; + qint64 totalSize = 0; + do { + const std::byte *dataPtr = writeBuffers[idx].data(); + const qint64 dataSize = writeBuffers[idx].size(); + dispatch_data_t data = dispatch_data_create(dataPtr, dataSize, queue, ^{}); + dataToWrite = dispatch_data_create_concat(dataToWrite, data); + [data release]; + totalSize += dataSize; + } while (++idx < totalBuffers); + + writeHelper(opInfo.opId, opInfo.channel, priv->offset, dataToWrite, totalSize); + } +} + +void QRandomAccessAsyncFilePrivate::executeFlush(OperationInfo &opInfo) +{ + opInfo.channel = duplicateIoChannel(opInfo.opId); + if (!opInfo.channel) { + queueCompletion(opInfo.opId, EBADF); + return; + } + + // flush() is a barrier operation, but dispatch_io_barrier does not work + // as documented with multiple channels :( + auto sharedThis = this; + const int fd = m_fd; + const OperationId opId = opInfo.opId; + dispatch_io_barrier(opInfo.channel, ^{ + const int err = fsync(fd); + + QMutexLocker locker(&sharedThis->m_mutex); + sharedThis->m_runningOps.remove(opId); + const auto cancelId = sharedThis->m_opToCancel; + if (cancelId == kAllOperationIds || cancelId == opId) { + if (cancelId == opId) + sharedThis->m_cancellationCondition.wakeOne(); + } else { + auto context = sharedThis->q_ptr; + const OperationResult res = { opId, 0LL, err }; + QMetaObject::invokeMethod(context, [sharedThis](const OperationResult &r) { + sharedThis->handleOperationComplete(r); + }, Qt::QueuedConnection, res); + } + }); +} + +// stolen from qfsfileengine_unix.cpp +static inline int openModeToOpenFlags(QIODevice::OpenMode mode) +{ + int oflags = QT_OPEN_RDONLY; +#ifdef QT_LARGEFILE_SUPPORT + oflags |= QT_OPEN_LARGEFILE; +#endif + if ((mode & QIODevice::ReadWrite) == QIODevice::ReadWrite) + oflags = QT_OPEN_RDWR; + else if (mode & QIODevice::WriteOnly) + oflags = QT_OPEN_WRONLY; + if ((mode & QIODevice::WriteOnly) + && !(mode & QIODevice::ExistingOnly)) // QFSFileEnginePrivate::openModeCanCreate(mode)) + oflags |= QT_OPEN_CREAT; + if (mode & QIODevice::Truncate) + oflags |= QT_OPEN_TRUNC; + if (mode & QIODevice::Append) + oflags |= QT_OPEN_APPEND; + if (mode & QIODevice::NewOnly) + oflags |= QT_OPEN_EXCL; + return oflags; +} + +void QRandomAccessAsyncFilePrivate::executeOpen(OperationInfo &opInfo) +{ + if (m_fileState != FileState::OpenPending) { + queueCompletion(opInfo.opId, EINVAL); + return; + } + + const QByteArray nativeName = QFile::encodeName(QDir::toNativeSeparators(m_filePath)); + + int openFlags = openModeToOpenFlags(m_openMode); + openFlags |= O_NONBLOCK; + + auto sharedThis = this; + const OperationId opId = opInfo.opId; + + // We don'd call duplicateIOChannel(), so need to update the running ops + // explicitly. + m_mutex.lock(); + m_runningOps.insert(opId); + m_mutex.unlock(); + + dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0), + ^{ + int err = 0; + const int fd = ::open(nativeName.data(), openFlags); + if (fd < 0) + err = errno; + + QMutexLocker locker(&sharedThis->m_mutex); + sharedThis->m_runningOps.remove(opId); + const auto cancelId = sharedThis->m_opToCancel; + if (cancelId == kAllOperationIds || cancelId == opId) { + // open() is a barrier operation, so it's always the + // only executing operation. + // Also, the main IO channel is not created yet. + // So we need to notify the condition variable in + // any both cases. + Q_ASSERT(sharedThis->m_runningOps.isEmpty()); + sharedThis->m_cancellationCondition.wakeOne(); + } else { + auto context = sharedThis->q_ptr; + const OperationResult res = { opId, qint64(fd), err }; + QMetaObject::invokeMethod(context, [sharedThis](const OperationResult &r) { + sharedThis->handleOperationComplete(r); + }, Qt::QueuedConnection, res); + } + }); +} + +void QRandomAccessAsyncFilePrivate::readOneBuffer(OperationId opId, qsizetype bufferIdx, + qint64 alreadyRead) +{ + // we need to lookup the operation again, because it could have beed removed + // by the user... + + auto it = std::find_if(m_operations.cbegin(), m_operations.cend(), + [opId](const auto &opInfo) { + return opId == opInfo.opId; + }); + if (it == m_operations.cend()) + return; + + auto op = it->operation; // QPointer could be null + if (!op) { + closeIoChannel(it->channel); + return; + } + + auto *priv = QIOOperationPrivate::get(op); + Q_ASSERT(priv->type == QIOOperation::Type::Read); + Q_ASSERT(priv->dataStorage->containsReadSpans()); + + auto &readBuffers = priv->dataStorage->getReadSpans(); + Q_ASSERT(readBuffers.size() > bufferIdx); + + qint64 newOffset = priv->offset; + for (qsizetype idx = 0; idx < bufferIdx; ++idx) + newOffset += readBuffers[idx].size(); + + std::byte *bytesPtr = readBuffers[bufferIdx].data(); + qint64 maxSize = readBuffers[bufferIdx].size(); + + readOneBufferHelper(opId, it->channel, newOffset, bytesPtr, maxSize, bufferIdx, + readBuffers.size(), alreadyRead); +} + +void QRandomAccessAsyncFilePrivate::readOneBufferHelper(OperationId opId, dispatch_io_t channel, + qint64 offset, void *bytesPtr, + qint64 maxSize, qsizetype currentBufferIdx, + qsizetype totalBuffers, qint64 alreadyRead) +{ + auto sharedThis = this; + __block size_t readFromBuffer = 0; + dispatch_io_read(channel, offset, maxSize, + dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0), + ^(bool done, dispatch_data_t data, int error) { + // Handle data. If there's an error, handle as much as + // we can. + if (data) { + dispatch_data_apply(data, ^(dispatch_data_t /*region*/, size_t offset, + const void *buffer, size_t size) { + const char *startPtr = + reinterpret_cast<const char *>(buffer) + offset; + // NOTE: This is a copy, but looks like we + // cannot do better :( + std::memcpy((std::byte *)bytesPtr + readFromBuffer, + startPtr, size); + readFromBuffer += size; + return true; // Keep processing if there is more data. + }); + } + + QMutexLocker locker(&sharedThis->m_mutex); + const auto cancelId = sharedThis->m_opToCancel; + if (cancelId == kAllOperationIds || cancelId == opId) { + sharedThis->m_runningOps.remove(opId); + if (cancelId == opId) + sharedThis->m_cancellationCondition.wakeOne(); + } else if (done) { + sharedThis->m_runningOps.remove(opId); + auto context = sharedThis->q_ptr; + // if error, or last buffer, or read less than expected, + // report operation completion + qint64 totalRead = qint64(readFromBuffer) + alreadyRead; + qsizetype nextBufferIdx = currentBufferIdx + 1; + if (error || nextBufferIdx == totalBuffers + || qint64(readFromBuffer) != maxSize) { + const OperationResult res = { opId, totalRead, error }; + QMetaObject::invokeMethod(context, + [sharedThis](const OperationResult &r) { + sharedThis->handleOperationComplete(r); + }, Qt::QueuedConnection, res); + } else { + // else execute read for the next buffer + QMetaObject::invokeMethod(context, + [sharedThis, opId, nextBufferIdx, totalRead] { + sharedThis->readOneBuffer(opId, nextBufferIdx, totalRead); + }, Qt::QueuedConnection); + } + } + }); +} + +void QRandomAccessAsyncFilePrivate::writeHelper(OperationId opId, dispatch_io_t channel, + qint64 offset, dispatch_data_t dataToWrite, + qint64 dataSize) +{ + auto sharedThis = this; + dispatch_io_write(channel, offset, dataToWrite, + dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0), + ^(bool done, dispatch_data_t data, int error) { + // Either an error or complete write. + // If there's an error, return the amount that we have + // written so far + QMutexLocker locker(&sharedThis->m_mutex); + const auto cancelId = sharedThis->m_opToCancel; + if (cancelId == kAllOperationIds || cancelId == opId) { + // Operation is canceled - do nothing + sharedThis->m_runningOps.remove(opId); + if (cancelId == opId) + sharedThis->m_cancellationCondition.wakeOne(); + } else if (done) { + sharedThis->m_runningOps.remove(opId); + // if no error, an attempt to access the data will + // crash, because it seems to have no buffer + // allocated (as everything was written) + const size_t toBeWritten = + (error == 0) ? 0 : dispatch_data_get_size(data); + const size_t written = dataSize - toBeWritten; + [dataToWrite release]; + + auto context = sharedThis->q_ptr; + const OperationResult res = { opId, qint64(written), error }; + QMetaObject::invokeMethod(context, + [sharedThis](const OperationResult &r) { + sharedThis->handleOperationComplete(r); + }, Qt::QueuedConnection, res); + } + }); +} + +QRandomAccessAsyncFilePrivate::OperationId QRandomAccessAsyncFilePrivate::getNextId() +{ + // never return reserved values + static OperationId opId = kInvalidOperationId; + if (++opId == kAllOperationIds) + opId = kInvalidOperationId + 1; + return opId; +} + +QT_END_NAMESPACE diff --git a/src/corelib/io/qrandomaccessasyncfile_p_p.h b/src/corelib/io/qrandomaccessasyncfile_p_p.h index ef996c37f07..73d7eebdf72 100644 --- a/src/corelib/io/qrandomaccessasyncfile_p_p.h +++ b/src/corelib/io/qrandomaccessasyncfile_p_p.h @@ -32,6 +32,17 @@ #endif // QT_RANDOMACCESSASYNCFILE_THREAD +#ifdef Q_OS_DARWIN + +#include <QtCore/qlist.h> +#include <QtCore/qmutex.h> +#include <QtCore/qset.h> +#include <QtCore/qwaitcondition.h> + +#include <dispatch/dispatch.h> + +#endif // Q_OS_DARWIN + QT_BEGIN_NAMESPACE class QRandomAccessAsyncFilePrivate : public QObjectPrivate @@ -69,6 +80,18 @@ public: writeFrom(qint64 offset, QSpan<const QSpan<const std::byte>> buffers); private: + // common for all backends + enum class FileState : quint8 + { + Closed, + OpenPending, // already got an open request + Opened, + }; + + QString m_filePath; + QIODeviceBase::OpenMode m_openMode; + FileState m_fileState = FileState::Closed; + #ifdef QT_RANDOMACCESSASYNCFILE_THREAD public: struct OperationResult @@ -78,13 +101,6 @@ public: }; private: - enum class FileState : quint8 - { - Closed, - OpenPending, // already got an open request - Opened, - }; - mutable QBasicMutex m_engineMutex; std::unique_ptr<QFSFileEngine> m_engine; QFutureWatcher<OperationResult> m_watcher; @@ -93,16 +109,90 @@ private: QPointer<QIOOperation> m_currentOperation; qsizetype numProcessedBuffers = 0; - QString m_filePath; - QIODeviceBase::OpenMode m_openMode; - FileState m_fileState = FileState::Closed; - void executeNextOperation(); void processBufferAt(qsizetype idx); void processFlush(); void processOpen(); void operationComplete(); #endif +#ifdef Q_OS_DARWIN + using OperationId = quint64; + static constexpr OperationId kInvalidOperationId = 0; + static constexpr OperationId kAllOperationIds = std::numeric_limits<OperationId>::max(); + + struct OperationResult + { + OperationId opId; + qint64 result; // num bytes processed or file descriptor + int error; + }; + + enum class OpState : quint8 + { + Pending, + Running, + }; + + struct OperationInfo + { + OperationId opId; + dispatch_io_t channel; + QPointer<QIOOperation> operation; + OpState state; + + OperationInfo(OperationId _id, QIOOperation *_op) + : opId(_id), + channel(nullptr), + operation(_op), + state(OpState::Pending) + {} + }; + + // We need to maintain an actual queue of the operations, because + // certain operations (i.e. flush) should act like barriers. The docs + // for dispatch_io_barrier mention that it can synchronize between multiple + // channels handling the same file descriptor, but that DOES NOT work in + // practice. It works correctly only when there's a signle IO channel. But + // with a signle IO channel we're not able to cancel individual operations. + // As a result, we need to make sure that all previous operations are + // completed before starting a barrier operation. Similarly, we cannot start + // any other operation until a barrier operation is finished. + QList<OperationInfo> m_operations; + dispatch_io_t m_ioChannel = nullptr; + int m_fd = -1; + + QMutex m_mutex; + // the members below should only be accessed with the mutex + OperationId m_opToCancel = kInvalidOperationId; + QSet<OperationId> m_runningOps; + QWaitCondition m_cancellationCondition; + + static OperationId getNextId(); + + template <typename Operation, typename ...Args> + Operation *addOperation(QIOOperation::Type type, qint64 offset, Args &&...args); + + dispatch_io_t createMainChannel(int fd); + dispatch_io_t duplicateIoChannel(OperationId opId); + void closeIoChannel(dispatch_io_t channel); + void releaseIoChannel(dispatch_io_t channel); + void handleOperationComplete(const OperationResult &opResult); + + void queueCompletion(OperationId opId, int error); + + void startOperationsUntilBarrier(); + void executeRead(OperationInfo &opInfo); + void executeWrite(OperationInfo &opInfo); + void executeFlush(OperationInfo &opInfo); + void executeOpen(OperationInfo &opInfo); + + void readOneBuffer(OperationId opId, qsizetype bufferIdx, qint64 alreadyRead); + void readOneBufferHelper(OperationId opId, dispatch_io_t channel, qint64 offset, + void *bytesPtr, qint64 maxSize, qsizetype currentBufferIdx, + qsizetype totalBuffers, qint64 alreadyRead); + void writeHelper(OperationId opId, dispatch_io_t channel, qint64 offset, + dispatch_data_t dataToWrite, qint64 dataSize); +#endif }; QT_END_NAMESPACE diff --git a/src/corelib/kernel/qobject.cpp b/src/corelib/kernel/qobject.cpp index 560a8c7d789..02c9f00f301 100644 --- a/src/corelib/kernel/qobject.cpp +++ b/src/corelib/kernel/qobject.cpp @@ -2715,6 +2715,20 @@ static void err_info_about_objects(const char *func, const QObject *sender, cons qCWarning(lcConnect, "QObject::%s: (receiver name: '%s')", func, b.toLocal8Bit().data()); } +Q_DECL_COLD_FUNCTION +static void connectWarning(const QObject *sender, + const QMetaObject *senderMetaObject, + const QObject *receiver, + const char *message) +{ + const char *senderString = sender ? sender->metaObject()->className() + : senderMetaObject ? senderMetaObject->className() + : "Unknown"; + const char *receiverString = receiver ? receiver->metaObject()->className() + : "Unknown"; + qCWarning(lcConnect, "QObject::connect(%s, %s): %s", senderString, receiverString, message); +} + /*! Returns a pointer to the object that sent the signal, if called in a slot activated by a signal; otherwise it returns \nullptr. The pointer @@ -4105,8 +4119,9 @@ QMetaObject::Connection QMetaObject::connectImpl(const QObject *sender, const QM { QtPrivate::SlotObjUniquePtr slotObj(slotObjRaw); + const QMetaObject *senderMetaObject = sender->metaObject(); if (!signal.isValid() || signal.methodType() != QMetaMethod::Signal) { - qCWarning(lcConnect, "QObject::connect: invalid signal parameter"); + connectWarning(sender, senderMetaObject, receiver, "invalid signal parameter"); return QMetaObject::Connection(); } @@ -4116,7 +4131,6 @@ QMetaObject::Connection QMetaObject::connectImpl(const QObject *sender, const QM QMetaObjectPrivate::memberIndexes(sender, signal, &signal_index, &dummy); } - const QMetaObject *senderMetaObject = sender->metaObject(); if (signal_index == -1) { qCWarning(lcConnect, "QObject::connect: Can't find signal %s on instance of class %s", signal.methodSignature().constData(), senderMetaObject->className()); @@ -5433,7 +5447,7 @@ QMetaObject::Connection QObject::connectImpl(const QObject *sender, void **signa { QtPrivate::SlotObjUniquePtr slotObj(slotObjRaw); if (!signal) { - qCWarning(lcConnect, "QObject::connect: invalid nullptr parameter"); + connectWarning(sender, senderMetaObject, receiver, "invalid nullptr parameter"); return QMetaObject::Connection(); } @@ -5445,26 +5459,13 @@ QMetaObject::Connection QObject::connectImpl(const QObject *sender, void **signa break; } if (!senderMetaObject) { - qCWarning(lcConnect, "QObject::connect: signal not found in %s", sender->metaObject()->className()); + connectWarning(sender, senderMetaObject, receiver, "signal not found"); return QMetaObject::Connection(nullptr); } signal_index += QMetaObjectPrivate::signalOffset(senderMetaObject); return QObjectPrivate::connectImpl(sender, signal_index, receiver, slot, slotObj.release(), type, types, senderMetaObject); } -static void connectWarning(const QObject *sender, - const QMetaObject *senderMetaObject, - const QObject *receiver, - const char *message) -{ - const char *senderString = sender ? sender->metaObject()->className() - : senderMetaObject ? senderMetaObject->className() - : "Unknown"; - const char *receiverString = receiver ? receiver->metaObject()->className() - : "Unknown"; - qCWarning(lcConnect, "QObject::connect(%s, %s): %s", senderString, receiverString, message); -} - /*! \internal @@ -5495,7 +5496,7 @@ QMetaObject::Connection QObjectPrivate::connectImpl(const QObject *sender, int s QOrderedMutexLocker locker(signalSlotLock(sender), signalSlotLock(receiver)); - if (type & Qt::UniqueConnection && slot) { + if (type & Qt::UniqueConnection) { QObjectPrivate::ConnectionData *connections = QObjectPrivate::get(s)->connections.loadRelaxed(); if (connections && connections->signalVectorCount() > signal_index) { const QObjectPrivate::Connection *c2 = connections->signalVector.loadRelaxed()->at(signal_index).first.loadRelaxed(); @@ -5683,7 +5684,7 @@ QMetaObject::Connection QObjectPrivate::connect(const QObject *sender, int signa { QtPrivate::SlotObjUniquePtr slotObj(slotObjRaw); if (!sender) { - qCWarning(lcConnect, "QObject::connect: invalid nullptr parameter"); + connectWarning(sender, nullptr, receiver, "invalid nullptr parameter"); return QMetaObject::Connection(); } const QMetaObject *senderMetaObject = sender->metaObject(); diff --git a/src/corelib/text/qunicodetools.cpp b/src/corelib/text/qunicodetools.cpp index 14c611bdb5b..2d0b65fcc76 100644 --- a/src/corelib/text/qunicodetools.cpp +++ b/src/corelib/text/qunicodetools.cpp @@ -1124,6 +1124,7 @@ static void getLineBreaks(const char16_t *string, qsizetype len, QCharAttributes static void getWhiteSpaces(const char16_t *string, qsizetype len, QCharAttributes *attributes) { for (qsizetype i = 0; i != len; ++i) { + const auto pos = i; uint ucs4 = string[i]; if (QChar::isHighSurrogate(ucs4) && i + 1 != len) { ushort low = string[i + 1]; @@ -1134,7 +1135,7 @@ static void getWhiteSpaces(const char16_t *string, qsizetype len, QCharAttribute } if (Q_UNLIKELY(QChar::isSpace(ucs4))) - attributes[i].whiteSpace = true; + attributes[pos].whiteSpace = true; } } diff --git a/src/corelib/time/qtimezoneprivate.cpp b/src/corelib/time/qtimezoneprivate.cpp index 5b61940cbbe..d8434f4fe1b 100644 --- a/src/corelib/time/qtimezoneprivate.cpp +++ b/src/corelib/time/qtimezoneprivate.cpp @@ -1081,12 +1081,6 @@ QList<QByteArray> QTimeZonePrivate::windowsIdToIanaIds(const QByteArray &windows return list; } -// Define template for derived classes to reimplement so QSharedDataPointer clone() works correctly -template<> QTimeZonePrivate *QSharedDataPointer<QTimeZonePrivate>::clone() -{ - return d->clone(); -} - static bool isEntryInIanaList(QByteArrayView id, QByteArrayView ianaIds) { qsizetype cut; diff --git a/src/corelib/time/qtimezoneprivate_p.h b/src/corelib/time/qtimezoneprivate_p.h index 2714c67b093..b1217402ce7 100644 --- a/src/corelib/time/qtimezoneprivate_p.h +++ b/src/corelib/time/qtimezoneprivate_p.h @@ -209,8 +209,6 @@ protected: }; Q_DECLARE_TYPEINFO(QTimeZonePrivate::Data, Q_RELOCATABLE_TYPE); -template<> QTimeZonePrivate *QSharedDataPointer<QTimeZonePrivate>::clone(); - class Q_AUTOTEST_EXPORT QUtcTimeZonePrivate final : public QTimeZonePrivate { bool operator=(const QUtcTimeZonePrivate &) const = delete; diff --git a/src/corelib/tools/qlist.h b/src/corelib/tools/qlist.h index 93f7ddb9465..a11f7913dc7 100644 --- a/src/corelib/tools/qlist.h +++ b/src/corelib/tools/qlist.h @@ -579,7 +579,12 @@ public: { d->assign(first, last); return *this; } QList &assign(std::initializer_list<T> l) - { return assign(l.begin(), l.end()); } + { + if (l.size()) + return assign(l.begin(), l.end()); + clear(); + return *this; + } template <typename ...Args> iterator emplace(const_iterator before, Args&&... args) diff --git a/src/gui/rhi/qrhivulkan.cpp b/src/gui/rhi/qrhivulkan.cpp index b946f8777b6..c5167a6e7de 100644 --- a/src/gui/rhi/qrhivulkan.cpp +++ b/src/gui/rhi/qrhivulkan.cpp @@ -8824,6 +8824,18 @@ bool QVkSwapChain::ensureSurface() if (ok) { colorFormat = formats[i].format; colorSpace = formats[i].colorSpace; +#if QT_CONFIG(wayland) + // On Wayland, only one color management surface can be created at a time without + // triggering a protocol error, and we create one ourselves in some situations. + // To avoid this problem, use VK_COLOR_SPACE_PASS_THROUGH_EXT when supported, + // so that the driver doesn't create a color management surface as well. + const bool hasPassThrough = std::any_of(formats.begin(), formats.end(), [this](const VkSurfaceFormatKHR &fmt) { + return fmt.format == colorFormat && fmt.colorSpace == VK_COLOR_SPACE_PASS_THROUGH_EXT; + }); + if (hasPassThrough) { + colorSpace = VK_COLOR_SPACE_PASS_THROUGH_EXT; + } +#endif break; } } diff --git a/src/gui/vulkan/qvulkanwindow.cpp b/src/gui/vulkan/qvulkanwindow.cpp index a1457006888..1e52e460d38 100644 --- a/src/gui/vulkan/qvulkanwindow.cpp +++ b/src/gui/vulkan/qvulkanwindow.cpp @@ -871,6 +871,19 @@ void QVulkanWindowPrivate::init() } } +#if QT_CONFIG(wayland) + // On Wayland, only one color management surface can be created at a time without + // triggering a protocol error, and we create one ourselves in some situations. + // To avoid this problem, use VK_COLOR_SPACE_PASS_THROUGH_EXT when supported, + // so that the driver doesn't create a color management surface as well. + const bool hasPassthrough = std::any_of(formats.cbegin(), formats.cend(), [this](const VkSurfaceFormatKHR &format) { + return format.format == colorFormat && format.colorSpace == VK_COLOR_SPACE_PASS_THROUGH_EXT; + }); + if (hasPassthrough) { + colorSpace = VK_COLOR_SPACE_PASS_THROUGH_EXT; + } +#endif + const VkFormat dsFormatCandidates[] = { VK_FORMAT_D24_UNORM_S8_UINT, VK_FORMAT_D32_SFLOAT_S8_UINT, diff --git a/src/network/kernel/qhostaddress.cpp b/src/network/kernel/qhostaddress.cpp index ec67ee80a1e..82632110d32 100644 --- a/src/network/kernel/qhostaddress.cpp +++ b/src/network/kernel/qhostaddress.cpp @@ -140,13 +140,6 @@ bool QHostAddressPrivate::parse(const QString &ipString) return false; } -void QHostAddressPrivate::clear() -{ - a = 0; - protocol = QHostAddress::UnknownNetworkLayerProtocol; - memset(&a6, 0, sizeof(a6)); -} - AddressClassification QHostAddressPrivate::classify() const { if (a) { diff --git a/src/network/kernel/qhostaddress_p.h b/src/network/kernel/qhostaddress_p.h index 6cc28cd5a9b..608080e9ede 100644 --- a/src/network/kernel/qhostaddress_p.h +++ b/src/network/kernel/qhostaddress_p.h @@ -74,7 +74,13 @@ public: void setAddress(const Q_IPV6ADDR &a_); bool parse(const QString &ipString); - void clear(); + void clear() + { + a6 = {}; + a = 0; + protocol = QHostAddress::UnknownNetworkLayerProtocol; + scopeId.clear(); + } QString scopeId; diff --git a/src/network/kernel/qnetworkinformation.cpp b/src/network/kernel/qnetworkinformation.cpp index 80551b64633..5c4e65839c5 100644 --- a/src/network/kernel/qnetworkinformation.cpp +++ b/src/network/kernel/qnetworkinformation.cpp @@ -16,7 +16,6 @@ #include <algorithm> #include <memory> -#include <mutex> QT_BEGIN_NAMESPACE diff --git a/src/widgets/doc/images/designer-stylesheet-options.png b/src/widgets/doc/images/designer-stylesheet-options.png Binary files differdeleted file mode 100644 index a6893e770bc..00000000000 --- a/src/widgets/doc/images/designer-stylesheet-options.png +++ /dev/null diff --git a/src/widgets/doc/images/designer-stylesheet-options.webp b/src/widgets/doc/images/designer-stylesheet-options.webp Binary files differnew file mode 100644 index 00000000000..14d1ad368fc --- /dev/null +++ b/src/widgets/doc/images/designer-stylesheet-options.webp diff --git a/src/widgets/doc/images/designer-stylesheet-usage.png b/src/widgets/doc/images/designer-stylesheet-usage.png Binary files differdeleted file mode 100644 index f6875900def..00000000000 --- a/src/widgets/doc/images/designer-stylesheet-usage.png +++ /dev/null diff --git a/src/widgets/doc/images/designer-stylesheet-usage.webp b/src/widgets/doc/images/designer-stylesheet-usage.webp Binary files differnew file mode 100644 index 00000000000..79dd6803853 --- /dev/null +++ b/src/widgets/doc/images/designer-stylesheet-usage.webp diff --git a/src/widgets/doc/images/designer-validator-highlighter.png b/src/widgets/doc/images/designer-validator-highlighter.png Binary files differdeleted file mode 100644 index a6661d5c955..00000000000 --- a/src/widgets/doc/images/designer-validator-highlighter.png +++ /dev/null diff --git a/src/widgets/doc/images/designer-validator-highlighter.webp b/src/widgets/doc/images/designer-validator-highlighter.webp Binary files differnew file mode 100644 index 00000000000..7ca6cdf6eb3 --- /dev/null +++ b/src/widgets/doc/images/designer-validator-highlighter.webp diff --git a/src/widgets/doc/src/widgets-and-layouts/stylesheet.qdoc b/src/widgets/doc/src/widgets-and-layouts/stylesheet.qdoc index 841948b671f..84226fdb5b5 100644 --- a/src/widgets/doc/src/widgets-and-layouts/stylesheet.qdoc +++ b/src/widgets/doc/src/widgets-and-layouts/stylesheet.qdoc @@ -536,21 +536,20 @@ to preview style sheets. You can right-click on any widget in Designer and select \uicontrol{Change styleSheet...} to set the style sheet. - \image designer-stylesheet-options.png + \image designer-stylesheet-options.webp {Editing a form in Qt Widgets Designer} - In Qt 4.2 and later, \QD also includes a - style sheet syntax highlighter and validator. The validator indicates - if the syntax is valid or invalid, at the bottom left of the \uicontrol{Edit - Style Sheet} dialog. + \QD also includes a style sheet syntax highlighter and validator. The + validator indicates if the syntax is valid or invalid, at the bottom left + of the \uicontrol{Edit Style Sheet} dialog. - \image designer-validator-highlighter.png + \image designer-validator-highlighter.webp {Editing and validating a stylesheet} When you click \uicontrol{OK} or \uicontrol{Apply}, \QD will automatically display the widget with its new stylesheet. - \image designer-stylesheet-usage.png + \image designer-stylesheet-usage.webp {Preview of a form with the new stylesheet} */ |
