Monthly Archives: August 2016

QRPC: A Qt remoting library

This project of mine has been around for quite a while already. I’ve never much publicised it, however, and for the past year the code hasn’t seen any changes (until a few days ago, anyway). But related to my current job, I’ve found a new need for remote procedure calls, so I came back to the code after all.

QRPC is a remoting library tightly integrated with Qt: in simplified terms, it’s an easy way to get signal-slot connections over the network. At least that was its original intention, and hence comes the name (QRPC as in “Qt Remote Procedure Call”).

But actually, it’s a little more than that. QRPC pipes the whole of Qt’s meta-object functionality over a given transport. That can be a network socket, unix domain socket or something more high-level like ZeroMQ. But it could, for example, also be a serial port (not sure why anybody would want that, though).
The actual remoting system works as follows: A server marks some objects for “export”, meaning they will be accessible by clients. Clients can then request any such object by its identifier. The server then serialises the whole QMetaObject hierarchy of the exported object and sends it to the client. There, it is reconstructed and used as the dynamic QMetaObject of a specialised QObject. Thus, the client not only has access to signals and slots, but also to properties and Q_CLASSINFOs (actually anything that a QMetaObject has to offer). The property support also includes dynamic properties, not only the statically defined QMetaProperties.
Method invocations, signal emissions and property changes are serialised and sent over the transport – after all, a QMetaObject without the dynamics isn’t that useful ;).

To give an impression of how QRPC is used, here’s an excerpt from the example included in the repository:

Server:

Widget::Widget(QWidget *parent) :
    QWidget(parent), ui(new Ui::Widget),
    localServer(new QLocalServer(this)),
    exporter(new QRPCObjectExporter(this))
{
    ui->setupUi(this);

    // Export the QSpinBox with the identifier "spinbox".
    exporter->exportObject("spinbox", ui->spinBox);

    // Handle new connections, serve under the name "qrpcsimpleserver".
    connect(localServer, &QLocalServer::newConnection, this, &Widget::handleNewConnection);
    localServer->listen("qrpcsimpleserver");
}

void Widget::handleNewConnection()
{
    // Get the connection socket
    QLocalSocket *socket = localServer->nextPendingConnection();

    // Create a transport...
    QRPCIODeviceTransport *transport = new QRPCIODeviceTransport(socket, socket);

    // ... and the server communicating over the transport, serving objects exported from the
    // exporter. Both the transport and the server are children of the socket so they get
    // properly cleaned up.
    new QRPCServer(exporter, transport, socket);
}

Client:

Widget::Widget(QWidget *parent) :
    QWidget(parent),
    ui(new Ui::Widget), socket(new QLocalSocket(this)), rpcClient(0)
{
    // ...
    // Connect to the server on button click
    connect(ui->connect, &QPushButton::clicked, [this]() {
        socket->connectToServer("qrpcsimpleserver");
    });

    // Handle connection events
    connect(socket, &QLocalSocket::connected, this, &Widget::connectionEstablished);
    connect(socket, &QLocalSocket::disconnected, this, &Widget::connectionLost);
}

void Widget::connectionEstablished()
{
    // Create a transport...
    QRPCIODeviceTransport *transport = new QRPCIODeviceTransport(socket, this);
    // ... and a client communicating over the transport.
    QRPCClient *client = new QRPCClient(transport, this);

    // When we receive a remote object (in this example, it can only be the spinbox),
    // synchronise the state and connect the slider to it.
    connect(client, &QRPCClient::remoteObjectReady, [this](QRPCObject *object) {
        ui->horizontalSlider->setValue(object->property("value").toInt());
        ui->horizontalSlider->setMinimum(object->property("minimum").toInt());
        ui->horizontalSlider->setMaximum(object->property("maximum").toInt());

        connect(ui->horizontalSlider, SIGNAL(valueChanged(int)), object, SLOT(setValue(int)));
    });

    // Request the spinbox.
    client->requestRemoteObject("spinbox");

    // Clean up when we lose the connection.
    connect(socket, &QLocalSocket::disconnected, client, &QObject::deleteLater);
    connect(socket, &QLocalSocket::disconnected, transport, &QObject::deleteLater);

    // ...
}

Argument or property types have to be registered metatypes (same as when using queued connections) and serialisable with QDataStream. The QDataStream operators also have to be registered with Qt’s meta type system (cf. QMetaType docs).
At the moment, one shortcoming is that references to QObjects (i.e. “QObject*”) can’t be transferred over the network, even if the relevant QObjects are “exported” by the server. Part of the problem is that Qt doesn’t allow us to register custom stream operators for “QObject*”. This could be solved by manually traversing the serialised values, but that will negatively impact the performance. Usually one can work around that limitation, though.

Furthermore, method return values are intentionally not supported. Supporting them would first require a more complex system to keep track of state and secondly impose blocking method invocations, both of which run contrary to my design goals.

In some final remarks, I’d like to point out two similar projects: For one, there’s QtRemoteObjects in Qt’s playground which works quite similarly to QRPC. I began working on QRPC at around the same time as QtRemoteObjects was started and I didn’t know it existed until quite some time later. Having glanced over its source code, I find it to be a little too complex and doing too much for my needs without offering the same flexibility (like custom transports). I must admit that I haven’t checked it out in more detail, though.
Then there’s also QtWebChannel which again offers much of the metaobject functionality over the web, but this time specifically geared towards JavaScript/HTML5 clients and Web apps. I thought about reusing part of its code, but ultimately its focus on JS was too strong to make this a viable path.