Simplify VARIANT handling using a QComVariant RAII wrapper

The QComVariant RAII wrapper automates lifetime management of VARIANT
structures and elements that it contains. For example, it will
call IUnknown::AddRef if a COM interface is assigned to it. It will also
clear all held resources at destruction.

Task-number: QTBUG-126530
Change-Id: I543d236293d25cbc70ff25046e553351eccfb850
Reviewed-by: Oliver Wolff <oliver.wolff@qt.io>
(cherry picked from commit 717580c1985dd0804363bafb8822f82dacfb3f90)
Reviewed-by: Qt Cherry-pick Bot <cherrypick_bot@qt-project.org>
This commit is contained in:
Jøger Hansegård 2024-06-27 16:46:51 +02:00 committed by Qt Cherry-pick Bot
parent 51f7513bc0
commit 267e378d14
9 changed files with 401 additions and 86 deletions

View File

@ -563,6 +563,7 @@ qt_internal_extend_target(Core CONDITION WIN32
platform/windows/qcomobject_p.h
platform/windows/qcomptr_p.h
platform/windows/qbstr_p.h
platform/windows/qcomvariant_p.h
LIBRARIES
advapi32
authz

View File

@ -0,0 +1,134 @@
// Copyright (C) 2024 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
#ifndef QCOMVARIANT_P_H
#define QCOMVARIANT_P_H
//
// W A R N I N G
// -------------
//
// This file is not part of the Qt API. It exists purely as an
// implementation detail. This header file may change from version to
// version without notice, or even be removed.
//
// We mean it.
//
#if defined(Q_OS_WIN) || defined(Q_QDOC)
// clang-format off
#include <QtCore/private/qglobal_p.h>
#include <QtCore/qt_windows.h>
#include <QtCore/private/qbstr_p.h>
#include <QtCore/private/qcomptr_p.h>
// clang-format on
QT_BEGIN_NAMESPACE
struct QComVariant
{
// clang-format off
QComVariant() noexcept
{
VariantInit(&m_variant);
}
~QComVariant() noexcept
{
clear();
}
explicit QComVariant(bool value) noexcept
{
m_variant.vt = VT_BOOL;
m_variant.boolVal = value ? VARIANT_TRUE : VARIANT_FALSE;
}
explicit QComVariant(int value) noexcept
{
m_variant.vt = VT_INT;
m_variant.intVal = value;
}
explicit QComVariant(long value) noexcept
{
m_variant.vt = VT_I4;
m_variant.lVal = value;
}
explicit QComVariant(double value) noexcept
{
m_variant.vt = VT_R8;
m_variant.dblVal = value;
}
template <typename T>
QComVariant(const ComPtr<T> &value) noexcept
{
static_assert(std::is_base_of_v<IUnknown, T>, "Invalid COM interface");
ComPtr<IUnknown> unknown = value;
m_variant.vt = VT_UNKNOWN;
m_variant.punkVal = unknown.Detach(); // Transfer ownership
}
QComVariant(QBStr &&value) noexcept
{
m_variant.vt = VT_BSTR;
m_variant.bstrVal = value.release(); // Transfer ownership
}
QComVariant(const QString &str)
{
m_variant.vt = VT_BSTR;
m_variant.bstrVal = QBStr{ str }.release(); // Transfer ownership of copy
}
const VARIANT &get() const noexcept
{
return m_variant;
}
VARIANT &get() noexcept
{
return m_variant;
}
// clang-format on
[[nodiscard]] VARIANT *operator&() noexcept // NOLINT(google-runtime-operator)
{
clear();
return &m_variant;
}
VARIANT release() noexcept
{
const VARIANT detached{ m_variant };
VariantInit(&m_variant);
return detached;
}
QComVariant(const QComVariant &) = delete;
QComVariant &operator=(const QComVariant &) = delete;
QComVariant(QComVariant && other) = delete;
QComVariant &operator=(QComVariant &&) = delete;
private:
void clear()
{
const HRESULT hr = VariantClear(&m_variant);
Q_ASSERT(hr == S_OK);
Q_UNUSED(hr)
VariantInit(&m_variant); // Clear value field
}
VARIANT m_variant{};
};
QT_END_NAMESPACE
#endif // Q_OS_WIN
#endif // QCOMVARIANT_P_H

View File

@ -28,6 +28,7 @@
#include <QtGui/qguiapplication.h>
#include <QtGui/qwindow.h>
#include <qpa/qplatforminputcontextfactory_p.h>
#include <QtCore/private/qcomvariant_p.h>
#if !defined(Q_CC_BOR) && !defined (Q_CC_GNU)
#include <comdef.h>
@ -85,14 +86,14 @@ void QWindowsUiaMainProvider::notifyStateChange(QAccessibleStateChangeEvent *eve
// Notifies states changes in checkboxes.
if (accessible->role() == QAccessible::CheckBox) {
if (auto provider = providerForAccessible(accessible)) {
VARIANT oldVal, newVal;
clearVariant(&oldVal);
int toggleState = ToggleState_Off;
long toggleState = ToggleState_Off;
if (accessible->state().checked)
toggleState = accessible->state().checkStateMixed ? ToggleState_Indeterminate : ToggleState_On;
setVariantI4(toggleState, &newVal);
QComVariant oldVal;
QComVariant newVal{toggleState};
UiaRaiseAutomationPropertyChangedEvent(
provider.Get(), UIA_ToggleToggleStatePropertyId, oldVal, newVal);
provider.Get(), UIA_ToggleToggleStatePropertyId, oldVal.get(), newVal.get());
}
}
}
@ -139,24 +140,18 @@ void QWindowsUiaMainProvider::notifyValueChange(QAccessibleValueChangeEvent *eve
if (event->value().typeId() == QMetaType::QString) {
if (auto provider = providerForAccessible(accessible)) {
// Notifies changes in string values.
VARIANT oldVal, newVal;
clearVariant(&oldVal);
setVariantString(event->value().toString(), &newVal);
const QComVariant oldVal;
const QComVariant newVal{ event->value().toString() };
UiaRaiseAutomationPropertyChangedEvent(provider.Get(), UIA_ValueValuePropertyId,
oldVal, newVal);
HRESULT hr = VariantClear(&newVal); // Free string allocated by setVariantString
Q_ASSERT(hr == S_OK);
Q_UNUSED(hr)
oldVal.get(), newVal.get());
}
} else if (QAccessibleValueInterface *valueInterface = accessible->valueInterface()) {
if (auto provider = providerForAccessible(accessible)) {
// Notifies changes in values of controls supporting the value interface.
VARIANT oldVal, newVal;
clearVariant(&oldVal);
setVariantDouble(valueInterface->currentValue().toDouble(), &newVal);
const QComVariant oldVal;
const QComVariant newVal{ valueInterface->currentValue().toDouble() };
UiaRaiseAutomationPropertyChangedEvent(
provider.Get(), UIA_RangeValueValuePropertyId, oldVal, newVal);
provider.Get(), UIA_RangeValueValuePropertyId, oldVal.get(), newVal.get());
}
}
}
@ -169,12 +164,10 @@ void QWindowsUiaMainProvider::notifyNameChange(QAccessibleEvent *event)
// in order to avoid slowdowns with unnecessary notifications.
if (accessible->role() == QAccessible::ComboBox) {
if (auto provider = providerForAccessible(accessible)) {
VARIANT oldVal, newVal;
clearVariant(&oldVal);
setVariantString(accessible->text(QAccessible::Name), &newVal);
UiaRaiseAutomationPropertyChangedEvent(provider.Get(), UIA_NamePropertyId, oldVal,
newVal);
::SysFreeString(newVal.bstrVal);
QComVariant oldVal;
QComVariant newVal{ accessible->text(QAccessible::Name) };
UiaRaiseAutomationPropertyChangedEvent(provider.Get(), UIA_NamePropertyId,
oldVal.get(), newVal.get());
}
}
}
@ -424,7 +417,7 @@ void QWindowsUiaMainProvider::setAriaProperties(QAccessibleInterface *accessible
}
}
setVariantString(ariaString, pRetVal);
*pRetVal = QComVariant{ ariaString }.release();
}
void QWindowsUiaMainProvider::setStyle(QAccessibleInterface *accessible, VARIANT *pRetVal)
@ -449,8 +442,8 @@ void QWindowsUiaMainProvider::setStyle(QAccessibleInterface *accessible, VARIANT
if (level < 1 || level > 9)
return;
const int styleId = styleIdForHeadingLevel(level);
setVariantI4(styleId, pRetVal);
const long styleId = styleIdForHeadingLevel(level);
*pRetVal = QComVariant{ styleId }.release();
}
int QWindowsUiaMainProvider::styleIdForHeadingLevel(int headingLevel)
@ -490,24 +483,24 @@ HRESULT QWindowsUiaMainProvider::GetPropertyValue(PROPERTYID idProp, VARIANT *pR
switch (idProp) {
case UIA_ProcessIdPropertyId:
// PID
setVariantI4(int(GetCurrentProcessId()), pRetVal);
*pRetVal = QComVariant{ static_cast<long>(GetCurrentProcessId()) }.release();
break;
case UIA_AccessKeyPropertyId:
// Accelerator key.
setVariantString(accessible->text(QAccessible::Accelerator), pRetVal);
*pRetVal = QComVariant{ accessible->text(QAccessible::Accelerator) }.release();
break;
case UIA_AriaPropertiesPropertyId:
setAriaProperties(accessible, pRetVal);
break;
case UIA_AutomationIdPropertyId:
// Automation ID, which can be used by tools to select a specific control in the UI.
setVariantString(QAccessibleBridgeUtils::accessibleId(accessible), pRetVal);
*pRetVal = QComVariant{ QAccessibleBridgeUtils::accessibleId(accessible) }.release();
break;
case UIA_ClassNamePropertyId:
// Class name.
if (QObject *o = accessible->object()) {
QString className = QLatin1StringView(o->metaObject()->className());
setVariantString(className, pRetVal);
*pRetVal = QComVariant{ className }.release();
}
break;
case UIA_DescribedByPropertyId:
@ -520,12 +513,12 @@ HRESULT QWindowsUiaMainProvider::GetPropertyValue(PROPERTYID idProp, VARIANT *pR
fillVariantArrayForRelation(accessible, QAccessible::FlowsFrom, pRetVal);
break;
case UIA_FrameworkIdPropertyId:
setVariantString(QStringLiteral("Qt"), pRetVal);
*pRetVal = QComVariant{ QStringLiteral("Qt") }.release();
break;
case UIA_ControlTypePropertyId:
if (topLevelWindow) {
// Reports a top-level widget as a window, instead of "custom".
setVariantI4(UIA_WindowControlTypeId, pRetVal);
*pRetVal = QComVariant{ UIA_WindowControlTypeId }.release();
} else {
// Control type converted from role.
auto controlType = roleToControlTypeId(accessible->role());
@ -540,63 +533,66 @@ HRESULT QWindowsUiaMainProvider::GetPropertyValue(PROPERTYID idProp, VARIANT *pR
if (controlType == UIA_EditControlTypeId && (!imModuleEmpty || nativeVKDisabled))
controlType = UIA_TextControlTypeId;
setVariantI4(controlType, pRetVal);
*pRetVal = QComVariant{ controlType }.release();
}
break;
case UIA_HelpTextPropertyId:
setVariantString(accessible->text(QAccessible::Help), pRetVal);
*pRetVal = QComVariant{ accessible->text(QAccessible::Help) }.release();
break;
case UIA_HasKeyboardFocusPropertyId:
if (topLevelWindow) {
// Windows set the active state to true when they are focused
setVariantBool(accessible->state().active, pRetVal);
*pRetVal = QComVariant{ accessible->state().active ? true : false }.release();
} else {
setVariantBool(accessible->state().focused, pRetVal);
*pRetVal = QComVariant{ accessible->state().focused ? true : false }.release();
}
break;
case UIA_IsKeyboardFocusablePropertyId:
if (topLevelWindow) {
// Windows should always be focusable
setVariantBool(true, pRetVal);
*pRetVal = QComVariant{ true }.release();
} else {
setVariantBool(accessible->state().focusable, pRetVal);
*pRetVal = QComVariant{ accessible->state().focusable ? true : false }.release();
}
break;
case UIA_IsOffscreenPropertyId:
setVariantBool(accessible->state().offscreen, pRetVal);
*pRetVal = QComVariant{ accessible->state().offscreen ? true : false }.release();
break;
case UIA_IsContentElementPropertyId:
setVariantBool(true, pRetVal);
*pRetVal = QComVariant{ true }.release();
break;
case UIA_IsControlElementPropertyId:
setVariantBool(true, pRetVal);
*pRetVal = QComVariant{ true }.release();
break;
case UIA_IsEnabledPropertyId:
setVariantBool(!accessible->state().disabled, pRetVal);
*pRetVal = QComVariant{ !accessible->state().disabled }.release();
break;
case UIA_IsPasswordPropertyId:
setVariantBool(accessible->role() == QAccessible::EditableText
&& accessible->state().passwordEdit, pRetVal);
*pRetVal = QComVariant{ accessible->role() == QAccessible::EditableText
&& accessible->state().passwordEdit }
.release();
break;
case UIA_IsPeripheralPropertyId:
// True for peripheral UIs.
if (QWindow *window = windowForAccessible(accessible)) {
const Qt::WindowType wt = window->type();
setVariantBool(wt == Qt::Popup || wt == Qt::ToolTip || wt == Qt::SplashScreen, pRetVal);
*pRetVal = QComVariant{ wt == Qt::Popup || wt == Qt::ToolTip || wt == Qt::SplashScreen }
.release();
}
break;
case UIA_IsDialogPropertyId:
setVariantBool(accessible->role() == QAccessible::Dialog
|| accessible->role() == QAccessible::AlertMessage, pRetVal);
*pRetVal = QComVariant{ accessible->role() == QAccessible::Dialog
|| accessible->role() == QAccessible::AlertMessage }
.release();
break;
case UIA_FullDescriptionPropertyId:
setVariantString(accessible->text(QAccessible::Description), pRetVal);
*pRetVal = QComVariant{ accessible->text(QAccessible::Description) }.release();
break;
case UIA_NamePropertyId: {
QString name = accessible->text(QAccessible::Name);
if (name.isEmpty() && topLevelWindow)
name = QCoreApplication::applicationName();
setVariantString(name, pRetVal);
*pRetVal = QComVariant{ name }.release();
break;
}
case UIA_StyleIdAttributeId:

View File

@ -13,6 +13,7 @@
#include <QtCore/qloggingcategory.h>
#include <QtCore/qstring.h>
#include <QtCore/qvarlengtharray.h>
#include <QtCore/private/qcomvariant_p.h>
QT_BEGIN_NAMESPACE
@ -157,15 +158,15 @@ HRESULT STDMETHODCALLTYPE QWindowsUiaTextRangeProvider::GetAttributeValue(TEXTAT
switch (attributeId) {
case UIA_IsReadOnlyAttributeId:
setVariantBool(accessible->state().readOnly, pRetVal);
*pRetVal = QComVariant{ accessible->state().readOnly ? true : false }.release();
break;
case UIA_CaretPositionAttributeId:
if (textInterface->cursorPosition() == 0)
setVariantI4(CaretPosition_BeginningOfLine, pRetVal);
*pRetVal = QComVariant{ static_cast<long>(CaretPosition_BeginningOfLine) }.release();
else if (textInterface->cursorPosition() == textInterface->characterCount())
setVariantI4(CaretPosition_EndOfLine, pRetVal);
*pRetVal = QComVariant{ static_cast<long>(CaretPosition_EndOfLine) }.release();
else
setVariantI4(CaretPosition_Unknown, pRetVal);
*pRetVal = QComVariant{ static_cast<long>(CaretPosition_Unknown) }.release();
break;
case UIA_StrikethroughStyleAttributeId:
{
@ -173,7 +174,7 @@ HRESULT STDMETHODCALLTYPE QWindowsUiaTextRangeProvider::GetAttributeValue(TEXTAT
if (value.isEmpty())
break;
const TextDecorationLineStyle uiaLineStyle = uiaLineStyleForIA2LineStyle(value);
setVariantI4(uiaLineStyle, pRetVal);
*pRetVal = QComVariant{ static_cast<long>(uiaLineStyle) }.release();
break;
}
default:

View File

@ -63,30 +63,6 @@ void clearVariant(VARIANT *variant)
variant->punkVal = nullptr;
}
void setVariantI4(int value, VARIANT *variant)
{
variant->vt = VT_I4;
variant->lVal = value;
}
void setVariantBool(bool value, VARIANT *variant)
{
variant->vt = VT_BOOL;
variant->boolVal = value ? -1 : 0;
}
void setVariantDouble(double value, VARIANT *variant)
{
variant->vt = VT_R8;
variant->dblVal = value;
}
void setVariantString(const QString &value, VARIANT *variant)
{
variant->vt = VT_BSTR;
variant->bstrVal = QBStr(value).release();
}
// Scales a rect to native coordinates, according to high dpi settings.
void rectToNativeUiaRect(const QRect &rect, const QWindow *w, UiaRect *uiaRect)
{

View File

@ -33,14 +33,6 @@ bool isTextUnitSeparator(TextUnit unit, const QChar &ch);
void clearVariant(VARIANT *variant);
void setVariantI4(int value, VARIANT *variant);
void setVariantBool(bool value, VARIANT *variant);
void setVariantDouble(double value, VARIANT *variant);
void setVariantString(const QString &value, VARIANT *variant);
} // namespace QWindowsUiAutomation
QT_END_NAMESPACE

View File

@ -3,3 +3,4 @@
add_subdirectory(qcomobject)
add_subdirectory(qbstr)
add_subdirectory(qcomvariant)

View File

@ -0,0 +1,15 @@
# Copyright (C) 2024 The Qt Company Ltd.
# SPDX-License-Identifier: BSD-3-Clause
if(NOT QT_BUILD_STANDALONE_TESTS AND NOT QT_BUILDING_QT)
cmake_minimum_required(VERSION 3.16)
project(tst_qcomobject LANGUAGES CXX)
find_package(Qt6BuildInternals REQUIRED COMPONENTS STANDALONE_TEST)
endif()
qt_internal_add_test(tst_qcomvariant
SOURCES
tst_qcomvariant.cpp
LIBRARIES
Qt::CorePrivate
)

View File

@ -0,0 +1,199 @@
// Copyright (C) 2024 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
#include <QTest>
#include <private/qcomvariant_p.h>
#include <private/qcomobject_p.h>
#include <wrl/client.h>
using Microsoft::WRL::ComPtr;
QT_USE_NAMESPACE
using namespace Qt::StringLiterals;
struct IUnknownStub : QComObject<IUnknown>
{
ULONG refCount()
{
AddRef();
return Release();
}
};
extern "C" {
void __cdecl SetOaNoCache(void);
}
class tst_qcomvariant : public QObject
{
Q_OBJECT
public slots:
void initTestCase()
{
SetOaNoCache();
}
private slots:
static void constructor_initializesToEmpty()
{
const QComVariant var;
QCOMPARE_EQ(var.get().vt, VT_EMPTY);
}
static void constructor_initializes_withBool()
{
QComVariant falseVal{ false };
QCOMPARE_EQ(falseVal.get().vt, VT_BOOL);
QCOMPARE_EQ(falseVal.get().boolVal, VARIANT_FALSE);
QComVariant trueVal{ true };
QCOMPARE_EQ(trueVal.get().vt, VT_BOOL);
QCOMPARE_EQ(trueVal.get().boolVal, VARIANT_TRUE);
}
static void constructor_initializes_withInt()
{
QComVariant var{ 3 };
QCOMPARE_EQ(var.get().vt, VT_INT);
QCOMPARE_EQ(var.get().intVal, 3);
}
static void constructor_initializes_withLong()
{
QComVariant var{ 4l };
QCOMPARE_EQ(var.get().vt, VT_I4);
QCOMPARE_EQ(var.get().lVal, 4);
}
static void constructor_initializes_withDouble()
{
QComVariant var{ 3.14 };
QCOMPARE_EQ(var.get().vt, VT_R8);
QVERIFY(qFuzzyCompare(var.get().dblVal, 3.14));
}
static void constructor_initializes_withQBStr()
{
QComVariant var{ QBStr{ L"Hello world!" } };
QCOMPARE_EQ(var.get().vt, VT_BSTR);
QCOMPARE_EQ(var.get().bstrVal, QStringView(L"Hello world!"));
}
static void constructor_initializesWithNullQBStr()
{
QComVariant var{ QBStr{ nullptr } };
QCOMPARE_EQ(var.get().vt, VT_BSTR);
QCOMPARE_EQ(var.get().bstrVal, nullptr);
}
static void constructor_initializes_withQString()
{
QComVariant var{ "Hello world!"_L1 };
QCOMPARE_EQ(var.get().vt, VT_BSTR);
QCOMPARE_EQ(var.get().bstrVal, QStringView(L"Hello world!"));
}
static void constructor_initializesWithNullQString()
{
QComVariant var{ QString{} };
QCOMPARE_EQ(var.get().vt, VT_BSTR);
QCOMPARE_EQ(var.get().bstrVal, nullptr);
}
static void constructor_initializesWithEmptyQString()
{
QComVariant var{ QString{ ""_L1 } };
QCOMPARE_EQ(var.get().vt, VT_BSTR);
QCOMPARE_EQ(var.get().bstrVal, ""_L1);
}
static void constructor_initializes_withIUnknown()
{
const ComPtr<IUnknownStub> value = makeComObject<IUnknownStub>();
QComVariant var{ value };
QCOMPARE_EQ(var.get().vt, VT_UNKNOWN);
QCOMPARE_EQ(var.get().punkVal, value.Get());
}
static void constructor_initializes_withNullptrIUnknown()
{
ComPtr<IUnknown> ptr;
QComVariant var{ ptr };
QCOMPARE_EQ(var.get().vt, VT_UNKNOWN);
QCOMPARE_EQ(var.get().punkVal, nullptr);
}
static void addressOfOperator_clearsVariant_whenVariantOwnsIUnknown()
{
// Arrange
ComPtr<IUnknownStub> value = makeComObject<IUnknownStub>();
QComVariant var{ value };
QCOMPARE_EQ(value->refCount(), 2);
// Act
const VARIANT *ptr = &var;
// Assert
QCOMPARE_EQ(value->refCount(), 1);
QCOMPARE_EQ(ptr->vt, VT_EMPTY);
QCOMPARE_EQ(ptr->punkVal, nullptr);
}
static void destructor_clearsVariant_whenOwningIUnknown()
{
// Arrange
const ComPtr<IUnknownStub> value = makeComObject<IUnknownStub>();
{
QComVariant var{ value };
QCOMPARE_EQ(value->refCount(), 2);
// Act (run destructor)
}
// Assert
QCOMPARE_EQ(value->refCount(), 1);
}
static void destructor_doesNotCrash_whenOwningBSTR()
{
// Arrange
QComVariant var{ QBStr{ L"Hello world!" } };
// Act (run destructor)
}
static void release_releasesOwnershipAndReturnsVariant_whenOwningIUnknown()
{
// Arrange
const ComPtr<IUnknownStub> value = makeComObject<IUnknownStub>();
VARIANT detached{};
{
QComVariant var{ value };
QCOMPARE_EQ(value->refCount(), 2);
// Act (Release and run destructor)
detached = var.release();
}
// Assert
QCOMPARE_EQ(value->refCount(), 2);
QCOMPARE_EQ(detached.vt, VT_UNKNOWN);
QCOMPARE_EQ(detached.punkVal, value.Get());
// Cleanup
QCOMPARE_EQ(VariantClear(&detached), S_OK);
}
};
QTEST_MAIN(tst_qcomvariant)
#include "tst_qcomvariant.moc"