summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorMilian Wolff <milian.wolff@kdab.com>2013-11-21 13:11:03 +0100
committerMilian Wolff <milian.wolff@kdab.com>2013-12-11 13:08:40 +0100
commitacf7f0b1ae956f2fac7182c194e0441cd9c6f4d0 (patch)
tree21420faece9a6c1eca68727c916660c9a286c912 /src
parentb6158dc3525c1c906d4040b2e88cd20feb21a2b2 (diff)
downloadqtwebchannel-acf7f0b1ae956f2fac7182c194e0441cd9c6f4d0.tar.gz
Port the MetaObjectPublisher to C++.
This will allow us to create a stand-alone WebChannel C++ library, without any QML dependencies, that can be used to publisher QObjects to remote clients running in any browser engine supporting WebSockets. The patch is large, as working with introspection through the QMetaObject from C++ is more complicated compared to QML. On the other hand, the move to C++ allows a much more performant implementation of quite some parts of the publisher. One thing is that signal and method invocations can be handled via numeric indices, where before we needed to transmit the string identifier of the signal or method. Eventually this can now also be applied to properties, further decreasing the size of messages between server and clients. Note that this patch contains quite some TODOs and rough edges, such as the invokable bench_* helper functions in the public API of the MetaObjectPublisher. These are to be seen as temporary, and will be cleaned up in followup commits later. This is done to prevent a further blow-up of this already big patch. Change-Id: I57e788d8a19edd410651611382d912f9ad6660c9 Reviewed-by: Simon Hausmann <simon.hausmann@digia.com>
Diffstat (limited to 'src')
-rw-r--r--src/MetaObjectPublisher.qml384
-rw-r--r--src/qmldir1
-rw-r--r--src/qobject.js53
-rw-r--r--src/qtmetaobjectpublisher.cpp629
-rw-r--r--src/qtmetaobjectpublisher.h73
-rw-r--r--src/qwebchannel_plugin.cpp2
-rw-r--r--src/signalhandler.h278
-rw-r--r--src/src.pri4
-rw-r--r--src/src.pro6
-rw-r--r--src/variantargument.h103
10 files changed, 1041 insertions, 492 deletions
diff --git a/src/MetaObjectPublisher.qml b/src/MetaObjectPublisher.qml
deleted file mode 100644
index 8d93fc4..0000000
--- a/src/MetaObjectPublisher.qml
+++ /dev/null
@@ -1,384 +0,0 @@
-/****************************************************************************
-**
-** Copyright (C) 2011 Nokia Corporation and/or its subsidiary(-ies).
-** All rights reserved.
-** Contact: Nokia Corporation (qt-info@nokia.com)
-**
-** Copyright (C) 2013 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author Milian Wolff <milian.wolff@kdab.com>
-** Contact: http://www.qt-project.org/legal
-**
-** This file is part of the QWebChannel module on Qt labs.
-**
-** $QT_BEGIN_LICENSE:LGPL$
-** No Commercial Usage
-** This file contains pre-release code and may not be distributed.
-** You may use this file in accordance with the terms and conditions
-** contained in the Technology Preview License Agreement accompanying
-** this package.
-**
-** GNU Lesser General Public License Usage
-** Alternatively, this file may be used under the terms of the GNU Lesser
-** General Public License version 2.1 as published by the Free Software
-** Foundation and appearing in the file LICENSE.LGPL included in the
-** packaging of this file. Please review the following information to
-** ensure the GNU Lesser General Public License version 2.1 requirements
-** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
-**
-** In addition, as a special exception, Nokia gives you certain additional
-** rights. These rights are described in the Nokia Qt LGPL Exception
-** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
-**
-** If you have questions regarding the use of this file, please contact
-** Nokia at qt-info@nokia.com.
-**
-**
-**
-**
-**
-**
-**
-**
-** $QT_END_LICENSE$
-**
-****************************************************************************/
-
-import QtQuick 2.0
-import Qt.labs.WebChannel 1.0
-
-MetaObjectPublisherImpl
-{
- id: publisher
-
- // The web channel this publisher works on.
- property var webChannel
-
- /**
- * This map contains the registered objects indexed by their name.
- */
- property variant registeredObjects: ({})
- /**
- * Tracks how many connections are active to object signals.
- *
- * Maps objectName -> signalName -> {handler: functor, subscribers: int}.
- */
- property var subscriberCountMap: ({})
-
- // Map of object names to maps of signal names to an array of all their properties.
- // The last value is an array as a signal can be the notify signal of multiple properties.
- property var signalToPropertyMap: ({})
-
- // Objects that changed their properties and are waiting for idle client.
- // map of object name to map of signal name to arguments
- property var pendingPropertyUpdates: ({})
-
- // true when the client is idle, false otherwise
- property bool clientIsIdle: false
-
- // true when no property updates should be sent, false otherwise
- property bool blockUpdates: false
-
- // true when at least one client needs to be initialized,
- // i.e. when a Qt.init came in which was not handled yet.
- property bool pendingInit: false
-
- // true when at least one client was initialized and thus
- // the property updates have been initialized and the
- // object info map set.
- property bool propertyUpdatesInitialized: false
-
- /**
- * Wrap a result value if it's a Qt QObject
- *
- * @return object info for wrapped Qt Object,
- * or the same value if no wrapping needed
- *
- */
- function wrapResult(result)
- {
- if (typeof(result) === "object"
- && result["objectName"] !== undefined)
- {
- var ret = wrapObject(result);
- initializePropertyUpdates(ret.id, ret.data, result, webChannel);
- return ret;
- }
- return result;
- }
-
- function convertQMLArgsToJSArgs(qmlArgs)
- {
- // NOTE: QML arguments is a map not an array it seems...
- // so do the conversion manually
- var args = [];
- for (var i = 0; i < qmlArgs.length; ++i) {
- args.push(qmlArgs[i]);
- }
- return args;
- }
-
- /**
- * Handle the given WebChannel client request and potentially give a response.
- *
- * @return true if the request was handled, false otherwise.
- */
- function handleRequest(data)
- {
- var message = typeof(data) === "string" ? JSON.parse(data) : data;
- if (!message.data) {
- return false;
- }
- var payload = message.data;
- if (!payload.type) {
- return false;
- }
-
- if (payload.object) {
- var isWrapped = false;
- var object = registeredObjects[payload.object];
- if (!object) {
- object = unwrapObject(payload.object);
- if (object)
- isWrapped = true;
- else
- return false
- }
-
- if (payload.type === "Qt.invokeMethod") {
- var method = object[payload.method];
- if (method !== undefined) {
- webChannel.respond(message.id,
- wrapResult(method.apply(method, payload.args)));
- return true;
- }
- if (isWrapped && payload.method === "deleteLater") {
- // invoke `deleteLater` on wrapped QObject indirectly
- deleteWrappedObject(object);
- return true;
- }
- return false;
- }
- if (payload.type === "Qt.connectToSignal") {
- if (object.hasOwnProperty(payload.signal)) {
- subscriberCountMap[payload.object] = subscriberCountMap[payload.object] || {};
-
- // if no one is connected, connect.
- if (!subscriberCountMap[payload.object].hasOwnProperty(payload.signal)) {
- subscriberCountMap[payload.object][payload.signal] = {
- subscribers: 1,
- handler: function() {
- var args = convertQMLArgsToJSArgs(arguments);
- webChannel.sendMessage("Qt.signal", {
- object: payload.object,
- signal: payload.signal,
- args: args
- });
- }
- };
- object[payload.signal].connect(subscriberCountMap[payload.object][payload.signal].handler);
- } else {
- ++subscriberCountMap[payload.object][payload.signal].subscribers;
- }
- return true;
- }
- // connecting to `destroyed` signal of wrapped QObject
- if (isWrapped && payload.signal === "destroyed") {
- // is a no-op on this side
- return true;
- }
- return false;
- }
- if (payload.type === "Qt.disconnectFromSignal") {
- if (!object.hasOwnProperty(payload.signal)) {
- return false;
- }
- subscriberCountMap[payload.object] = subscriberCountMap[payload.object] || {};
-
- if (!subscriberCountMap[payload.object].hasOwnProperty(payload.signal)) {
- return false
- }
- if (--subscriberCountMap[payload.object][payload.signal].subscribers === 0) {
- object[payload.signal].disconnect(subscriberCountMap[payload.object][payload.signal].handler);
- delete subscriberCountMap[payload.object][payload.signal];
- }
- return true;
- }
- if (payload.type === "Qt.setProperty") {
- object[payload.property] = payload.value;
- return true;
- }
- }
- if (payload.type === "Qt.idle") {
- clientIsIdle = true;
- return true;
- }
- if (payload.type === "Qt.init") {
- if (!blockUpdates) {
- initializeClients();
- } else {
- pendingInit = true;
- }
- return true;
- }
- if (payload.type === "Qt.Debug") {
- console.log("DEBUG: ", payload.message);
- return true;
- }
- return false;
- }
-
- function registerObjects(objects)
- {
- if (propertyUpdatesInitialized) {
- console.error("Registered new object after initialization. This does not work!");
- }
- // joining a JS map and a QML one is not as easy as one would assume...
- // NOTE: the extra indirection via "merged" is required, using registeredObjects directly
- // does not work! this looks like a QML/v8 bug to me, but I could not find a
- // standalone testcase which reproduces this behavior :(
- var merged = registeredObjects;
- for (var name in objects) {
- if (!merged.hasOwnProperty(name)) {
- merged[name] = objects[name];
- }
- }
- registeredObjects = merged;
- }
-
- function initializeClients()
- {
- var objectInfos = classInfoForObjects(registeredObjects);
- webChannel.sendMessage("Qt.init", objectInfos);
- if (!propertyUpdatesInitialized) {
- for (var objectName in objectInfos) {
- var objectInfo = objectInfos[objectName];
- var object = registeredObjects[objectName];
- initializePropertyUpdates(objectName, objectInfo, object);
- }
- propertyUpdatesInitialized = true;
- }
- pendingInit = false;
- }
-
- // This function goes through all properties of all objects and connects against
- // their notify signal.
- // When receiving a notify signal, it will send a Qt.propertyUpdate message to the
- // server.
- function initializePropertyUpdates(objectName, objectInfo, object)
- {
- for (var propertyIndex in objectInfo.properties) {
- var propertyInfo = objectInfo.properties[propertyIndex];
- var propertyName = propertyInfo[0];
- var signalName = propertyInfo[1];
-
- if (!signalName) // Property without NOTIFY signal
- continue;
-
- if (signalName === 1) {
- /// signal name is optimized away, reconstruct the actual name
- signalName = propertyName + "Changed";
- }
-
- signalToPropertyMap[objectName] = signalToPropertyMap[objectName] || {};
- signalToPropertyMap[objectName][signalName] = signalToPropertyMap[objectName][signalName] || [];
- var connectedProperties = signalToPropertyMap[objectName][signalName];
- var numConnectedProperties = connectedProperties === undefined ? 0 : connectedProperties.length;
-
- // Only connect for a property update once
- if (numConnectedProperties === 0) {
- (function(signalName) {
- object[signalName].connect(function() {
- pendingPropertyUpdates[objectName] = pendingPropertyUpdates[objectName] || {};
- pendingPropertyUpdates[objectName][signalName] = arguments;
- });
- })(signalName);
- }
-
- if (connectedProperties.indexOf(propertyName) === -1) {
- /// TODO: this ensures that a given property is only once in
- /// the list of connected properties.
- /// This happens when multiple clients are connected to
- /// a single webchannel. A better place for the initialization
- /// should be found.
- connectedProperties.push(propertyName);
- }
- }
- }
-
- function sendPendingPropertyUpdates()
- {
- if (blockUpdates || !clientIsIdle) {
- return;
- }
-
- var data = [];
- for (var objectName in pendingPropertyUpdates) {
- var object = registeredObjects[objectName];
- if (!object) {
- object = unwrapObject(objectName);
- if (!object) {
- console.error("Got property update for unknown object " + objectName);
- continue;
- }
- }
- var signals = pendingPropertyUpdates[objectName];
- var propertyMap = {};
- for (var signalName in signals) {
- var propertyList = signalToPropertyMap[objectName][signalName];
- for (var propertyIndex in propertyList) {
- var propertyName = propertyList[propertyIndex];
- var propertyValue = object[propertyName];
- propertyMap[propertyName] = propertyValue;
- }
- signals[signalName] = convertQMLArgsToJSArgs(signals[signalName]);
- }
- data.push({
- object: objectName,
- signals: signals,
- propertyMap: propertyMap
- });
- }
- pendingPropertyUpdates = {};
- if (data.length > 0) {
- webChannel.sendMessage("Qt.propertyUpdate", data);
- clientIsIdle = false;
- }
- }
-
- onBlockUpdatesChanged: {
- if (blockUpdates) {
- return;
- }
-
- if (pendingInit) {
- initializeClients();
- } else {
- sendPendingPropertyUpdates();
- }
- }
-
- onWrappedObjectDestroyed: { // (const QString& id)
- // act as if object had sent `destroyed` signal
- webChannel.sendMessage("Qt.signal", {
- object: id,
- signal: "destroyed",
- args: []
- });
- delete subscriberCountMap[id];
- delete pendingPropertyUpdates[id];
- delete signalToPropertyMap[id]
- }
-
- /**
- * Aggregate property updates since we get multiple Qt.idle message when we have multiple
- * clients. They all share the same QWebProcess though so we must take special care to
- * prevent message flooding.
- */
- Timer {
- id: propertyUpdateTimer
- /// TODO: what is the proper value here?
- interval: 50;
- running: !publisher.blockUpdates && publisher.clientIsIdle;
- repeat: true;
- onTriggered: publisher.sendPendingPropertyUpdates()
- }
-}
diff --git a/src/qmldir b/src/qmldir
index f0b0992..ebd912a 100644
--- a/src/qmldir
+++ b/src/qmldir
@@ -1,3 +1,2 @@
module Qt.labs.WebChannel
plugin qwebchannel
-MetaObjectPublisher 1.0 MetaObjectPublisher.qml
diff --git a/src/qobject.js b/src/qobject.js
index 27cb1e5..b783a5b 100644
--- a/src/qobject.js
+++ b/src/qobject.js
@@ -4,6 +4,9 @@
** All rights reserved.
** Contact: Nokia Corporation (qt-info@nokia.com)
**
+** Copyright (C) 2013 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author Milian Wolff <milian.wolff@kdab.com>
+** Contact: http://www.qt-project.org/legal
+**
** This file is part of the QWebChannel module on Qt labs.
**
** $QT_BEGIN_LICENSE:LGPL$
@@ -82,45 +85,47 @@ function QObject(name, data, webChannel)
return qObject;
}
- function addSignal(signal, isPropertyNotifySignal)
+ function addSignal(signalData, isPropertyNotifySignal)
{
- object[signal] = {
+ var signalName = signalData[0];
+ var signalIndex = signalData[1];
+ object[signalName] = {
connect: function(callback) {
if (typeof(callback) !== "function") {
- console.error("Bad callback given to connect to signal " + signal);
+ console.error("Bad callback given to connect to signal " + signalName);
return;
}
- object.__objectSignals__[signal] = object.__objectSignals__[signal] || [];
- object.__objectSignals__[signal].push(callback);
+ object.__objectSignals__[signalIndex] = object.__objectSignals__[signalIndex] || [];
+ object.__objectSignals__[signalIndex].push(callback);
if (!isPropertyNotifySignal) {
// only required for "pure" signals, handled separately for properties in propertyUpdate
webChannel.exec({
type: "Qt.connectToSignal",
object: object.__id__,
- signal: signal
+ signal: signalIndex
});
}
},
disconnect: function(callback) {
if (typeof(callback) !== "function") {
- console.error("Bad callback given to disconnect from signal " + signal);
+ console.error("Bad callback given to disconnect from signal " + signalName);
return;
}
- object.__objectSignals__[signal] = object.__objectSignals__[signal] || [];
- var idx = object.__objectSignals__[signal].indexOf(callback);
+ object.__objectSignals__[signalIndex] = object.__objectSignals__[signalIndex] || [];
+ var idx = object.__objectSignals__[signalIndex].indexOf(callback);
if (idx === -1) {
- console.error("Cannot find connection for given callback to signal" + signal, callback);
+ console.error("Cannot find connection of signal " + signalName + " to " + callback.name);
return;
}
- object.__objectSignals__[signal].splice(idx, 1);
- if (!isPropertyNotifySignal && object.__objectSignals__[signal].length === 0) {
+ object.__objectSignals__[signalIndex].splice(idx, 1);
+ if (!isPropertyNotifySignal && object.__objectSignals__[signalIndex].length === 0) {
// only required for "pure" signals, handled separately for properties in propertyUpdate
webChannel.exec({
type: "Qt.disconnectFromSignal",
object: object.__id__,
- signal: signal
+ signal: signalIndex
});
}
}
@@ -160,9 +165,11 @@ function QObject(name, data, webChannel)
invokeSignalCallbacks(signalName, signalArgs);
}
- function addMethod(method)
+ function addMethod(methodData)
{
- object[method] = function() {
+ var methodName = methodData[0];
+ var methodIdx = methodData[1];
+ object[methodName] = function() {
var args = [];
var callback;
for (var i = 0; i < arguments.length; ++i) {
@@ -172,7 +179,7 @@ function QObject(name, data, webChannel)
args.push(arguments[i]);
}
- webChannel.exec({"type": "Qt.invokeMethod", "object": object.__id__, "method": method, "args": args}, function(response) {
+ webChannel.exec({"type": "Qt.invokeMethod", "object": object.__id__, "method": methodIdx, "args": args}, function(response) {
if ( (response !== undefined) && callback ) {
(callback)(unwrapQObject(response));
}
@@ -183,16 +190,16 @@ function QObject(name, data, webChannel)
function bindGetterSetter(propertyInfo)
{
var propertyName = propertyInfo[0];
- var notifySignal = propertyInfo[1];
+ var notifySignalData = propertyInfo[1];
// initialize property cache with current value
object.__propertyCache__[propertyName] = propertyInfo[2]
- if (notifySignal) {
- if (notifySignal === 1) {
- /// signal name is optimized away, reconstruct the actual name
- notifySignal = propertyName + "Changed";
+ if (notifySignalData) {
+ if (notifySignalData[0] === 1) {
+ // signal name is optimized away, reconstruct the actual name
+ notifySignalData[0] = propertyName + "Changed";
}
- addSignal(notifySignal, true);
+ addSignal(notifySignalData, true);
}
object.__defineSetter__(propertyName, function(value) {
@@ -263,7 +270,7 @@ window.setupQObjectWebChannel = function(webChannel, doneCallback)
var data = payload[i];
var object = window[data.object] || webChannel.objectMap[data.object];
if (object) {
- object.propertyUpdate(data.signals, data.propertyMap);
+ object.propertyUpdate(data.signals, data.properties);
} else {
console.warn("Unhandled property update: " + data.object + "::" + data.signal);
}
diff --git a/src/qtmetaobjectpublisher.cpp b/src/qtmetaobjectpublisher.cpp
index 511f514..9ca0d93 100644
--- a/src/qtmetaobjectpublisher.cpp
+++ b/src/qtmetaobjectpublisher.cpp
@@ -43,24 +43,433 @@
****************************************************************************/
#include "qtmetaobjectpublisher.h"
+#include "qwebchannel.h"
+#include "variantargument.h"
+#include "signalhandler.h"
#include <QStringList>
#include <QMetaObject>
-#include <QMetaProperty>
+#include <QJsonObject>
+#include <QJsonArray>
+#include <QBasicTimer>
#include <QDebug>
+#include <QPointer>
+#include <QEvent>
-static const QString KEY_SIGNALS = QStringLiteral("signals");
-static const QString KEY_METHODS = QStringLiteral("methods");
-static const QString KEY_PROPERTIES = QStringLiteral("properties");
-static const QString KEY_ENUMS = QStringLiteral("enums");
+namespace {
+const QString KEY_SIGNALS = QStringLiteral("signals");
+const QString KEY_METHODS = QStringLiteral("methods");
+const QString KEY_PROPERTIES = QStringLiteral("properties");
+const QString KEY_ENUMS = QStringLiteral("enums");
+const QString KEY_QOBJECT = QStringLiteral("__QObject*__");
+const QString KEY_ID = QStringLiteral("id");
+const QString KEY_DATA = QStringLiteral("data");
+const QString KEY_OBJECT = QStringLiteral("object");
+const QString KEY_DESTROYED = QStringLiteral("destroyed");
+const QString KEY_SIGNAL = QStringLiteral("signal");
+const QString KEY_TYPE = QStringLiteral("type");
+const QString KEY_MESSAGE = QStringLiteral("message");
+const QString KEY_METHOD = QStringLiteral("method");
+const QString KEY_ARGS = QStringLiteral("args");
+const QString KEY_PROPERTY = QStringLiteral("property");
+const QString KEY_VALUE = QStringLiteral("value");
-static const QString KEY_QOBJECT = QStringLiteral("__QObject*__");
-static const QString KEY_ID = QStringLiteral("id");
-static const QString KEY_DATA = QStringLiteral("data");
+const QString TYPE_SIGNAL = QStringLiteral("Qt.signal");
+const QString TYPE_PROPERTY_UPDATE = QStringLiteral("Qt.propertyUpdate");
+const QString TYPE_INIT = QStringLiteral("Qt.init");
+const QString TYPE_IDLE = QStringLiteral("Qt.idle");
+const QString TYPE_DEBUG = QStringLiteral("Qt.debug");
+const QString TYPE_INVOKE_METHOD = QStringLiteral("Qt.invokeMethod");
+const QString TYPE_CONNECT_TO_SIGNAL = QStringLiteral("Qt.connectToSignal");
+const QString TYPE_DISCONNECT_FROM_SIGNAL = QStringLiteral("Qt.disconnectFromSignal");
+const QString TYPE_SET_PROPERTY = QStringLiteral("Qt.setProperty");
-QtMetaObjectPublisher::QtMetaObjectPublisher(QQuickItem *parent)
- : QQuickItem(parent)
+QString objectId(const QObject *object)
{
+ return QString::number(quintptr(object), 16);
+}
+
+const int s_destroyedSignalIndex = QObject::staticMetaObject.indexOfMethod("destroyed(QObject*)");
+
+/// TODO: what is the proper value here?
+const int PROPERTY_UPDATE_INTERVAL = 50;
+}
+
+struct QtMetaObjectPublisherPrivate
+{
+ QtMetaObjectPublisherPrivate(QtMetaObjectPublisher *q)
+ : q(q)
+ , signalHandler(this)
+ , clientIsIdle(false)
+ , blockUpdates(false)
+ , pendingInit(false)
+ , propertyUpdatesInitialized(false)
+ {
+ }
+
+ /**
+ * Set the client to idle or busy, based on the value of @p isIdle.
+ *
+ * When the value changed, start/stop the property update timer accordingly.
+ */
+ void setClientIsIdle(bool isIdle);
+
+ /**
+ * Initialize clients by sending them the class information of the registered objects.
+ *
+ * Furthermore, if that was not done already, connect to their property notify signals.
+ */
+ void initializeClients();
+
+ /**
+ * Go through all properties of the given object and connect to their notify signal.
+ *
+ * When receiving a notify signal, it will store the information in pendingPropertyUpdates which
+ * gets send via a Qt.propertyUpdate message to the server when the grouping timer timeouts.
+ */
+ void initializePropertyUpdates(const QObject *const object, const QVariantMap &objectInfo);
+
+ /**
+ * Send the clients the new property values since the last time this function was invoked.
+ *
+ * This is a grouped batch of all properties for which their notify signal was emitted.
+ * The list of signals as well as the arguments they contained, are also transmitted to
+ * the remote clients.
+ *
+ * @sa timer, initializePropertyUpdates
+ */
+ void sendPendingPropertyUpdates();
+
+ /**
+ * Invoke the method of index @p methodIndex on @p object with the arguments @p args.
+ *
+ * The return value of the method invocation is then transmitted to the calling client
+ * via a webchannel response to the message identified by @p id.
+ */
+ bool invokeMethod(QObject *const object, const int methodIndex, const QJsonArray &args, const QJsonValue &id);
+
+ /**
+ * Callback of the signalHandler which forwards the signal invocation to the webchannel clients.
+ */
+ void signalEmitted(const QObject *object, const int signalIndex, const QVariantList &arguments);
+
+ /**
+ * Callback for registered or wrapped objects which erases all data related to @p object.
+ *
+ * @sa signalEmitted
+ */
+ void objectDestroyed(const QObject *object);
+
+ /**
+ * Given a QVariant containing a QObject*, wrap the object and register for property updates
+ * return the objects class information.
+ *
+ * All other input types are returned as-is.
+ *
+ * TODO: support wrapping of initially-registered objects
+ */
+ QVariant wrapResult(const QVariant &result);
+
+ /**
+ * Invoke delete later on @p object.
+ */
+ void deleteWrappedObject(QObject *object) const;
+
+ QtMetaObjectPublisher *q;
+ QPointer<QWebChannel> webChannel;
+ SignalHandler<QtMetaObjectPublisherPrivate> signalHandler;
+
+ // true when the client is idle, false otherwise
+ bool clientIsIdle;
+
+ // true when no property updates should be sent, false otherwise
+ bool blockUpdates;
+
+ // true when at least one client needs to be initialized,
+ // i.e. when a Qt.init came in which was not handled yet.
+ bool pendingInit;
+
+ // true when at least one client was initialized and thus
+ // the property updates have been initialized and the
+ // object info map set.
+ bool propertyUpdatesInitialized;
+
+ // Map of registered objects indexed by their id.
+ QHash<QString, QObject *> registeredObjects;
+
+ // Map the registered objects to their id.
+ QHash<const QObject *, QString> registeredObjectIds;
+
+ // Map of object names to maps of signal indices to a set of all their properties.
+ // The last value is a set as a signal can be the notify signal of multiple properties.
+ typedef QHash<int, QSet<QString> > SignalToPropertyNameMap;
+ QHash<const QObject *, SignalToPropertyNameMap> signalToPropertyMap;
+
+ // Objects that changed their properties and are waiting for idle client.
+ // map of object name to map of signal index to arguments
+ typedef QHash<int, QVariantList> SignalToArgumentsMap;
+ typedef QHash<const QObject *, SignalToArgumentsMap> PendingPropertyUpdates;
+ PendingPropertyUpdates pendingPropertyUpdates;
+
+ // Maps wrapped object to class info
+ QHash<const QObject *, QVariantMap> wrappedObjects;
+
+ // Aggregate property updates since we get multiple Qt.idle message when we have multiple
+ // clients. They all share the same QWebProcess though so we must take special care to
+ // prevent message flooding.
+ QBasicTimer timer;
+};
+
+void QtMetaObjectPublisherPrivate::setClientIsIdle(bool isIdle)
+{
+ if (clientIsIdle == isIdle) {
+ return;
+ }
+ clientIsIdle = isIdle;
+ if (!isIdle && timer.isActive()) {
+ timer.stop();
+ } else if (isIdle && !timer.isActive()) {
+ timer.start(PROPERTY_UPDATE_INTERVAL, q);
+ }
+}
+
+void QtMetaObjectPublisherPrivate::initializeClients()
+{
+ QJsonObject objectInfos;
+ {
+ const QHash<QString, QObject *>::const_iterator end = registeredObjects.constEnd();
+ for (QHash<QString, QObject *>::const_iterator it = registeredObjects.constBegin(); it != end; ++it) {
+ const QVariantMap &info = q->classInfoForObject(it.value());
+ if (!propertyUpdatesInitialized) {
+ initializePropertyUpdates(it.value(), info);
+ }
+ objectInfos[it.key()] = QJsonObject::fromVariantMap(info);
+ }
+ }
+ webChannel->sendMessage(TYPE_INIT, objectInfos);
+ propertyUpdatesInitialized = true;
+ pendingInit = false;
+}
+
+void QtMetaObjectPublisherPrivate::initializePropertyUpdates(const QObject *const object, const QVariantMap &objectInfo)
+{
+ foreach (const QVariant &propertyInfoVar, objectInfo[KEY_PROPERTIES].toList()) {
+ const QVariantList &propertyInfo = propertyInfoVar.toList();
+ if (propertyInfo.size() < 2) {
+ qWarning() << "Invalid property info encountered:" << propertyInfoVar;
+ continue;
+ }
+ const QString &propertyName = propertyInfo.at(0).toString();
+ const QVariantList &signalData = propertyInfo.at(1).toList();
+
+ if (signalData.isEmpty()) {
+ // Property without NOTIFY signal
+ continue;
+ }
+
+ const int signalIndex = signalData.at(1).toInt();
+
+ QSet<QString> &connectedProperties = signalToPropertyMap[object][signalIndex];
+
+ // Only connect for a property update once
+ if (connectedProperties.isEmpty()) {
+ signalHandler.connectTo(object, signalIndex);
+ }
+
+ connectedProperties.insert(propertyName);
+ }
+
+ // also always connect to destroyed signal
+ signalHandler.connectTo(object, s_destroyedSignalIndex);
+}
+
+void QtMetaObjectPublisherPrivate::sendPendingPropertyUpdates()
+{
+ if (blockUpdates || !clientIsIdle || pendingPropertyUpdates.isEmpty()) {
+ return;
+ }
+
+ QJsonArray data;
+
+ // convert pending property updates to JSON data
+ const PendingPropertyUpdates::const_iterator end = pendingPropertyUpdates.constEnd();
+ for (PendingPropertyUpdates::const_iterator it = pendingPropertyUpdates.constBegin(); it != end; ++it) {
+ const QObject *object = it.key();
+ const QMetaObject *const metaObject = object->metaObject();
+ const SignalToPropertyNameMap &objectsSignalToPropertyMap = signalToPropertyMap.value(object);
+ // maps property name to current property value
+ QJsonObject properties;
+ // maps signal index to list of arguments of the last emit
+ QJsonObject sigs;
+ const SignalToArgumentsMap::const_iterator sigEnd = it.value().constEnd();
+ for (SignalToArgumentsMap::const_iterator sigIt = it.value().constBegin(); sigIt != sigEnd; ++sigIt) {
+ // TODO: use property indices
+ foreach (const QString &propertyName, objectsSignalToPropertyMap.value(sigIt.key())) {
+ int propertyIndex = metaObject->indexOfProperty(qPrintable(propertyName));
+ if (propertyIndex == -1) {
+ qWarning("Unknown property %d encountered", propertyIndex);
+ continue;
+ }
+ const QMetaProperty &property = metaObject->property(propertyIndex);
+ properties[QString::fromLatin1(property.name())] = QJsonValue::fromVariant(property.read(object));
+ }
+ // TODO: can we get rid of the int <-> string conversions here?
+ sigs[QString::number(sigIt.key())] = QJsonArray::fromVariantList(sigIt.value());
+ }
+ QJsonObject obj;
+ obj[KEY_OBJECT] = registeredObjectIds.value(object);
+ obj[KEY_SIGNALS] = sigs;
+ obj[KEY_PROPERTIES] = properties;
+ data.push_back(obj);
+ }
+
+ pendingPropertyUpdates.clear();
+ webChannel->sendMessage(TYPE_PROPERTY_UPDATE, data);
+ setClientIsIdle(false);
+}
+
+bool QtMetaObjectPublisherPrivate::invokeMethod(QObject *const object, const int methodIndex,
+ const QJsonArray &args, const QJsonValue &id)
+{
+ const QMetaMethod &method = object->metaObject()->method(methodIndex);
+
+ if (method.name() == QByteArrayLiteral("deleteLater")) {
+ // invoke `deleteLater` on wrapped QObject indirectly
+ deleteWrappedObject(object);
+ return true;
+ } else if (!method.isValid()) {
+ qWarning() << "Cannot invoke unknown method of index" << methodIndex << "on object" << object << '.';
+ return false;
+ } else if (method.access() != QMetaMethod::Public) {
+ qWarning() << "Cannot invoke non-public method" << method.name() << "on object" << object << '.';
+ return false;
+ } else if (method.methodType() != QMetaMethod::Method && method.methodType() != QMetaMethod::Slot) {
+ qWarning() << "Cannot invoke non-public method" << method.name() << "on object" << object << '.';
+ return false;
+ } else if (args.size() > 10) {
+ qWarning() << "Cannot invoke method" << method.name() << "on object" << object << "with more than 10 arguments, as that is not supported by QMetaMethod::invoke.";
+ return false;
+ } else if (args.size() > method.parameterCount()) {
+ qWarning() << "Ignoring additional arguments while invoking method" << method.name() << "on object" << object << ':'
+ << args.size() << "arguments given, but method only takes" << method.parameterCount() << '.';
+ }
+
+ // construct converter objects of QVariant to QGenericArgument
+ VariantArgument arguments[10];
+ for (int i = 0; i < qMin(args.size(), method.parameterCount()); ++i) {
+ arguments[i].setValue(args.at(i).toVariant(), method.parameterType(i));
+ }
+
+ // construct QGenericReturnArgument
+ QVariant returnValue;
+ if (method.returnType() != qMetaTypeId<QVariant>() && method.returnType() != qMetaTypeId<void>()) {
+ // Only init variant with return type if its not a variant itself, which would
+ // lead to nested variants which is not what we want.
+ // Also, skip void-return types for obvious reasons (and to prevent a runtime warning inside Qt).
+ returnValue = QVariant(method.returnType(), 0);
+ }
+ QGenericReturnArgument returnArgument(method.typeName(), returnValue.data());
+
+ // now we can call the method
+ method.invoke(object, returnArgument,
+ arguments[0], arguments[1], arguments[2], arguments[3], arguments[4],
+ arguments[5], arguments[6], arguments[7], arguments[8], arguments[9]);
+
+ // and send the return value to the client
+ webChannel->respond(id, QJsonValue::fromVariant(wrapResult(returnValue)));
+
+ return true;
+}
+
+void QtMetaObjectPublisherPrivate::signalEmitted(const QObject *object, const int signalIndex, const QVariantList &arguments)
+{
+ if (!webChannel) {
+ return;
+ }
+ if (!signalToPropertyMap.value(object).contains(signalIndex)) {
+ QJsonObject data;
+ const QString &objectName = registeredObjectIds.value(object);
+ Q_ASSERT(!objectName.isEmpty());
+ data[KEY_OBJECT] = objectName;
+ data[KEY_SIGNAL] = signalIndex;
+ if (!arguments.isEmpty()) {
+ // TODO: wrap (new) objects on the fly
+ data[KEY_ARGS] = QJsonArray::fromVariantList(arguments);
+ }
+ webChannel->sendMessage(TYPE_SIGNAL, data);
+
+ if (signalIndex == s_destroyedSignalIndex) {
+ objectDestroyed(object);
+ }
+ } else {
+ pendingPropertyUpdates[object][signalIndex] = arguments;
+ if (clientIsIdle && !blockUpdates && !timer.isActive()) {
+ timer.start(PROPERTY_UPDATE_INTERVAL, q);
+ }
+ }
+}
+
+void QtMetaObjectPublisherPrivate::objectDestroyed(const QObject *object)
+{
+ const QString &id = registeredObjectIds.take(object);
+ Q_ASSERT(!id.isEmpty());
+ bool removed = registeredObjects.remove(id);
+ Q_ASSERT(removed);
+ Q_UNUSED(removed);
+
+ signalToPropertyMap.remove(object);
+ pendingPropertyUpdates.remove(object);
+ wrappedObjects.remove(object);
+}
+
+QVariant QtMetaObjectPublisherPrivate::wrapResult(const QVariant &result)
+{
+ if (QObject *object = result.value<QObject *>()) {
+ QVariantMap &objectInfo = wrappedObjects[object];
+ if (!objectInfo.isEmpty()) {
+ // already registered, use cached information
+ Q_ASSERT(registeredObjectIds.contains(object));
+ return objectInfo;
+ } // else the object is not yet wrapped, do it now
+
+ const QString &id = objectId(object);
+ Q_ASSERT(!registeredObjects.contains(id));
+ Q_ASSERT(!registeredObjectIds.contains(object));
+
+ objectInfo[KEY_QOBJECT] = true;
+ objectInfo[KEY_ID] = id;
+ objectInfo[KEY_DATA] = q->classInfoForObject(object);
+
+ registeredObjectIds[object] = id;
+ registeredObjects[id] = object;
+ wrappedObjects.insert(object, objectInfo);
+
+ initializePropertyUpdates(object, objectInfo);
+ return objectInfo;
+ }
+
+ // no need to wrap this
+ return result;
+}
+
+void QtMetaObjectPublisherPrivate::deleteWrappedObject(QObject *object) const
+{
+ if (!wrappedObjects.contains(object)) {
+ qWarning() << "Not deleting non-wrapped object" << object;
+ return;
+ }
+ object->deleteLater();
+}
+
+QtMetaObjectPublisher::QtMetaObjectPublisher(QObject *parent)
+ : QObject(parent)
+ , d(new QtMetaObjectPublisherPrivate(this))
+{
+}
+
+QtMetaObjectPublisher::~QtMetaObjectPublisher()
+{
+
}
QVariantMap QtMetaObjectPublisher::classInfoForObjects(const QVariantMap &objectMap) const
@@ -68,7 +477,7 @@ QVariantMap QtMetaObjectPublisher::classInfoForObjects(const QVariantMap &object
QVariantMap ret;
QMap<QString, QVariant>::const_iterator it = objectMap.constBegin();
while (it != objectMap.constEnd()) {
- QObject* object = it.value().value<QObject*>();
+ QObject *object = it.value().value<QObject *>();
if (object) {
const QVariantMap &info = classInfoForObject(object);
if (!info.isEmpty()) {
@@ -90,15 +499,15 @@ QVariantMap QtMetaObjectPublisher::classInfoForObject(QObject *object) const
QVariantList qtSignals, qtMethods;
QVariantList qtProperties;
QVariantMap qtEnums;
- const QMetaObject* metaObject = object->metaObject();
+ const QMetaObject *metaObject = object->metaObject();
QSet<int> notifySignals;
- QSet<QString> properties;
+ QSet<QString> identifiers;
for (int i = 0; i < metaObject->propertyCount(); ++i) {
const QMetaProperty &prop = metaObject->property(i);
QVariantList propertyInfo;
const QString &propertyName = QString::fromLatin1(prop.name());
propertyInfo.append(propertyName);
- properties << propertyName;
+ identifiers << propertyName;
if (prop.hasNotifySignal()) {
notifySignals << prop.notifySignalIndex();
const int numParams = prop.notifySignal().parameterCount();
@@ -112,9 +521,9 @@ QVariantMap QtMetaObjectPublisher::classInfoForObject(QObject *object) const
if (notifySignal.length() == changedSuffix.length() + propertyName.length() &&
notifySignal.endsWith(changedSuffix) && notifySignal.startsWith(prop.name()))
{
- propertyInfo.append(1);
+ propertyInfo.append(QVariant::fromValue(QVariantList() << 1 << prop.notifySignalIndex()));
} else {
- propertyInfo.append(QString::fromLatin1(notifySignal));
+ propertyInfo.append(QVariant::fromValue(QVariantList() << QString::fromLatin1(notifySignal) << prop.notifySignalIndex()));
}
} else {
if (!prop.isConstant()) {
@@ -122,7 +531,7 @@ QVariantMap QtMetaObjectPublisher::classInfoForObject(QObject *object) const
"value updates in HTML will be broken!",
prop.name(), object->metaObject()->className());
}
- propertyInfo.append(0);
+ propertyInfo.append(QVariant::fromValue(QVariantList()));
}
propertyInfo.append(prop.read(object));
qtProperties.append(QVariant::fromValue(propertyInfo));
@@ -132,18 +541,22 @@ QVariantMap QtMetaObjectPublisher::classInfoForObject(QObject *object) const
continue;
}
const QMetaMethod &method = metaObject->method(i);
- //NOTE: This will not work for overloaded methods/signals.
//NOTE: this must be a string, otherwise it will be converted to '{}' in QML
const QString &name = QString::fromLatin1(method.name());
- if (properties.contains(name)) {
- // optimize: Don't send the getter method, it gets overwritten by the
- // property on the client side anyways.
+ // optimize: skip overloaded methods/signals or property getters, on the JS side we can only
+ // call one of them anyways
+ // TODO: basic support for overloaded signals, methods
+ if (identifiers.contains(name)) {
continue;
}
- if (method.methodType() == QMetaMethod::Signal)
- qtSignals << name;
- else if (method.access() == QMetaMethod::Public)
- qtMethods << name;
+ identifiers << name;
+ // send data as array to client with format: [name, index]
+ const QVariant data = QVariant::fromValue(QVariantList() << name << i);
+ if (method.methodType() == QMetaMethod::Signal) {
+ qtSignals.append(data);
+ } else if (method.access() == QMetaMethod::Public) {
+ qtMethods.append(data);
+ }
}
for (int i = 0; i < metaObject->enumeratorCount(); ++i) {
QMetaEnum enumerator = metaObject->enumerator(i);
@@ -160,53 +573,159 @@ QVariantMap QtMetaObjectPublisher::classInfoForObject(QObject *object) const
return data;
}
-static QString objectId(QObject *object)
+void QtMetaObjectPublisher::registerObjects(const QVariantMap &objects)
{
- return QString::number(quintptr(object), 16);
+ if (d->propertyUpdatesInitialized) {
+ qWarning("Registered new object after initialization. This does not work!");
+ return;
+ }
+ const QMap<QString, QVariant>::const_iterator end = objects.end();
+ for (QMap<QString, QVariant>::const_iterator it = objects.begin(); it != end; ++it) {
+ QObject *object = it.value().value<QObject *>();
+ if (!object) {
+ qWarning("Invalid QObject given to register under name %s", qPrintable(it.key()));
+ continue;
+ }
+ d->registeredObjects[it.key()] = object;
+ d->registeredObjectIds[object] = it.key();
+ }
}
-QVariant QtMetaObjectPublisher::wrapObject(QObject *object)
+bool QtMetaObjectPublisher::handleRequest(const QJsonObject &message)
{
- if (!object)
- return QVariant();
-
- const QString& id = objectId(object);
+ if (!message.contains(KEY_DATA)) {
+ return false;
+ }
- const WrapMapCIt& p = m_wrappedObjects.constFind(id);
- if (p != m_wrappedObjects.constEnd())
- return p.value().second;
+ const QJsonObject &payload = message.value(KEY_DATA).toObject();
+ if (!payload.contains(KEY_TYPE)) {
+ return false;
+ }
- QVariantMap objectInfo;
- objectInfo[KEY_QOBJECT] = true;
- objectInfo[KEY_ID] = id;
- objectInfo[KEY_DATA] = classInfoForObject(object);
+ const QString &type = payload.value(KEY_TYPE).toString();
+ if (type == TYPE_IDLE) {
+ d->setClientIsIdle(true);
+ return true;
+ } else if (type == TYPE_INIT) {
+ if (!d->blockUpdates) {
+ d->initializeClients();
+ } else {
+ d->pendingInit = true;
+ }
+ return true;
+ } else if (type == TYPE_DEBUG) {
+ static QTextStream out(stdout);
+ out << "DEBUG: " << payload.value(KEY_MESSAGE).toString() << endl;
+ return true;
+ } else if (payload.contains(KEY_OBJECT)) {
+ const QString &objectName = payload.value(KEY_OBJECT).toString();
+ QObject *object = d->registeredObjects.value(objectName);
+ if (!object) {
+ qWarning() << "Unknown object encountered" << objectName;
+ return false;
+ }
- m_wrappedObjects.insert(id, WrapInfo(object, objectInfo));
- connect(object, SIGNAL(destroyed(QObject*)), SLOT(wrappedObjectDestroyed(QObject*)));
+ if (type == TYPE_INVOKE_METHOD) {
+ return d->invokeMethod(object, payload.value(KEY_METHOD).toInt(-1), payload.value(KEY_ARGS).toArray(), message.value(KEY_ID));
+ } else if (type == TYPE_CONNECT_TO_SIGNAL) {
+ d->signalHandler.connectTo(object, payload.value(KEY_SIGNAL).toInt(-1));
+ return true;
+ } else if (type == TYPE_DISCONNECT_FROM_SIGNAL) {
+ d->signalHandler.disconnectFrom(object, payload.value(KEY_SIGNAL).toInt(-1));
+ return true;
+ } else if (type == TYPE_SET_PROPERTY) {
+ // TODO: use property indices
+ const QString &propertyName = payload.value(KEY_PROPERTY).toString();
+ const int propertyIdx = object->metaObject()->indexOfProperty(qPrintable(propertyName));
+ if (propertyIdx == -1) {
+ qWarning() << "Cannot set unknown property" << propertyName << "of object" << objectName;
+ return false;
+ }
+ object->metaObject()->property(propertyIdx).write(object, payload.value(KEY_VALUE).toVariant());
+ return true;
+ }
+ }
+ return false;
+}
- return objectInfo;
+QWebChannel *QtMetaObjectPublisher::webChannel() const
+{
+ return d->webChannel;
}
-QObject *QtMetaObjectPublisher::unwrapObject(const QString& id) const
+void QtMetaObjectPublisher::setWebChannel(QWebChannel *webChannel)
{
- const WrapMapCIt& p = m_wrappedObjects.constFind(id);
- if (p != m_wrappedObjects.constEnd())
- return p.value().first;
- return 0;
+ if (d->webChannel == webChannel) {
+ return;
+ }
+
+ d->webChannel = webChannel;
+
+ emit webChannelChanged(webChannel);
}
-void QtMetaObjectPublisher::wrappedObjectDestroyed(QObject* object)
+bool QtMetaObjectPublisher::blockUpdates() const
{
- const QString& id = objectId(object);
- m_wrappedObjects.remove(id);
- emit wrappedObjectDestroyed(id);
+ return d->blockUpdates;
}
-void QtMetaObjectPublisher::deleteWrappedObject(QObject* object) const
+void QtMetaObjectPublisher::setBlockUpdates(bool block)
{
- if (!m_wrappedObjects.contains(objectId(object))) {
- qWarning() << "Not deleting non-wrapped object" << object;
+ if (d->blockUpdates == block) {
return;
}
- object->deleteLater();
+ d->blockUpdates = block;
+
+ if (!d->blockUpdates) {
+ if (d->pendingInit) {
+ d->initializeClients();
+ } else {
+ d->sendPendingPropertyUpdates();
+ }
+ } else if (d->timer.isActive()) {
+ d->timer.stop();
+ }
+
+ emit blockUpdatesChanged(block);
+}
+
+void QtMetaObjectPublisher::bench_ensureUpdatesInitialized()
+{
+ if (!d->propertyUpdatesInitialized) {
+ d->initializeClients();
+ }
+}
+
+void QtMetaObjectPublisher::bench_sendPendingPropertyUpdates()
+{
+ d->clientIsIdle = true;
+ d->sendPendingPropertyUpdates();
+}
+
+void QtMetaObjectPublisher::bench_initializeClients()
+{
+ d->propertyUpdatesInitialized = false;
+ d->signalToPropertyMap.clear();
+ d->signalHandler.clear();
+ d->initializeClients();
+}
+
+void QtMetaObjectPublisher::bench_registerObjects(const QVariantMap &objects)
+{
+ d->propertyUpdatesInitialized = false;
+ registerObjects(objects);
+}
+
+bool QtMetaObjectPublisher::test_clientIsIdle() const
+{
+ return d->clientIsIdle;
+}
+
+void QtMetaObjectPublisher::timerEvent(QTimerEvent *event)
+{
+ if (event->timerId() == d->timer.timerId()) {
+ d->sendPendingPropertyUpdates();
+ } else {
+ QObject::timerEvent(event);
+ }
}
diff --git a/src/qtmetaobjectpublisher.h b/src/qtmetaobjectpublisher.h
index 2fd3c0d..88a54c4 100644
--- a/src/qtmetaobjectpublisher.h
+++ b/src/qtmetaobjectpublisher.h
@@ -46,42 +46,69 @@
#define QTMETAOBJECTPUBLISHER_H
#include <QObject>
-#include <QVariantMap>
-#include <QQuickItem>
+#include <QVariant>
-// NOTE: QQuickItem inheritance required to enable QML item nesting (i.e. Timer in MetaObjectPublisher)
-class QtMetaObjectPublisher : public QQuickItem
+class QWebChannel;
+
+struct QtMetaObjectPublisherPrivate;
+
+class QtMetaObjectPublisher : public QObject
{
Q_OBJECT
+ Q_PROPERTY(QWebChannel *webChannel READ webChannel WRITE setWebChannel NOTIFY webChannelChanged);
+ Q_PROPERTY(bool blockUpdates READ blockUpdates WRITE setBlockUpdates NOTIFY blockUpdatesChanged);
+
public:
- explicit QtMetaObjectPublisher(QQuickItem *parent = 0);
+ explicit QtMetaObjectPublisher(QObject *parent = 0);
+ virtual ~QtMetaObjectPublisher();
Q_INVOKABLE QVariantMap classInfoForObjects(const QVariantMap &objects) const;
Q_INVOKABLE QVariantMap classInfoForObject(QObject *object) const;
- /// wrap object and return class info
- Q_INVOKABLE QVariant wrapObject(QObject *object);
- /// Search object by id and return it, or null if it could not be found.
- Q_INVOKABLE QObject *unwrapObject(const QString &id) const;
- /// Invoke delete later on @p object, but only if it is a wrapped object.
- Q_INVOKABLE void deleteWrappedObject(QObject *object) const;
+ /**
+ * Register a map of string ID to QObject* objects.
+ *
+ * The properties, signals and public methods of the QObject are
+ * published to the remote client, where an object with the given identifier
+ * is constructed.
+ *
+ * TODO: This must be called, before clients are initialized.
+ */
+ Q_INVOKABLE void registerObjects(const QVariantMap &objects);
+
+ /**
+ * Handle the given WebChannel client request and potentially give a response.
+ *
+ * @return true if the request was handled, false otherwise.
+ */
+ Q_INVOKABLE bool handleRequest(const QJsonObject &message);
+
+ QWebChannel *webChannel() const;
+ void setWebChannel(QWebChannel *webChannel);
+
+ /**
+ * When updates are blocked, no property updates are transmitted to remote clients.
+ */
+ bool blockUpdates() const;
+ void setBlockUpdates(bool block);
+
+ /// TODO: cleanup: rewrite tests in C++ and access PIMPL data from there
+ Q_INVOKABLE void bench_ensureUpdatesInitialized();
+ Q_INVOKABLE void bench_sendPendingPropertyUpdates();
+ Q_INVOKABLE void bench_registerObjects(const QVariantMap &objects);
+ Q_INVOKABLE void bench_initializeClients();
+ Q_INVOKABLE bool test_clientIsIdle() const;
signals:
- void wrappedObjectDestroyed(const QString& id);
+ void webChannelChanged(QWebChannel *channel);
+ void blockUpdatesChanged(bool block);
-private slots:
- void wrappedObjectDestroyed(QObject* object);
+protected:
+ virtual void timerEvent(QTimerEvent *);
private:
- /// Pairs of QObject and generated object info
- typedef QPair<QObject *, QVariantMap> WrapInfo;
- /// Maps object id to wrap info
- typedef QHash<QString, WrapInfo> WrapMap;
- /// Const iterator for map
- typedef WrapMap::const_iterator WrapMapCIt;
-
- /// Map of wrapped objects
- WrapMap m_wrappedObjects;
+ QScopedPointer<QtMetaObjectPublisherPrivate> d;
+ friend struct QtMetaObjectPublisherPrivate;
};
#endif // QTMETAOBJECTPUBLISHER_H
diff --git a/src/qwebchannel_plugin.cpp b/src/qwebchannel_plugin.cpp
index a5834ba..13a38a5 100644
--- a/src/qwebchannel_plugin.cpp
+++ b/src/qwebchannel_plugin.cpp
@@ -49,6 +49,6 @@
void QWebChannelPlugin::registerTypes(const char *uri)
{
qmlRegisterType<QWebChannel>(uri, 1, 0, "WebChannel");
- qmlRegisterType<QtMetaObjectPublisher>(uri, 1, 0, "MetaObjectPublisherImpl");
+ qmlRegisterType<QtMetaObjectPublisher>(uri, 1, 0, "MetaObjectPublisher");
}
diff --git a/src/signalhandler.h b/src/signalhandler.h
new file mode 100644
index 0000000..0db6085
--- /dev/null
+++ b/src/signalhandler.h
@@ -0,0 +1,278 @@
+/****************************************************************************
+**
+** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies).
+** Copyright (C) 2013 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author Milian Wolff <milian.wolff@kdab.com>
+** Contact: http://www.qt-project.org/legal
+*
+** This file is part of the QtCore module of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:LGPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and Digia. For licensing terms and
+** conditions see http://qt.digia.com/licensing. For further information
+** use the contact form at http://qt.digia.com/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 2.1 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPL included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 2.1 requirements
+** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
+**
+** In addition, as a special exception, Digia gives you certain additional
+** rights. These rights are described in the Digia Qt LGPL Exception
+** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3.0 as published by the Free Software
+** Foundation and appearing in the file LICENSE.GPL included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU General Public License version 3.0 requirements will be
+** met: http://www.gnu.org/copyleft/gpl.html.
+**
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#ifndef SIGNALHANDLER_H
+#define SIGNALHANDLER_H
+
+#include <QObject>
+#include <QHash>
+#include <QVector>
+#include <QMetaMethod>
+#include <QDebug>
+
+/**
+ * The signal handler is similar to QSignalSpy, but geared towards the usecase of the web channel.
+ *
+ * It allows connecting to any number of signals of arbitrary objects and forwards the signal
+ * invocations to the Receiver by calling its signalEmitted function, which takes the object,
+ * signal index and a QVariantList of arguments.
+ */
+template<class Receiver>
+class SignalHandler : public QObject
+{
+public:
+ SignalHandler(Receiver *receiver, QObject *parent = 0)
+ : QObject(parent)
+ , m_receiver(receiver)
+ {
+ }
+
+ /**
+ * Connect to a signal of @p object identified by @p signalIndex.
+ *
+ * If the handler is already connected to the signal, an internal counter is increased,
+ * i.e. the handler never connects multiple times to the same signal.
+ */
+ void connectTo(const QObject *object, const int signalIndex);
+
+ /**
+ * Decrease the connection counter for the connection to the given signal.
+ *
+ * When the counter drops to zero, the connection is disconnected.
+ */
+ void disconnectFrom(const QObject *object, const int signalIndex);
+
+ /**
+ * @internal
+ *
+ * Custom implementation of qt_metacall which calls dispatch() for connected signals.
+ */
+ int qt_metacall(QMetaObject::Call call, int methodId, void **args) Q_DECL_OVERRIDE;
+
+ /**
+ * Reset all connections, useful for benchmarks.
+ */
+ void clear();
+
+private:
+ /**
+ * Exctract the arguments of a signal call and pass them to the receiver.
+ *
+ * The @p argumentData is converted to a QVariantList and then passed to the receiver's
+ * signalEmitted method.
+ */
+ void dispatch(const QObject *object, const int signalIdx, void **argumentData);
+
+ Receiver *m_receiver;
+
+ // maps meta object -> signalIndex -> list of arguments
+ // NOTE: This data is "leaked" on disconnect until deletion of the handler, is this a problem?
+ typedef QVector<int> ArgumentTypeList;
+ typedef QHash<int, ArgumentTypeList> SignalArgumentHash;
+ QHash<const QMetaObject *, SignalArgumentHash > m_signalArgumentTypes;
+
+ /*
+ * Tracks how many connections are active to object signals.
+ *
+ * Maps object -> signalIndex -> pair of connection and number of connections
+ *
+ * Note that the handler is connected to the signal only once, whereas clients
+ * may have connected multiple times.
+ *
+ * TODO: Move more of this logic to the HTML client side, esp. the connection counting.
+ */
+ typedef QPair<QMetaObject::Connection, int> ConnectionPair;
+ typedef QHash<int, ConnectionPair> SignalConnectionHash;
+ typedef QHash<const QObject*, SignalConnectionHash> ConnectionHash;
+ ConnectionHash m_connectionsCounter;
+};
+
+/**
+ * Find and return the signal of index @p signalIndex in the meta object of @p object and return it.
+ *
+ * The return value is also verified to ensure it is a signal.
+ */
+QMetaMethod findSignal(const QMetaObject *metaObject, const int signalIndex)
+{
+ QMetaMethod signal = metaObject->method(signalIndex);
+ if (!signal.isValid()) {
+ qWarning("Cannot find signal with index %d of object %s", signalIndex, metaObject->className());
+ return QMetaMethod();
+ }
+ Q_ASSERT(signal.methodType() == QMetaMethod::Signal);
+ return signal;
+}
+
+template<class Receiver>
+void SignalHandler<Receiver>::connectTo(const QObject *object, const int signalIndex)
+{
+ const QMetaObject *metaObject = object->metaObject();
+ const QMetaMethod &signal = findSignal(metaObject, signalIndex);
+ if (!signal.isValid()) {
+ return;
+ }
+
+ ConnectionPair &connectionCounter = m_connectionsCounter[object][signalIndex];
+ if (connectionCounter.first) {
+ // increase connection counter if already connected
+ ++connectionCounter.second;
+ return;
+ } // otherwise not yet connected, do so now
+
+ const int memberOffset = QObject::staticMetaObject.methodCount();
+ QMetaObject::Connection connection = QMetaObject::connect(object, signal.methodIndex(), this, memberOffset, Qt::DirectConnection, 0);
+ if (!connection) {
+ qWarning() << "SignalHandler: QMetaObject::connect returned false. Unable to connect to" << object << signal.name() << signal.methodSignature();
+ return;
+ }
+ connectionCounter.first = connection;
+ connectionCounter.second = 1;
+
+ if (!m_signalArgumentTypes.value(metaObject).contains(signal.methodIndex())) {
+ // find the type ids of the signal parameters, see also QSignalSpy::initArgs
+ QVector<int> args;
+ args.reserve(signal.parameterCount());
+ for (int i = 0; i < signal.parameterCount(); ++i) {
+ int tp = signal.parameterType(i);
+ if (tp == QMetaType::UnknownType && object) {
+ void *argv[] = { &tp, &i };
+ QMetaObject::metacall(const_cast<QObject *>(object),
+ QMetaObject::RegisterMethodArgumentMetaType,
+ signal.methodIndex(), argv);
+ if (tp == -1) {
+ tp = QMetaType::UnknownType;
+ }
+ }
+ if (tp == QMetaType::UnknownType) {
+ Q_ASSERT(tp != QMetaType::Void); // void parameter => metaobject is corrupt
+ qWarning("Don't know how to handle '%s', use qRegisterMetaType to register it.",
+ signal.parameterNames().at(i).constData());
+ }
+ args << tp;
+ }
+
+ m_signalArgumentTypes[metaObject][signal.methodIndex()] = args;
+ }
+}
+
+template<class Receiver>
+void SignalHandler<Receiver>::dispatch(const QObject *object, const int signalIdx, void **argumentData)
+{
+ Q_ASSERT(m_signalArgumentTypes.contains(object->metaObject()));
+ const QHash<int, QVector<int> > &objectSignalArgumentTypes = m_signalArgumentTypes.value(object->metaObject());
+ QHash<int, QVector<int> >::const_iterator signalIt = objectSignalArgumentTypes.constFind(signalIdx);
+ if (signalIt == objectSignalArgumentTypes.constEnd()) {
+ // not connected to this signal, skip
+ return;
+ }
+ const QVector<int> &argumentTypes = *signalIt;
+ QVariantList arguments;
+ arguments.reserve(argumentTypes.count());
+ // TODO: basic overload resolution based on number of arguments?
+ for (int i = 0; i < argumentTypes.count(); ++i) {
+ const QMetaType::Type type = static_cast<QMetaType::Type>(argumentTypes.at(i));
+ QVariant arg;
+ if (type == QMetaType::QVariant) {
+ arg = *reinterpret_cast<QVariant *>(argumentData[i + 1]);
+ } else {
+ arg = QVariant(type, argumentData[i + 1]);
+ }
+ arguments.append(arg);
+ }
+ m_receiver->signalEmitted(object, signalIdx, arguments);
+}
+
+template<class Receiver>
+void SignalHandler<Receiver>::disconnectFrom(const QObject *object, const int signalIndex)
+{
+ Q_ASSERT(m_connectionsCounter.value(object).contains(signalIndex));
+ ConnectionPair &connection = m_connectionsCounter[object][signalIndex];
+ --connection.second;
+ if (!connection.second || !connection.first) {
+ QObject::disconnect(connection.first);
+ m_connectionsCounter[object].remove(signalIndex);
+ if (m_connectionsCounter[object].isEmpty()) {
+ m_connectionsCounter.remove(object);
+ }
+ }
+}
+
+template<class Receiver>
+int SignalHandler<Receiver>::qt_metacall(QMetaObject::Call call, int methodId, void **args)
+{
+ methodId = QObject::qt_metacall(call, methodId, args);
+ if (methodId < 0)
+ return methodId;
+
+ if (call == QMetaObject::InvokeMetaMethod) {
+ if (methodId == 0) {
+ Q_ASSERT(sender());
+ Q_ASSERT(senderSignalIndex() != -1);
+ dispatch(sender(), senderSignalIndex(), args);
+ static const int destroyedIndex = metaObject()->indexOfSignal("destroyed(QObject*)");
+ if (senderSignalIndex() == destroyedIndex) {
+ ConnectionHash::iterator it = m_connectionsCounter.find(sender());
+ Q_ASSERT(it != m_connectionsCounter.end());
+ foreach (const ConnectionPair &connection, *it) {
+ QObject::disconnect(connection.first);
+ }
+ m_connectionsCounter.erase(it);
+ }
+ }
+ --methodId;
+ }
+ return methodId;
+}
+
+template<class Receiver>
+void SignalHandler<Receiver>::clear()
+{
+ foreach (const SignalConnectionHash &connections, m_connectionsCounter) {
+ foreach (const ConnectionPair &connection, connections) {
+ QObject::disconnect(connection.first);
+ }
+ }
+ m_connectionsCounter.clear();
+ m_signalArgumentTypes.clear();
+}
+
+#endif // SIGNALHANDLER_H
diff --git a/src/src.pri b/src/src.pri
index 408b430..5ae01f3 100644
--- a/src/src.pri
+++ b/src/src.pri
@@ -6,4 +6,6 @@ SOURCES += \
HEADERS += \
$$PWD/qwebchannel.h \
$$PWD/qtmetaobjectpublisher.h \
- $$PWD/qwebsocketserver.h
+ $$PWD/qwebsocketserver.h \
+ $$PWD/variantargument.h \
+ $$PWD/signalhandler.h
diff --git a/src/src.pro b/src/src.pro
index 5b860ba..bd2e3c7 100644
--- a/src/src.pro
+++ b/src/src.pro
@@ -17,15 +17,13 @@ RESOURCES += \
OTHER_FILES = qmldir \
webchannel.js \
- qobject.js \
- MetaObjectPublisher.qml
+ qobject.js
target.path = $$[QT_INSTALL_QML]/$$TARGETPATH
# extra files that need to be deployed to $$TARGETPATH
DEPLOY_FILES = \
- qmldir \
- MetaObjectPublisher.qml
+ qmldir
for(FILE, DEPLOY_FILES): qmldir.files += $$PWD/$$FILE
qmldir.path += $$[QT_INSTALL_QML]/$$TARGETPATH
diff --git a/src/variantargument.h b/src/variantargument.h
new file mode 100644
index 0000000..d5382bd
--- /dev/null
+++ b/src/variantargument.h
@@ -0,0 +1,103 @@
+/****************************************************************************
+**
+** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies).
+** Copyright (C) 2013 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author Milian Wolff <milian.wolff@kdab.com>
+** Contact: http://www.qt-project.org/legal
+*
+** This file is part of the QtCore module of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:LGPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and Digia. For licensing terms and
+** conditions see http://qt.digia.com/licensing. For further information
+** use the contact form at http://qt.digia.com/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 2.1 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPL included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 2.1 requirements
+** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
+**
+** In addition, as a special exception, Digia gives you certain additional
+** rights. These rights are described in the Digia Qt LGPL Exception
+** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3.0 as published by the Free Software
+** Foundation and appearing in the file LICENSE.GPL included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU General Public License version 3.0 requirements will be
+** met: http://www.gnu.org/copyleft/gpl.html.
+**
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#ifndef VARIANTARGUMENT_H
+#define VARIANTARGUMENT_H
+
+#include <QVariant>
+#include <QMetaType>
+
+/**
+ * RAII QVariant to Q[Generic]Argument conversion
+ */
+class VariantArgument
+{
+public:
+ explicit VariantArgument()
+ : m_data(0)
+ , m_paramType(0)
+ {
+ }
+
+ /// TODO: test with C++ methods that don't take a QVariant as arg
+ /// also test conversions
+ void setValue(const QVariant &value, int paramType)
+ {
+ if (m_data) {
+ QMetaType::destroy(m_paramType, m_data);
+ m_name.clear();
+ m_data = 0;
+ }
+
+ m_paramType = paramType;
+
+ if (value.isValid()) {
+ m_name = value.typeName();
+ m_data = QMetaType::create(m_paramType, value.constData());
+ }
+ }
+
+ ~VariantArgument()
+ {
+ if (m_data) {
+ QMetaType::destroy(m_paramType, m_data);
+ m_data = 0;
+ }
+ }
+
+ operator QGenericArgument() const
+ {
+ if (!m_data) {
+ return QGenericArgument();
+ }
+ return QGenericArgument(m_name.constData(), m_data);
+ }
+
+private:
+ Q_DISABLE_COPY(VariantArgument)
+
+ QByteArray m_name;
+ void* m_data;
+ int m_paramType;
+};
+
+#endif // VARIANTARGUMENT_H