summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorArno Rehn <a.rehn@menlosystems.com>2019-01-15 18:07:18 +0100
committerMilian Wolff <milian.wolff@kdab.com>2019-03-29 21:06:14 +0000
commitd91fd4fc4e57a22e1d268e4602017f629cfccf46 (patch)
tree9a50d8d1fc4320008a62e5d6cc4b81ee18eeabf5 /src
parent984c4e6b4dd05561bb39f6daf305e520dfa9f0e6 (diff)
downloadqtwebchannel-d91fd4fc4e57a22e1d268e4602017f629cfccf46.tar.gz
Implement actual overload resolution
This implements host-side overload resolution. If a client invokes a method by its name instead of its id, the overload resolution tries to find the best match for the given arguments. The JavaScript client implementation now defaults to invocation-by-name, except when a method is invoked by its full signature. In that case, the invocation is still performed by method id. Change-Id: I09f12bdbfee2e84ff66a1454608468113f96e3ed Reviewed-by: Milian Wolff <milian.wolff@kdab.com>
Diffstat (limited to 'src')
-rw-r--r--src/webchannel/doc/src/javascript.qdoc20
-rw-r--r--src/webchannel/qmetaobjectpublisher.cpp183
-rw-r--r--src/webchannel/qmetaobjectpublisher_p.h38
3 files changed, 227 insertions, 14 deletions
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);