Generate QtRemoteObjects based backends with the Qt Interface Framework Generator
Learn how to use the Qt Interface Framework Generator to create QtRemoteObjects based backends.
Introduction
This example shows how to generate a Middleware API, a Middleware Backend, and the corresponding Middleware Service using the Qt Interface Framework Generator. The communication between the backend and the service is done with QtRemoteObjects as the IPC.
We use a single QFace IDL file to generate:
- a shared library with the front-end code
- a backend plugin that implements a client to connect to the server
- a server that runs the actual backend logic in a separate server process
- a demo application that connects to the server and provides a UI to use the service
In addition to the generated C++ code, the backend plugin and the server also contain an intermediate .rep
file that is further processed by the replica compiler to produce the source and replica classes.
Walkthrough
The IDL file used in the example represents an imaginary remote service for processing data. It contains a single interface with one property and one method.
First, we need to define which module we want to describe. The module acts as a namespace, because the IDL file can contain multiple interfaces.
module Example.If.RemoteModule 1.0;
The most important part is the definition of the interface.
@config: { qml_type: "UiProcessingService" } interface ProcessingService { string lastMessage; int process(string data); }
In this case, we define an interface named ProcessingService with one property and one method. Every property and method definition needs to contain at least a type and a name. Most of the basic types are built-in and can be found in the QFace IDL Syntax.
Frontend library
Next, we use the Interface Framework Generator to generate a shared library containing a C++ implementation of our module and its interface; particularly the frontend template. This template generates a class derived from QIfAbstractFeature, that includes all of the specified properties. The generated library uses the Dynamic Backend System from QtInterfaceFramework, consequently providing an easy way to change how the behavior is implemented.
To call the autogenerator for our shared library, it needs to be integrated into the build system.
CMake:
First the InterfaceFramework
package needs to be found using find_package
:
find_package(Qt6 REQUIRED COMPONENTS InterfaceFramework)
Afterwards we proceed to build a library and let the autogenerator extend this target with the generated source code by invoking qt6_ifcodegen_extend_target.
qt_add_library(QtIfRemoteExample) # Interface Framework Generator: qt_ifcodegen_extend_target(QtIfRemoteExample IDL_FILES ../example-remote.qface TEMPLATE frontend )
qmake:
CONFIG += ifcodegen IFCODEGEN_SOURCES = ../example-remote.qface
By adding ifcodegen
to the CONFIG
variable, the ifcodegen
feature file is loaded and interprets the IFCODEGEN_SOURCES
variable, similar to the SOURCES
variable in regular qmake projects. However, activating the qmake feature via the CONFIG
variable has one disadvantage: if the feature is not available, no errors are reported. We recommend using the following additional code to report errors:
QT_FOR_CONFIG += interfaceframework !qtConfig(ifcodegen): error("No ifcodegen available")
The remaining part of the project file is a normal library setup that works on Linux, macOS, and Windows.
QtRemoteObjects Backend Plugin
As mentioned above, the frontend library uses the Dynamic Backend System. This means that for the library to provide some functionality, we also need a backend plugin. The generated plugin here works as a client that connects to the server using Qt Remote Objects. The build system integration works in the same way, but it uses a different generation template.
CMake:
A plugin is defined and extended by calling the codegenerator, this time with the backend_simulator
template:
qt_add_plugin(remote_backend_qtro PLUGIN_TYPE interfaceframework)
qmake:
CONFIG += ifcodegen plugin IFCODEGEN_TEMPLATE = backend_qtro IFCODEGEN_SOURCES = ../example-remote.qface PLUGIN_TYPE = interfaceframework PLUGIN_CLASS_NAME = RemoteClientQtROPlugin
The generated backend plugin code is usable as is, and doesn't require any further change. As we want to generate a plugin instead of a plain library, we need to instruct qmake to do so by adding plugin
to the CONFIG
variable.
For the plugin to compile correctly it needs to get the backend interface header from the previously created library. But this header is also generated, so it's not part of our source tree, but part of the build tree. To provide the backend interface header, we add it to the include path using the following code:
CMake:
target_link_libraries(remote_backend_qtro PUBLIC QtIfRemoteExample )
qmake:
INCLUDEPATH += $$OUT_PWD/../frontend
Most of the code in the backend plugin is generated by the Interface Framework Generator, but some of it is generated by the Qt's Remote Object Compiler, repc
. To achieve this, the Interface Framework Generator produces an intermediate .repc
file that's further processed by the repc
compiler. This compiler is called via the generated build system file, found in the build directory.
Our application doesn't know about the existence of our backend plugin, so we need to put this plugin in a folder where the application typically looks for plugins. By default, Qt either searches in the plugins folder within its installation directory or in the application's current working directory. For QtInterfaceFramework plugins to be found, they need to be provided within a interfaceframework sub-folder. Add the following line to the backend build system file, as follows:
CMake:
set_target_properties(remote_backend_qtro PROPERTIES LIBRARY_OUTPUT_DIRECTORY ../interfaceframework)
qmake:
DESTDIR = ../interfaceframework
RemoteObjects Server
The server is an independent, GUI-less application that contains the backend's business logic, and we need to write most of its implementation. Nevertheless, the generator produces some code to simplify the development. We can generate server side code by using the Interface Framework Generator with the server_qtro template:
CMake:
# Interface Framework Generator: qt_ifcodegen_extend_target(qface-remote-server IDL_FILES ../example-remote.qface TEMPLATE server_qtro )
qmake:
TEMPLATE = app QT += interfaceframework QT -= gui CONFIG += c++11 ifcodegen ... IFCODEGEN_TEMPLATE = server_qtro IFCODEGEN_SOURCES = ../example-remote.qface
To use the generated remote source, we need to inherit from one of the classes defined in the generated rep_processingservice_source.h
file. In this example, we implement our server's logic in the ProcessingService
class and use the ProcessingServiceSimpleSource
as the base class:
// server_qtro/processingservice.h #include "rep_processingservice_source.h" class ProcessingService : public ProcessingServiceSimpleSource { public: ProcessingService(QObject *parent); QVariant process(const QString & data) override; };
Note that the base class already has the definitions for property accessors, but any custom method or slot needs to be overridden and defined. Our implementation of the process function merely counts and returns the length of the data passed and updates the lastMessage
property:
// server_qtro/processingservice.cpp ProcessingService::ProcessingService(QObject *parent) : ProcessingServiceSimpleSource(parent) { setLastMessage(u"Service online."_s); } QVariant ProcessingService::process(const QString & data) { setLastMessage(u"Processed data \'%1\'"_s.arg(data)); return data.length(); }
To make the ProcessingService
class accessible remotely, we need to share it via the QRemoteObjectNode::enableRemoting() function. Instead of calling that directly, we use the QIfRemoteObjectsConfig class instead as it also takes care of creating a correctly configured QRemoteObjectHost. To do that we need to pass the module name and the interface name together with the ProcessingService
instance to the QIfRemoteObjectsConfig::enableRemoting() function.
All this can be done in our own main.cpp implementation, but to take the full advantage of the QIfRemoteObjectsConfig class, we let the ifcodegen
generate a main.cpp for us. This autogenerated main introduces command-line arguments to configure which URLs should be used for sharing and will call a serverMain function which we need to provide.
First enable the main.cpp autogeneration in the qface file:
@config_server_qtro: { useGeneratedMain: true } module Example.If.RemoteModule 1.0;
Now, implement the serverMain to instantiate and share our service:
#include <QCoreApplication> #include "processingservice.h" #include <QIfRemoteObjectsConfig> using namespace Qt::StringLiterals; void serverMain(QIfRemoteObjectsConfig &config) { auto service = new ProcessingService(qApp); config.enableRemoting(u"Example.If.RemoteModule"_s, u"ProcessingService"_s, service); }
This is all you need to do to implement a service that is accessible remotely; use the properties as usual and provide the method implementations. The QtRemoteObjects library takes care of the communication.
Demo Client Application
The demo application presents a simple QML GUI to use the remote service over the generated interface.
As we do not provide a QML plugin, the application needs to link to the generated frontend library and call the RemoteModule::registerTypes
and RemoteModule::registerQmlTypes
methods that are generated in the module singleton to register all autogenerated interfaces and types with the QML engine.
In our QML application, we still need to import the module using the same module URI which is used in the QFace file. Afterwards the interface can be instantiated like any other QML item.
// demo/main.qml import Example.If.RemoteModule Window { visible: true width: 640 height: 480 title: qsTr("QtIF Remote example") UiProcessingService { id: processingService } ...
Every method call that is made through a generated API, is asynchronous. This means that instead of directly returning a return value, a QIfPendingReply object is returned. Using the QIfPendingReply::then() method on the returned object, we may assign callbacks to it that are called when the method call has been successfully finished; or if it has failed.
// demo/main.qml Button { text: "Process" onClicked: processingService.process(inputField.text).then( function(result) { //success callback resultLabel.text = result }, function() { //failure callback resultLabel.text = "failed" } ) }
In case of properties, we use bindings as usual:
// demo/main.qml Row { Text { text: "Last message: " } Text { id: serverMessage text: processingService.lastMessage } }
Running the Example
To see the demo's entire functionality, run both the server and the demo application simultaneously. You may leave the server running and restart the application, or vice versa, to see that the reconnection works. Run the demo application alone without the server running, to test how the remote method call fails when there is no connection.
© 2024 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners. The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation. Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.