diff options
-rw-r--r-- | examples/webchannel/shared/qwebchannel.js | 6 | ||||
-rw-r--r-- | src/webchannel/doc/src/javascript.qdoc | 20 | ||||
-rw-r--r-- | src/webchannel/qmetaobjectpublisher.cpp | 183 | ||||
-rw-r--r-- | src/webchannel/qmetaobjectpublisher_p.h | 38 | ||||
-rw-r--r-- | tests/auto/webchannel/tst_webchannel.cpp | 83 | ||||
-rw-r--r-- | tests/auto/webchannel/tst_webchannel.h | 6 |
6 files changed, 293 insertions, 43 deletions
diff --git a/examples/webchannel/shared/qwebchannel.js b/examples/webchannel/shared/qwebchannel.js index 800a66e..9108a61 100644 --- a/examples/webchannel/shared/qwebchannel.js +++ b/examples/webchannel/shared/qwebchannel.js @@ -339,6 +339,10 @@ function QObject(name, data, webChannel) { var methodName = methodData[0]; var methodIdx = methodData[1]; + + // Fully specified methods are invoked by id, others by name for host-side overload resolution + var invokedMethod = methodName[methodName.length - 1] === ')' ? methodIdx : methodName + object[methodName] = function() { var args = []; var callback; @@ -357,7 +361,7 @@ function QObject(name, data, webChannel) webChannel.exec({ "type": QWebChannelMessageTypes.invokeMethod, "object": object.__id__, - "method": methodIdx, + "method": invokedMethod, "args": args }, function(response) { if (response !== undefined) { diff --git a/src/webchannel/doc/src/javascript.qdoc b/src/webchannel/doc/src/javascript.qdoc index 9f8c580..ef44250 100644 --- a/src/webchannel/doc/src/javascript.qdoc +++ b/src/webchannel/doc/src/javascript.qdoc @@ -100,9 +100,15 @@ new QWebChannel(yourTransport, function(channel) { \section2 Overloaded methods and signals - When you publish a \c QObject that has overloaded methods or signals, then - only the first one is accessible directly via the pretty JavaScript notation. - All others are accessible through their complete \c QMetaMethod signature. + When you publish a \c QObject that has overloaded methods, QWebChannel will resolve + method invocations to the best match. Note that due to JavaScript's type system, there is only + a single 'number' type which maps best to a C++ 'double'. When overloads differ only in the type + of a number-like parameter, QWebChannel will always choose that overload which best matches the + JavaScript 'number' type. + When you connect to an overloaded signal, the QWebChannel client will by default only connect to + the first signal overload of that name. + Additionally, overloads of methods and signals can explicitly be requested by their complete + \c QMetaMethod signature. Assume we have the following \c QObject subclass on the C++ side: \code @@ -111,6 +117,7 @@ new QWebChannel(yourTransport, function(channel) { Q_OBJECT slots: void foo(int i); + void foo(double d); void foo(const QString &str); void foo(const QString &str, int i); @@ -125,9 +132,10 @@ new QWebChannel(yourTransport, function(channel) { \code // methods - foo.foo(42); // will call first method named foo, i.e. foo(int i) - foo.foo("asdf"); // will also call foo(int i), probably not what you want - foo["foo(int)"](42); // explicitly call foo(int i) + foo.foo(42); // will call the method named foo which best matches the JavaScript number parameter, i.e. foo(double d) + foo.foo("asdf"); // will call foo(const QString &str) + foo.foo("asdf", 42); // will call foo(const QString &str, int i) + foo["foo(int)"](42); // explicitly call foo(int i), *not* foo(double d) foo["foo(QString)"]("asdf"); // explicitly call foo(const QString &str) foo["foo(QString,int)"]("asdf", 42); // explicitly call foo(const QString &str, int i) diff --git a/src/webchannel/qmetaobjectpublisher.cpp b/src/webchannel/qmetaobjectpublisher.cpp index b0581c4..9f5e9cd 100644 --- a/src/webchannel/qmetaobjectpublisher.cpp +++ b/src/webchannel/qmetaobjectpublisher.cpp @@ -83,6 +83,65 @@ bool isQFlagsType(uint id) return mo->indexOfEnumerator(name.constData()) > -1; } +// Common scores for overload resolution +enum OverloadScore { + PerfectMatchScore = 0, + VariantScore = 1, + NumberBaseScore = 2, + GenericConversionScore = 100, + IncompatibleScore = 10000, +}; + +// Scores the conversion of a double to a number-like user type. Better matches +// for a JS 'number' get a lower score. +int doubleToNumberConversionScore(int userType) +{ + switch (userType) { + case QMetaType::Bool: + return NumberBaseScore + 7; + case QMetaType::Char: + case QMetaType::SChar: + case QMetaType::UChar: + return NumberBaseScore + 6; + case QMetaType::Short: + case QMetaType::UShort: + return NumberBaseScore + 5; + case QMetaType::Int: + case QMetaType::UInt: + return NumberBaseScore + 4; + case QMetaType::Long: + case QMetaType::ULong: + return NumberBaseScore + 3; + case QMetaType::LongLong: + case QMetaType::ULongLong: + return NumberBaseScore + 2; + case QMetaType::Float: + return NumberBaseScore + 1; + case QMetaType::Double: + return NumberBaseScore; + default: + break; + } + + if (QMetaType::typeFlags(userType) & QMetaType::IsEnumeration) + return doubleToNumberConversionScore(QMetaType::Int); + + return IncompatibleScore; +} + +// Keeps track of the badness of a QMetaMethod candidate for overload resolution +struct OverloadResolutionCandidate +{ + OverloadResolutionCandidate(const QMetaMethod &method = QMetaMethod(), int badness = PerfectMatchScore) + : method(method), badness(badness) + {} + + QMetaMethod method; + int badness; + + bool operator<(const OverloadResolutionCandidate &other) const { return badness < other.badness; } +}; + MessageType toType(const QJsonValue &value) { int i = value.toInt(-1); @@ -122,6 +181,8 @@ QJsonObject createResponse(const QJsonValue &id, const QJsonValue &data) const int PROPERTY_UPDATE_INTERVAL = 50; } +Q_DECLARE_TYPEINFO(OverloadResolutionCandidate, Q_MOVABLE_TYPE); + QMetaObjectPublisher::QMetaObjectPublisher(QWebChannel *webChannel) : QObject(webChannel) , webChannel(webChannel) @@ -368,17 +429,15 @@ void QMetaObjectPublisher::sendPendingPropertyUpdates() } } -QVariant QMetaObjectPublisher::invokeMethod(QObject *const object, const int methodIndex, +QVariant QMetaObjectPublisher::invokeMethod(QObject *const object, const QMetaMethod &method, const QJsonArray &args) { - const QMetaMethod &method = object->metaObject()->method(methodIndex); - if (method.name() == QByteArrayLiteral("deleteLater")) { // invoke `deleteLater` on wrapped QObject indirectly deleteWrappedObject(object); return QJsonValue(); } else if (!method.isValid()) { - qWarning() << "Cannot invoke unknown method of index" << methodIndex << "on object" << object << '.'; + qWarning() << "Cannot invoke invalid method on object" << object << '.'; return QJsonValue(); } else if (method.access() != QMetaMethod::Public) { qWarning() << "Cannot invoke non-public method" << method.name() << "on object" << object << '.'; @@ -422,6 +481,55 @@ QVariant QMetaObjectPublisher::invokeMethod(QObject *const object, const int met return returnValue; } +QVariant QMetaObjectPublisher::invokeMethod(QObject *const object, const int methodIndex, + const QJsonArray &args) +{ + const QMetaMethod &method = object->metaObject()->method(methodIndex); + if (!method.isValid()) { + qWarning() << "Cannot invoke method of unknown index" << methodIndex << "on object" + << object << '.'; + return QJsonValue(); + } + return invokeMethod(object, method, args); +} + +QVariant QMetaObjectPublisher::invokeMethod(QObject *const object, const QByteArray &methodName, + const QJsonArray &args) +{ + QVector<OverloadResolutionCandidate> candidates; + + const QMetaObject *mo = object->metaObject(); + for (int i = 0; i < mo->methodCount(); ++i) { + QMetaMethod method = mo->method(i); + if (method.name() != methodName || method.parameterCount() != args.count() + || method.access() != QMetaMethod::Public + || (method.methodType() != QMetaMethod::Method + && method.methodType() != QMetaMethod::Slot) + || method.parameterCount() > 10) + { + // Not a candidate + continue; + } + + candidates.append({method, methodOverloadBadness(method, args)}); + } + + if (candidates.isEmpty()) { + qWarning() << "No candidates found for" << methodName << "with" << args.size() + << "arguments on object" << object << '.'; + return QJsonValue(); + } + + std::sort(candidates.begin(), candidates.end()); + + if (candidates.size() > 1 && candidates[0].badness == candidates[1].badness) { + qWarning().nospace() << "Ambiguous overloads for method " << methodName << ". Choosing " + << candidates.first().method.methodSignature(); + } + + return invokeMethod(object, candidates.first().method, args); +} + void QMetaObjectPublisher::setProperty(QObject *object, const int propertyIndex, const QJsonValue &value) { QMetaProperty property = object->metaObject()->property(propertyIndex); @@ -534,6 +642,57 @@ QVariant QMetaObjectPublisher::toVariant(const QJsonValue &value, int targetType return variant; } +int QMetaObjectPublisher::conversionScore(const QJsonValue &value, int targetType) const +{ + if (targetType == QMetaType::QJsonValue) { + return PerfectMatchScore; + } else if (targetType == QMetaType::QJsonArray) { + return value.isArray() ? PerfectMatchScore : IncompatibleScore; + } else if (targetType == QMetaType::QJsonObject) { + return value.isObject() ? PerfectMatchScore : IncompatibleScore; + } else if (QMetaType::typeFlags(targetType) & QMetaType::PointerToQObject) { + if (value.isNull()) + return PerfectMatchScore; + if (!value.isObject()) + return IncompatibleScore; + + QJsonObject object = value.toObject(); + if (object[KEY_ID].isUndefined()) + return IncompatibleScore; + + QObject *unwrappedObject = unwrapObject(object[KEY_ID].toString()); + return unwrappedObject != Q_NULLPTR ? PerfectMatchScore : IncompatibleScore; + } else if (targetType == QMetaType::QVariant) { + return VariantScore; + } + + // Check if this is a number conversion + if (value.isDouble()) { + int score = doubleToNumberConversionScore(targetType); + if (score != IncompatibleScore) { + return score; + } + } + + QVariant variant = value.toVariant(); + if (variant.userType() == targetType) { + return PerfectMatchScore; + } else if (variant.canConvert(targetType)) { + return GenericConversionScore; + } + + return IncompatibleScore; +} + +int QMetaObjectPublisher::methodOverloadBadness(const QMetaMethod &method, const QJsonArray &args) const +{ + int badness = PerfectMatchScore; + for (int i = 0; i < args.size(); ++i) { + badness += conversionScore(args[i], method.parameterType(i)); + } + return badness; +} + void QMetaObjectPublisher::transportRemoved(QWebChannelAbstractTransport *transport) { auto it = transportedWrappedObjects.find(transport); @@ -715,10 +874,18 @@ void QMetaObjectPublisher::handleMessage(const QJsonObject &message, QWebChannel QPointer<QMetaObjectPublisher> publisherExists(this); QPointer<QWebChannelAbstractTransport> transportExists(transport); - QVariant result = - invokeMethod(object, - message.value(KEY_METHOD).toInt(-1), - message.value(KEY_ARGS).toArray()); + QJsonValue method = message.value(KEY_METHOD); + QVariant result; + + if (method.isString()) { + result = invokeMethod(object, + method.toString().toUtf8(), + message.value(KEY_ARGS).toArray()); + } else { + result = invokeMethod(object, + method.toInt(-1), + message.value(KEY_ARGS).toArray()); + } if (!publisherExists || !transportExists) return; transport->sendMessage(createResponse(message.value(KEY_ID), wrapResult(result, transport))); diff --git a/src/webchannel/qmetaobjectpublisher_p.h b/src/webchannel/qmetaobjectpublisher_p.h index 23a9b96..6030de2 100644 --- a/src/webchannel/qmetaobjectpublisher_p.h +++ b/src/webchannel/qmetaobjectpublisher_p.h @@ -148,6 +148,14 @@ public: void sendPendingPropertyUpdates(); /** + * Invoke the @p method on @p object with the arguments @p args. + * + * The return value of the method invocation is then serialized and a response message + * is returned. + */ + QVariant invokeMethod(QObject *const object, const QMetaMethod &method, const QJsonArray &args); + + /** * Invoke the method of index @p methodIndex on @p object with the arguments @p args. * * The return value of the method invocation is then serialized and a response message @@ -156,6 +164,16 @@ public: QVariant invokeMethod(QObject *const object, const int methodIndex, const QJsonArray &args); /** + * Invoke the method of name @p methodName on @p object with the arguments @p args. + * + * This method performs overload resolution on @p methodName. + * + * The return value of the method invocation is then serialized and a response message + * is returned. + */ + QVariant invokeMethod(QObject *const object, const QByteArray &methodName, const QJsonArray &args); + + /** * Set the value of property @p propertyIndex on @p object to @p value. */ void setProperty(QObject *object, const int propertyIndex, const QJsonValue &value); @@ -177,6 +195,26 @@ public: QVariant toVariant(const QJsonValue &value, int targetType) const; /** + * Assigns a score for the conversion from @p value to @p targetType. + * + * Scores can be compared to find the best match. The lower the score, the + * more preferable is the conversion. + * + * @sa invokeMethod, methodOverloadBadness + */ + int conversionScore(const QJsonValue &value, int targetType) const; + + /** + * Scores @p method against @p args. + * + * Scores can be compared to find the best match from a set of overloads. + * The lower the score, the more preferable is the method. + * + * @sa invokeMethod, conversionScore + */ + int methodOverloadBadness(const QMetaMethod &method, const QJsonArray &args) const; + + /** * Remove wrapped objects which last transport relation is with the passed transport object. */ void transportRemoved(QWebChannelAbstractTransport *transport); diff --git a/tests/auto/webchannel/tst_webchannel.cpp b/tests/auto/webchannel/tst_webchannel.cpp index 24778c7..4e622c7 100644 --- a/tests/auto/webchannel/tst_webchannel.cpp +++ b/tests/auto/webchannel/tst_webchannel.cpp @@ -383,9 +383,12 @@ void TestWebChannel::testInfoForObject() addMethod(QStringLiteral("setObjectProperty"), "setObjectProperty(QObject*)"); addMethod(QStringLiteral("setProp"), "setProp(QString)"); addMethod(QStringLiteral("fire"), "fire()"); - addMethod(QStringLiteral("overload"), "overload(int)"); + addMethod(QStringLiteral("overload"), "overload(double)"); + addMethod(QStringLiteral("overload"), "overload(int)", false); + addMethod(QStringLiteral("overload"), "overload(QObject*)", false); addMethod(QStringLiteral("overload"), "overload(QString)", false); addMethod(QStringLiteral("overload"), "overload(QString,int)", false); + addMethod(QStringLiteral("overload"), "overload(QJsonArray)", false); addMethod(QStringLiteral("method1"), "method1()"); QCOMPARE(info["methods"].toArray(), expected); } @@ -506,9 +509,7 @@ void TestWebChannel::testInvokeMethodConversion() args.append(QJsonValue(1000)); { - int method = metaObject()->indexOfMethod("setInt(int)"); - QVERIFY(method != -1); - channel.d_func()->publisher->invokeMethod(this, method, args); + channel.d_func()->publisher->invokeMethod(this, "setInt", args); QCOMPARE(m_lastInt, args.at(0).toInt()); int getterMethod = metaObject()->indexOfMethod("readInt()"); QVERIFY(getterMethod != -1); @@ -516,11 +517,9 @@ void TestWebChannel::testInvokeMethodConversion() QCOMPARE(retVal, args.at(0).toVariant()); } { - int method = metaObject()->indexOfMethod("setBool(bool)"); - QVERIFY(method != -1); QJsonArray args; args.append(QJsonValue(!m_lastBool)); - channel.d_func()->publisher->invokeMethod(this, method, args); + channel.d_func()->publisher->invokeMethod(this, "setBool", args); QCOMPARE(m_lastBool, args.at(0).toBool()); int getterMethod = metaObject()->indexOfMethod("readBool()"); QVERIFY(getterMethod != -1); @@ -528,9 +527,7 @@ void TestWebChannel::testInvokeMethodConversion() QCOMPARE(retVal, args.at(0).toVariant()); } { - int method = metaObject()->indexOfMethod("setDouble(double)"); - QVERIFY(method != -1); - channel.d_func()->publisher->invokeMethod(this, method, args); + channel.d_func()->publisher->invokeMethod(this, "setDouble", args); QCOMPARE(m_lastDouble, args.at(0).toDouble()); int getterMethod = metaObject()->indexOfMethod("readDouble()"); QVERIFY(getterMethod != -1); @@ -538,9 +535,7 @@ void TestWebChannel::testInvokeMethodConversion() QCOMPARE(retVal, args.at(0).toVariant()); } { - int method = metaObject()->indexOfMethod("setVariant(QVariant)"); - QVERIFY(method != -1); - channel.d_func()->publisher->invokeMethod(this, method, args); + channel.d_func()->publisher->invokeMethod(this, "setVariant", args); QCOMPARE(m_lastVariant, args.at(0).toVariant()); int getterMethod = metaObject()->indexOfMethod("readVariant()"); QVERIFY(getterMethod != -1); @@ -548,9 +543,7 @@ void TestWebChannel::testInvokeMethodConversion() QCOMPARE(retVal, args.at(0).toVariant()); } { - int method = metaObject()->indexOfMethod("setJsonValue(QJsonValue)"); - QVERIFY(method != -1); - channel.d_func()->publisher->invokeMethod(this, method, args); + channel.d_func()->publisher->invokeMethod(this, "setJsonValue", args); QCOMPARE(m_lastJsonValue, args.at(0)); int getterMethod = metaObject()->indexOfMethod("readJsonValue()"); QVERIFY(getterMethod != -1); @@ -558,13 +551,11 @@ void TestWebChannel::testInvokeMethodConversion() QCOMPARE(retVal, args.at(0).toVariant()); } { - int method = metaObject()->indexOfMethod("setJsonObject(QJsonObject)"); - QVERIFY(method != -1); QJsonObject object; object["foo"] = QJsonValue(123); object["bar"] = QJsonValue(4.2); args[0] = object; - channel.d_func()->publisher->invokeMethod(this, method, args); + channel.d_func()->publisher->invokeMethod(this, "setJsonObject", args); QCOMPARE(m_lastJsonObject, object); int getterMethod = metaObject()->indexOfMethod("readJsonObject()"); QVERIFY(getterMethod != -1); @@ -572,13 +563,11 @@ void TestWebChannel::testInvokeMethodConversion() QCOMPARE(retVal, QVariant::fromValue(object)); } { - int setterMethod = metaObject()->indexOfMethod("setJsonArray(QJsonArray)"); - QVERIFY(setterMethod != -1); QJsonArray array; array << QJsonValue(123); array << QJsonValue(4.2); args[0] = array; - channel.d_func()->publisher->invokeMethod(this, setterMethod, args); + channel.d_func()->publisher->invokeMethod(this, "setJsonArray", args); QCOMPARE(m_lastJsonArray, array); int getterMethod = metaObject()->indexOfMethod("readJsonArray()"); QVERIFY(getterMethod != -1); @@ -674,6 +663,49 @@ void TestWebChannel::testSetPropertyConversion() } } +void TestWebChannel::testInvokeMethodOverloadResolution() +{ + QWebChannel channel; + TestObject testObject; + TestObject exportedObject; + channel.registerObject("test", &exportedObject); + channel.connectTo(m_dummyTransport); + + QVariant result; + QMetaObjectPublisher *publisher = channel.d_func()->publisher; + + { + result = publisher->invokeMethod(&testObject, "overload", { 41.0 }); + QVERIFY(result.userType() == QMetaType::Double); + QCOMPARE(result.toDouble(), 42.0); + } + { + // In JavaScript, there's only 'double', so this should always invoke the 'double' overload + result = publisher->invokeMethod(&testObject, "overload", { 41 }); + QVERIFY(result.userType() == QMetaType::Double); + QCOMPARE(result.toDouble(), 42); + } + { + QJsonObject wrappedObject { {"id", "test"} }; + result = publisher->invokeMethod(&testObject, "overload", { wrappedObject }); + QCOMPARE(result.value<TestObject*>(), &exportedObject); + } + { + result = publisher->invokeMethod(&testObject, "overload", { "hello world" }); + QCOMPARE(result.toString(), QStringLiteral("HELLO WORLD")); + } + { + result = publisher->invokeMethod(&testObject, "overload", { "the answer is ", 41 }); + QCOMPARE(result.toString(), QStringLiteral("THE ANSWER IS 42")); + } + { + QJsonArray args; + args.append(QJsonArray { "foobar", 42 }); + result = publisher->invokeMethod(&testObject, "overload", args); + QCOMPARE(result.toString(), QStringLiteral("42foobar")); + } +} + void TestWebChannel::testDisconnect() { QWebChannel channel; @@ -771,7 +803,7 @@ void TestWebChannel::testPassWrappedObjectBack() QJsonObject argProperty; argProperty["id"] = returnedObjPropertyInfo["id"]; - pub->invokeMethod(®isteredObj, registeredObj.metaObject()->indexOfSlot("setReturnedObject(TestObject*)"), argsMethod); + pub->invokeMethod(®isteredObj, "setReturnedObject", argsMethod); QCOMPARE(registeredObj.mReturnedObject, &returnedObjMethod); pub->setProperty(®isteredObj, registeredObj.metaObject()->indexOfProperty("returnedObject"), argProperty); QCOMPARE(registeredObj.mReturnedObject, &returnedObjProperty); @@ -858,12 +890,9 @@ void TestWebChannel::testAsyncObject() QJsonArray args; args.append(QJsonValue("message")); - int method = obj.metaObject()->indexOfMethod("setProp(QString)"); - QVERIFY(method != -1); - { QSignalSpy spy(&obj, &TestObject::propChanged); - channel.d_func()->publisher->invokeMethod(&obj, method, args); + channel.d_func()->publisher->invokeMethod(&obj, "setProp", args); QTRY_COMPARE(spy.count(), 1); QCOMPARE(spy.at(0).at(0).toString(), args.at(0).toString()); } diff --git a/tests/auto/webchannel/tst_webchannel.h b/tests/auto/webchannel/tst_webchannel.h index ed769e9..8ca1cdd 100644 --- a/tests/auto/webchannel/tst_webchannel.h +++ b/tests/auto/webchannel/tst_webchannel.h @@ -158,9 +158,12 @@ public slots: void setProp(const QString&prop) {emit propChanged(mProp=prop);} void fire() {emit replay();} - int overload(int i) { return i + 1; } + double overload(double d) { return d + 1; } + int overload(int i) { return i * 2; } + QObject *overload(QObject *object) { return object; } QString overload(const QString &str) { return str.toUpper(); } QString overload(const QString &str, int i) { return str.toUpper() + QString::number(i + 1); } + QString overload(const QJsonArray &v) { return QString::number(v[1].toInt()) + v[0].toString(); } protected slots: void slot3() {} @@ -320,6 +323,7 @@ private slots: void testInvokeMethodConversion(); void testFunctionOverloading(); void testSetPropertyConversion(); + void testInvokeMethodOverloadResolution(); void testDisconnect(); void testWrapRegisteredObject(); void testUnwrapObject(); |