C++
Qt
QtDay 19

Qt Remote Object入門

はじめに

本当は、14日目に投稿するはずだったhermit4 です。間に合わずに @helicalgear さんに埋めていただいたようで、ごめんなさい。
昨日は、 @moguriso さんによる「websocketで待ち受けるサーバなのにJSONベースのコマンドを自発的に投げるツールを作ろう」でした。カレンダー表記ではタイトル長くて見切れてましたが、中身は必要なところをシンプルにまとめた良い記事です。こういうちょっとしたテストツールを作りたいって時にもQtはとても便利ですので、ぜひご一読下さい。

前回は、穴埋めのためのお手軽記事として、Qtを使っているアプリケーションのご紹介をさせていただきましたが、本日は真面目にQtの記事を書いてみようと思います。

Qt5.9から技術プレビューとしてQt Remote Objectの機能が導入されています。Qt 5.10では[TP2]となっているので、まだ正式採用にはなっていませんが、ネットワーク越しにsignal/slotやプロパティ機構を実現する仕組みのようですので、少し触って見たいと思います。

Qt初学者のための復習

シグナルとスロット

シグナルとスロットは、Qtがmoc(Meta Object Compiler)と呼ばれる独自のプリプロセッサとC++のdefineマクロを駆使して、C++に付け加えているオブジェクト間のメッセージ機構です。

QtではQObjectというクラスを派生し、いくつかのルールに従って記述することで、メタオブジェクトと呼ぶ情報を生成することでシグナルとスロットというメッセージ機構を利用できるようになります。

実装例

簡単な例を挙げてみましょう。

counter.h
#ifndef COUNTER_H
#define COUNTER_H

#include <QObject>

class Counter : public QObject // [1] 
{
    Q_OBJECT // [2]
public:
    Counter(int value=0, QObject* parent=0); 
    int value() const { return value_; }

public slots: // [3]
    void setValue(int value);

signals:     // [4]
    void valueChanged(int newValue);

private:
    int value_;
};
#endif // COUNTER_H

[1] まず、CounterというQObjectクラスの派生クラスを作成しました。
[2] Q_OBJECTは、メタオブジェクトのためのマクロで、とりあえずおまじないと思って下さい。
[3] ここからスロットの定義であることを示唆する指定方法です
[4] ここからシグナルであることを示唆する指定方法です

この書きっぷりから、C++規格を無視して独自拡張しているという誤解を招いて、C++erの方に嫌われることがあるのですが、実は、slotsとsignalsはdefineマクロになっていて、C++コンパイラでビルドすると、普通のアクセス指定子になります。つまり行儀の良し悪しは置いておいて、C++の規格の中で実装されています。

これを利用するのはmocの方で、mocは、"signals"と"slots"の文字列を解釈しながら、必要な情報を追加した moc_counter.h を生成するために利用されます。

counter.cpp
#include "counter.h"

Counter::Counter(int value, QObject* parent) 
       : QObject(parent), value_(value)
{
}

void Counter::setValue(int value) 
{
    if (value != value_) {
        value_ = value;
        emit valueChanged(value_); // [5] 
    }
}

実装の方をみてみると、シグナルの実装がされていないことがわかります。シグナルの実装はmocが自動生成し、moc_counter.hの中に書き込まれるため、ユーザーは宣言のみで実装することはありません。スロットは、普通にユーザーが処理を実装することになります。

[5]では、value が変化したことを通知するためのシグナルを発信するためemitが記述されています。このemitですが、実はこちらもマクロで、空文字列に変換されます。見かけ上C++を逸脱しているように見えますが、C++としてはただの関数呼び出しになります。

main.cpp
#include <QCoreApplication> 
#include "counter.h" 
#include <QTextStream>

QTextStream cout(stdout);

int main(int argc, char* argv[]) 
{
    QCoreApplication app(argc, argv);
    Counter a(1);
    Counter b(2);
    QObject::connect(&a, SIGNAL(valueChanged(int)), &b, SLOT(setValue(int))); // [6] 
    a.setValue(1024);
    cout << "1) a.value = " << a.value() << ", b.value = " << b.value() << endl;

    b.setValue(256);
    cout << "2) a.value = " << a.value() << ", b.value = " << b.value() << endl;

    return 0;
}

[6] 最後に、このCounterクラスのインスタンスを2つ作成し、誰のシグナルを誰につなげるというconnect文を記述します。

これで実行すると、以下のような結果になります。

  1) a.value = 1024, b.value = 1024 
  2) a.value = 1024, b.value = 256

aの値が変化した場合は、bに通知するようにしていますが、bの値の変化についてはconnectしていないため、誰にも通知されません。ですので、a.setValue(1024)はb.setValue(1024)が行われ、b.setValue(256)はaに影響を与えていないのです。

connect式ですが、Qt5からは以下のような書き方もできるようになりました。

Object(&b, &Counter::valueChanged, &a &Counter::setValue);

この一文を付け加えると、bの変化がaに通知され値が同じになります。

解説

これの何が便利なのかというと、同一クラスに限らず、同じ型を引数に持つシグナルとスロットであれば同じことが可能なのです。

QSliderクラスとQSpinBoxに同じようなvalueChanged シグナルとsetValueスロットがありますが、こちらでも同じことができ、なおかつ、QSliderもQSpinBoxもクラスの実装上、お互いのことを全く知らない状態を保つことが可能です。

さらに、このシグナルとスロットは、同一スレッドではその場でメタオブジェクト経由での通常の関数呼び出しに置き換わりますが、異なるスレッド間の場合は、メッセージキューとイベントループを使ったメッセージ通信が利用され、スレッドの異なるオブジェクト間の通信にも利用できるのです。

プロパティ

プロパティはオブジェクト指向における属性を意味する用語で、C++では通常メンバ変数のことを意味しています。しかし、メンバ変数はコンパイル時に確定していなくてはならず、またQMLやJavaScriptエンジンとの組み合わせて利用するには手がかかるため、Qtではメタオブジェクトを利用する独自のプロパティシステムを用意しています。

Qtのプロパティシステムは、セッター、ゲッターと呼ばれる設定、参照のための関数を用意し、それを経由でアクセスすることをメタ情報として保持します。これによりQMLやJavaScriptからプロパティにアクセス出来るようになります。また、Qt Creatorの提供するデザイナの機能もこのメタオブジェクトを利用しており、独自ウィジェットをデザイナからプロパティ設定出来るようにするためにもこのメタオブジェクトが利用されます。

Qtのプロパティシステムには2つの種類があり、1つはQ_PROPERTYマクロを使って静的に定義するプロパティと、事前定義をせず、セッターであるsetProperty, ゲッターであるpropertyを使って生成・削除と読み出しを行う動的プロパティがあります。

普通のメンバ変数と比べると少々手はかかりますが、QMLとの連携などの重要な役割を担っています。

Qt Remote Objectとは

Qt Remote Objectは、Qt向けに開発されたプロセス間通信(IPC)機構です。Qt基盤とも言えるQObjectを拡張し、シグナル、スロット、プロパティといった機能をプロセス間あるいはデバイス越しにでも利用出来ることを目的としています。

まずはドキュメントに出てくる用語を整理しておきます。

用語

  • レプリカ
    レプリカオブジェクト。レプリカは複製を意味しています。Qt Remote Objectのコピーで、スロットの呼び出しは、オリジナルであるソースに転送されます。
  • ソース
    ソースオブジェクト。複製元となるQt Remote Objectをソースと呼びます。ソースの変更はレプリカへと転送されます
  • ノード
    レプリカとソースで情報を受け渡すデータチャネルのエンドポイントです。
  • ホストノード
    ホストノードは、他のノードにソースオブジェクトを公開するノード
  • クライアントノード
     ドキュメントにはない用語ですが、exampleではホストではない側をClientと記述しているため、ホストノードではないノードをクライアントノードと呼称しておきます。
  • レジストリ ノードの接続仲介者です
  • RECP
    Replica Compilerの略で、repと呼ぶドメイン固有言語を元に、ソース用ヘッダを生成します。
  • 静的レプリカ(公式ドキュメントでは静的ソース)
    recpされたヘッダを使いレプリカコードを静的に作成するレプリカです。
  • 動的レプリカ
    ソースの方を知らず、レプリカの初期化の中でソースのAPIを動的に追加するレプリカです
  • ダイレクト接続
    ノード間の接続方法で、直接ホストのURLを指定して接続する方法です
  • レジストリ接続
    ホストノード、ノードともレジストリに問い合わせることでノードをリンクする接続方法です

関連性

  • 処理を受け持つオブジェクトは、ソースと呼ばれ、ホストノードとして、他のノードからの接続を待ち受けます。
  • Qt Remote Objectを利用する側は、ノードを用意し、ホストノードへ接続して、ソースの複製であるレプリカを作成します。
  • 接続する方法には、ホストノードのURLを指定する直接接続と、レジストリに仲介を依頼するレジストリ接続があります。
  • ソースはAPIをrepと呼ぶ独自のインターフェース記述言語をrecpでビルドし、QObjectのメタオブジェクトを拡張するヘッダファイルを生成します。
  • レプリカは、ソースヘッダを読み込む静的レプリカと、実行時に初期化されてAPIを読み込む動的レプリカがあります

さっそく試してみよう

1. ソースオブジェクト側を作成する

1.1 コンソールアプリケーションプロジェクトの作成

まずは、新しいプロジェクトを作成します。名前はなんでもいいですが、私はqtro_hostにしました。
コンソールアプリケーションである必要性はないのですが、GUIを作る手間を省くためコンソールアプリケーションにしています。

スクリーンショット 2017-12-15 12.52.51.png

1.2 repファイルの作成

repファイル用のエディタやテンプレートは用意されていないようなので、まずは空ファイルとして作成します。名前は公式ドキュメントのGetting Startedと同じsimpleswitch.repにしておきます。

スクリーンショット 2017-12-15 12.56.19.png

スクリーンショット 2017-12-15 12.57.12.png

simpleswitch.rep の中身は以下の通りです。

simpleswitch.rep
#include <QtCore>

class SimpleSwitch
{
    PROP(bool currState=false);
    SLOT(void server_slot(bool clientState));
};

1.3 .proファイルの修正

続いて、プロジェクトファイルを修正し、Remote Objectの使用宣言と、simpleswitch.repがRECP ファイルであると認識させます。

qtro_host.pro
QT -= gui
QT += remoteobjects

CONFIG += c++11 console
CONFIG -= app_bundle

# The following define makes your compiler emit warnings if you use
# any feature of Qt which as been marked deprecated (the exact warnings
# depend on your compiler). Please consult the documentation of the
# deprecated API in order to know how to port your code away from it.
DEFINES += QT_DEPRECATED_WARNINGS

# You can also make your code fail to compile if you use deprecated APIs.
# In order to do so, uncomment the following line.
# You can also select to disable deprecated APIs only up to a certain version of Qt.
#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000    # disables all the APIs deprecated before Qt 6.0.0

SOURCES += main.cpp simpleswitch.cpp
HEADERS += simpleswitch.h


REPC_SOURCE = simpleswitch.rep

変更箇所は以下の2点です

QT += remoteobjects
REPC_SOURCE = simpleswitch.rep

なお、REPC_SOURCEはリストではなく単体出なくてはならないようで += を使うと正しく認識されませんでした。

1.4 リモートオブジェクトの実装

続いて、リモートオブジェクトソースの宣言と定義を行います。手抜きのためにまずはQObjectを継承するクラスとして作成しましょう。クラス名はSimpleSwitchです。

スクリーンショット 2017-12-15 13.47.04.png

スクリーンショット 2017-12-15 13.47.25.png

自動的にプロジェクトに追加されるので、必要な修正を行いましょう。まずはヘッダファイルから

simpleswitch.h
#ifndef SIMPLESWITCH_H
#define SIMPLESWITCH_H

#include "rep_simpleswitch_source.h" // 変更

class QTimer; // 追加

class SimpleSwitch : public SimpleSwitchSimpleSource // 変更
{
    Q_OBJECT
public:
    explicit SimpleSwitch(QObject *parent = nullptr);
    ~SimpleSwitch(); // 追加
    virtual void server_slot(bool clientState); // 追加


public slots:
    void timeout_slot(); // 追加

private:
    QTimer *stateChangeTimer_; // 追加
};

#endif // SIMPLESWITCH_H
  • rep_simpleswitch_source.h はsimpleswitch.repから生成されるヘッダです
  • SimpleSwitchSimpleSource が生成されるヘッダで作られる基本クラスです。SimpleSwitchSourceだと思って小一時間悩みました。
  • repでスロットと定義したものはslotsには入れていません。
simpleswitch.cpp
#include "simpleswitch.h"
#include <QtCore/QTimer>
#include <QtCore/QDebug>

SimpleSwitch::SimpleSwitch(QObject *parent)
    : SimpleSwitchSimpleSource(parent) ,// 変更
      stateChangeTimer_(new QTimer(this))
{
    connect(stateChangeTimer_, &QTimer::timeout,
            this, &SimpleSwitch::timeout_slot);
    stateChangeTimer_->start(2000);
}

SimpleSwitch::~SimpleSwitch()
{
    stateChangeTimer_->stop();
}

void SimpleSwitch::server_slot(bool clientState)
{
    qDebug() << "Replica State is " << clientState;
}

void SimpleSwitch::timeout_slot()
{
    setCurrState(!currState());
    qDebug() << "Source State is " << currState();
}

実装はいたって単純で、コンストラクタでタイマーを作り、2秒おきにプロパティのcurrState をトグルしています。server_slotはレプリカント向けに用意されたスロットで、呼び出されたらデバッグ文を表示するだけです。

なお、repで定義したプロパティの場合、以下の機能が自動で定義されます。

  • setter : void setCurrState(bool val);
  • getter : bool currState()
  • notify : void currStateChanged(bool val);

1.5 ソースオブジェクトのインスタンス作成とホストノード作成

最後にmain.cppでインスタンスの作成と、ホストノードの作成を行います。

main.cpp
#include <QCoreApplication>
#include <QtRemoteObjects/QRemoteObjectHost>
#include "simpleswitch.h" /* 追加 */

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);
    SimpleSwitch simpleSwitch; // 追加 : Remote Objectの生成

    QRemoteObjectHost host(QUrl(QStringLiteral("local:replica"))); // 追加
    host.enableRemoting(&simpleSwitch);


    return a.exec();
}

ノードは、QUrlでURLを使って接続方法を定義するのですが、一般的なURLではない特殊な記述をすることに注意が必要です。

起動すると、2秒おきにフラグの値が変わり、画面に表示されます。
スクリーンショット 2017-12-15 16.31.14.png

2. 静的レプリカの生成

2.1 コンソールアプリケーションプロジェクトの作成

ホストノードと同じく、コンソールアプリケーションとしてレプリカ側も作成しましょう。
プロジェクト名は任意ですが、私はqtro_replica_staticとしました。

2.2 repファイルの登録

プロジェクトファイルに、1のプロジェクトで作成したrepファイルを登録します。

qtro_replica_static.pro
QT -= gui
QT += remoteobjects

CONFIG += c++11 console
CONFIG -= app_bundle

# The following define makes your compiler emit warnings if you use
# any feature of Qt which as been marked deprecated (the exact warnings
# depend on your compiler). Please consult the documentation of the
# deprecated API in order to know how to port your code away from it.
DEFINES += QT_DEPRECATED_WARNINGS

# You can also make your code fail to compile if you use deprecated APIs.
# In order to do so, uncomment the following line.
# You can also select to disable deprecated APIs only up to a certain version of Qt.
#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000    # disables all the APIs deprecated before Qt 6.0.0

SOURCES += main.cpp

REPC_REPLICA = ../qtro_host/simpleswitch.rep

変更箇所は以下の2点です

QT += remoteobjects
REPC_REPLICA = ../qtro_host/simpleswitch.rep

REPC_SOURCEではないことにご注意ください。これでビルドすると以下のヘッダファイルが生成されます。

rep_simpleswitch_replica.h

この中に生成されるのはレプリカ用クラスです

SimpleSwitchReplica

なお、提供されるのは以下の機能です。
- setter : void SimpleSwitchReplica::setCurrState(bool val);
- getter : bool SimpleSwitchReplica::currState()
- notify : void SimpleSwitchReplica::currStateChanged(bool val);
- slot : SimpleSwitchReplica::void server_slot(bool clientState)

2.3 クライアントノードの作成とレプリカの複製

main.cppでクライアントノードの作成とレプリカの複製を行います。

main.cpp
#include <QCoreApplication>
#include <QtRemoteObjects/QRemoteObjectNode> // 追加

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);
    QRemoteObjectNode clientNode; // 追加
    clientNode.connectToNode(QUrl(QStringLiteral("local:replica"))); // 追加

    QSharedPointer<SimpleSwitchReplica> replica(clientNode.acquire<SimpleSwitchReplica>()); // 追加

    return a.exec();
}

  • QRemoteObjectNode client; でclient nodeの作成
  • client.connectToNode(QUrl(QStringLiteral("local:replica"))); でホストと接続
  • client.acquire() でレプリカを複製してポインタ取得

これだけですと、レプリカを複製しただけで何もしないので、レプリカを操作するクラスを作ります。

2.4 レプリカ操作クラスの追加

QObjectを継承したクラスとして、Clientという名前のクラスを追加しましょう。手順自体は1.4と同じです。

スクリーンショット 2017-12-15 18.05.47.png

client.h
#ifndef CLIENT_H
#define CLIENT_H

#include <QtCore/QObject>
#include <QtCore/QSharedPointer>
#include "rep_simpleswitch_replica.h" /* 追加 */

class Client : public QObject
{
    Q_OBJECT
public:
    explicit Client(QSharedPointer<SimpleSwitchReplica>& replica, QObject *parent = nullptr);

signals:
    void echoSwitchState(bool switchState);

public slots:
    void recvSwitchState(bool state);

private:
    QSharedPointer<SimpleSwitchReplica> replica_;
};

#endif // CLIENT_H

レプリカの共有ポインタを受け取り保持できるようにし、操作用のシグナルとスロットを用意します。

client.cpp
#include "client.h"
#include <QDebug>

Client::Client(QSharedPointer<SimpleSwitchReplica> &replica, QObject *parent)
    : QObject(parent), replica_(replica)
{
    connect(replica_.data(), &SimpleSwitchReplica::currStateChanged,
            this, &Client::recvSwitchState);
    connect(this, &Client::echoSwitchState,
            replica_.data(), &SimpleSwitchReplica::server_slot);
}

void Client::recvSwitchState(bool state)
{
    qDebug() << "Recieve : " << state << "," << replica_->currState();
    emit echoSwitchState(state);
}

やっていることは少し回りくどいのですが

  1. ソースオブジェクトのプロパティ変更が転送されてきてSimpleSwitchReplica::currStateChangedシグナルが発生
  2. SimpleSwitchReplica::currStateChangedシグナルをClient::recvSwitchStateスロットで受信
  3. Client::rrecvSwitchStateスロットでClient::rechoSwitchStateシグナルを発信
  4. Client::rechoSwitchStateシグナルをSimpleSwitchReplica::server_slotで受信
  5. SimpleSwitchReplica::server_slotはソースオブジェクトに転送される

という作りになります。このクライアントの呼び出しをmain.cppに追加します。

main.cpp
#include <QCoreApplication>
#include <QtRemoteObjects/QRemoteObjectNode> 
#include "client.h" // 追加

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);
    QRemoteObjectNode clientNode;
    clientNode.connectToNode(QUrl(QStringLiteral("local:replica")));

    QSharedPointer<SimpleSwitchReplica> replica(clientNode.acquire<SimpleSwitchReplica>());
    Client client(replica);  // 追加
    return a.exec();
}

2.5 動作させると

ホストから先でもクライアントから先でも問題ないようです。ホストを起動するとタイマーによるスイッチが始まり、クライアントが起動するとホストの変化を受信し動作を開始します。

スクリーンショット 2017-12-15 23.07.18.png

3. 動的レプリカの生成

3.1 コンソールアプリケーションプロジェクトの作成

ホストノードと同じく、コンソールアプリケーションとしてレプリカ側も作成しましょう。
プロジェクト名は任意ですが、私はqtro_replica_dynamicとしました。

3.2 proファイルの修正

プロジェクトファイルは、remoteobjectsの利用だけを追加します。

qtro_replica_dynamic.pro
QT -= gui
QT += remoteobjects

CONFIG += c++11 console
CONFIG -= app_bundle

# The following define makes your compiler emit warnings if you use
# any feature of Qt which as been marked deprecated (the exact warnings
# depend on your compiler). Please consult the documentation of the
# deprecated API in order to know how to port your code away from it.
DEFINES += QT_DEPRECATED_WARNINGS

# You can also make your code fail to compile if you use deprecated APIs.
# In order to do so, uncomment the following line.
# You can also select to disable deprecated APIs only up to a certain version of Qt.
#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000    # disables all the APIs deprecated before Qt 6.0.0

SOURCES += main.cpp

3.3 クライアントノードの作成とレプリカの複製

main.cppでクライアントノードの作成とレプリカの複製を行います。
2と違うのは、repファイルを使わず動的レプリカにすることです。

main.cpp
#include <QCoreApplication>
#include <QtRemoteObjects/QRemoteObjectNode>
#include <QtRemoteObjects/QRemoteObjectDynamicReplica>

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);
    QRemoteObjectNode clientNode; // 追加
    clientNode.connectToNode(QUrl(QStringLiteral("local:replica"))); // 追加

    QSharedPointer<QRemoteObjectDynamicReplica>  replica(clientNode.acquireDynamic("SimpleSwitch"));

    return a.exec();
}

ノードの作成までは前回と同じですが、今回はacquireDynamic("SimpleSwitch")としてQRemoteObjectDynamicReplicaポインタを取得しています。
ホストからクライアントに渡されるメタオブジェクトはクラスの情報を文字列として保持するため、このように文字列で指定することになります。

3.4 レプリカ操作クラスの追加

2.4と同じく、QObjectを継承したクラスとして、Clientという名前のクラスを追加しましょう。

client.h
#ifndef CLIENT_H
#define CLIENT_H

#include <QtCore/QObject>
#include <QtRemoteObjects/QRemoteObjectDynamicReplica>

class Client : public QObject
{
    Q_OBJECT
public:
    explicit Client(QSharedPointer<QRemoteObjectDynamicReplica>& replica,QObject *parent = nullptr);

signals:
    void echoSwitchState(bool switchState);

public slots:
    void setup();
    void recvSwitchState(bool state);



private:
    QSharedPointer<QRemoteObjectDynamicReplica> replica_;
};

#endif // CLIENT_H

コンストラクタで受け取るレプリカの型が異なるのと、新しくsetup() スロットを追加した以外は、静的レプリカのケースと同じになっています。

client.cpp
#include "client.h"

Client::Client(QSharedPointer<QRemoteObjectDynamicReplica> &replica, QObject *parent)
    : QObject(parent), replica_(replica)
{
    QSignalBlocker block(*replica_);
    if (replica_->isInitialized()) {
        setup();
    } else {
        connect(replica_.data(), &QRemoteObjectDynamicReplica::initialized,
                this, &Client::setup);
    }
}

void Client::setup()
{
    connect(replica_.data(), SIGNAL(currStateChanged(bool)), SLOT(recvSwitchState(bool)));
    connect(this, SIGNAL(echoSwitchState(bool)), replica_.data(), SLOT(server_slot(bool)));
}

void Client::recvSwitchState(bool state)
{
    qDebug() << "Recieve : " << state << "," << replica_->property("currState").toBool();
    emit echoSwitchState(state);
}

Clientの実装側は、コンストラクタが大きく異なる。静的なレプリカは初めから型が分かっていたため、いきなり定義していたレプリカのシグナルとスロットをconnectしていました。それに対し、動的レプリカの場合、まずはメタ情報の取得と動的な生成から始まるため、初期化が終わるまでは利用できません。そこで、まずはシグナルをブロックしたうえで、動的レプリカの初期化状況を確認しています。

もしも、初期化が終わっていなければ、initialized()シグナルを待ち受けて、setup()を行うようにしています(ドキュメントに記載はないですが、こうした方が安全だと思います)。

Client::setup()でのconnect式が、静的レプリカの場合と異なりSIGNALとSLOT マクロを使っていることに注意してください。動的なレプリカでは型情報がないため、メタオブジェクトを使ってシグナルやスロット、プロパティの文字列で検索する必要があり、この旧来のconnect文が必要になります。

最後にこのクライアントのインスタンス生成をmain.cppに追加します。

main.cpp
#include <QCoreApplication>
#include <QtRemoteObjects/QRemoteObjectNode>
#include <QtRemoteObjects/QRemoteObjectDynamicReplica>
#include "client.h"

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);
    QRemoteObjectNode clientNode; // 追加
    clientNode.connectToNode(QUrl(QStringLiteral("local:replica"))); // 追加

    QSharedPointer<QRemoteObjectDynamicReplica>  replica(clientNode.acquireDynamic("SimpleSwitch"));
    Client client(replica);

    return a.exec();
}

3.5 動作させると

スクリーンショット 2017-12-15 23.12.33.png

結果表示自体は同じになるようにしていますので、実行結果は静的レプリカのケースと変わりません。心なしかちょっともっさりな気はしますが・・・。

動的レプリカは静的レプリカに比べると文字列を使った検索を伴いますし、C++では面倒な手間も増えますが、QMLの場合に有用なのだそうです。

接続方法

直接接続

ノードの作成時に、これまでURLを使っていました。

host
QRemoteObjectHost host(QUrl(QStringLiteral("local:replica"))); 
client
clientNode.connectToNode(QUrl(QStringLiteral("local:replica")));

ここで指定するURLは通常のURLとは異なっています。

URL Server Client
QUrl("local:replica") QLocalServer("replica") QLocalSocket("replica")
QUrl("tcp://192.168.1.1:9999") QTcpServer("192.168.1.1",9999) QTcpSocket("192.168.1.1",9999)

localの場合は名前付きパイプなどで、tcpの場合はTCPソケット通信で、ノード間が接続されます。
注意が必要なのは、これらのネットワークリソースは競合しいることです。実際にアプリケーションで利用する場合は、一意の名前や適切なポート番号を利用するよう設計する必要があります。

レジストリの利用

これまでの接続は、ホストで利用したノードの接続先を、クライアント側でも直接指定し接続していました。少数ノードの場合はこれで問題がありませんが、ノードが増えてきた場合、portや名前の管理が大変になります。そこで、それらを仲介するレジストリという機能が提供されています。

1. レジストリプロジェクトの作成

ドキュメントではソースオブジェクトと同時にレジストリも作成していますが、その辺りの制限は特にありません。単独でレジストリを起動しておくことも可能です。まずは、コンソールアプリケーションを作成します。
名前は、qtro_registryとしておきます。.proファイルはremoteobjectsを有効にしておいてください。

2. レジストリの作成

main.cppでレジストリノードを作成します。

main.cpp
#include <QCoreApplication>
#include <QtRemoteObjects/QRemoteObjectRegistryHost>

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);
    QRemoteObjectRegistryHost registry(QUrl(QStringLiteral("local:registry")));

    return a.exec();
}

3. ホストノードの修正

先のホストオブジェクトで、ホストノード作成の処理を以下のように書き換えます。

main.cpp
#include <QCoreApplication>
#include <QtRemoteObjects/QRemoteObjectHost>
#include "simpleswitch.h" /* 追加 */

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);
    SimpleSwitch simpleSwitch; // 追加 : Remote Objectの生成
#if 0
    QRemoteObjectHost host(QUrl(QStringLiteral("local:replica"))); // 追加
#else
    QRemoteObjectHost host(QUrl(QStringLiteral("local:replica")), QUrl(QStringLiteral("local:registry")));
#endif
    host.enableRemoting(&simpleSwitch);

    return a.exec();
}

4. クライアントノードの修正

クライアントノードの作成を以下のように書き換えます。

#if 0
    QRemoteObjectNode clientNode;
    clientNode.connectToNode(QUrl(QStringLiteral("local:replica")));
#else
    QRemoteObjectNode clientNode(QUrl(QStringLiteral("local:registry")));
#endif

これで起動すると、URLとしてレジストリ相手に接続に行き、結果としてホストノードと接続されます。

まとめ

Qt Remote Objectのようなプロセス間通信は、世の中一般的にはリモートプロシージャコール(RPC)と呼ばれています。一般的なRPCは、クライアントは要求を出した後、サーバーの応答を待ちます。逆にサーバー側はクライアントの要求がない限りは何も送りません。
これに対し、Qt Remote Objectは、クライアントの変化は暗黙的にサーバーの送られ、サーバーの変化はクライアントに送られるという挙動をする点に注意が必要です。逆にいうと、要求と応答という挙動ではなく、サーバー側とクライアント側で、あたかも同じオブジェクトを操作しているという、まさにQObject間のメッセージ機構を、そのままネットワーク越しに行うかのような体験が可能です。

ドキュメントには出てきませんが、QAbstractItemModelを継承したモデルの場合、repファイルなしのやり取りも用意されています。Exampleのremoteobjectsにサンプルコードが用意されているので、眺めてみるのも良いでしょう。

この機能はまだ技術プレビューの段階のため、本格的な機能の実装に使うことは推奨できません。パフォーマンスなどもチューニングしきれていない感じですし、ドキュメントの説明も不足しています。使い方やAPIもこの後変化があるかもしれません。

そんな状況ですが、軽いお試しで触ってみる程度の道しるべを書いたつもりですので、もしお時間があるようでしたら、冬の夜長の暇つぶしに、ちょっと触ってみるのも良いでしょう。なんとも不思議な感覚を味わうことができるかもしれませんよ。

明日は、@Donokono さんがグラフ関連で何か書いて下さるそうです。お楽しみに。