TL;DR
- QtDBus の使い方、C++ で書く。
- とりあえずメソッドを実行できればいいレベル
- シグナル/スロットも使える
はじめに
Linuxでいろいろシステムを作っているとIPC(プロセス間通信)が必要になるときがあります。
世の中にはいろいろなIPCがありますが、
- 少数のプロパティの操作
- メソッドの一方的な呼び出し
- シグナルの送受信
程度の簡単なものなら最近のディストリビューションなら動いているであろう D-Bus で十分可能です。
自分は Qt を使う都合上、QtDBus を使って書くことが多いのでその書き方を見てみましょう。
公式
- QtDBus
QtDBus のサンプル
いまいちサンプルがわかりにくいので簡単に作ってみました。
Qt 5.15.9 と Qt 6.3.1 両方で動作確認してあります。
サンプルを使ってみる
サンプルは QtQucik で簡単な GUI をつけてあります。
$ git clone https://github.com/tkhshmsy/qtdbus-demo
$ mkdir build
$ cd build
$ /path/to/qmake -r ../qtdbus-demo/qtdbus-demo.pro
$ make
dbusserverを実行してからdbusclientを実行すると、D-Bus を介して接続されます。
(dbusclient はいくつ起動してもOKです)
- dbusclient の count の表示は dbusserver の count プロパティをとってきています。
- dbusclient の ’reset' や 'send' を使うと dbusserver 側の count プロパティを変更します。
- 変更されるとシグナルが通知され、dbusclient が再取得します。
- dbuserver で 'reset' しても変更時と同様な流れで dbusclient に影響します。
解説
QtDBus の部分をどう書いているのかみていきます。
サーバー側
まずはプロジェクトファイルに dbus を足しておきます。
これで QtDBus モジュールが使用可能になります。
QT = gui quick qml dbus
宣言部(dbusserver.h)
D-Busへの接続情報(Service, Path, Interface)を書いておきます。
このへんが実際どうみえるのかは後述します。
class DBusServer : public QObject
{
Q_OBJECT
Q_CLASSINFO("D-Bus Service", "local.myhost")
Q_CLASSINFO("D-Bus Path", "/QtDBusDemo/DBusServer")
Q_CLASSINFO("D-Bus Interface", "api.dbusserver")
続いて、公開するメソッドや変数ですが、QtQuickへ公開する宣言と同じ書き方になります。
まず、プロパティ(変数の読み書き等のセット)を公開するには Q_PROPERTY で宣言しておきます。
Q_PROPERTY(int count READ count WRITE setCount NOTIFY countChanged) // export
メソッドを公開するには Q_INVOKABLE をつけて宣言しておきます。
Q_INVOKABLE をつけないと Q_PROPERTY で使うメソッドであっても公開されないことに注意しましょう。
public:
...
int count() const; // NOT export
Q_INVOKABLE void reset(); // export
シグナルやスロットの公開もできます。
こちらは Q_INVOKABLE をつけなくてOK。
シグナルは普通に emit すれば D-Bus 側にも通知されます。簡単ですね。
public Q_SLOTS:
void setCount(int newCount); // exprot
Q_SIGNALS:
void countChanged(int newCount); // export
定義部(dbusserver.cpp)
あとはこのインスタンスを D-Bus 上に登録するだけです。
(サンプルはセッションバスを使ってます)
class DBusServer::Private
{
public:
// if running on systemBus, needs configuration in /etc/dbus-1/system.conf
QDBusConnection bus = QDBusConnection::sessionBus();
...
};
DBusServer::DBusServer(QObject *parent)
: QObject{parent}
, d{new Private}
{
d->bus.registerService("local.myhost"); // from D-Bus Service
d->bus.registerObject("/QtDBusDemo/DBusServer", // from D-Bus Path
"api.dbusserver", // from D-Bus Interface
this,
QDBusConnection::ExportAllContents);
}
で、どうなってる?
こうなってます。設定したものが公開されてますね。
- プロパティ count
- integer型で読み書き可能
- countChanged シグナル
- integer型の戻り値1つ
- setCount メソッド(、というかスロット)
- integer型の引数1つ
- reset メソッド
- 引数なし
$ dbus-send --session --print-reply --type=method_call --dest=local.myhost /QtDBusDemo/DBusServer org.freedesktop.DBus.Introspectable.Introspect
method return time=1662280080.064612 sender=:1.170 -> destination=:1.174 serial=11 reply_serial=2
string "<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN"
"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
<node>
<interface name="api.dbusserver">
<property name="count" type="i" access="readwrite"/>
<signal name="countChanged">
<arg name="newCount" type="i" direction="out"/>
</signal>
<method name="setCount">
<arg name="newCount" type="i" direction="in"/>
</method>
<method name="reset">
</method>
</interface>
<interface name="org.freedesktop.DBus.Properties">
...
</interface>
</node>
"
クライアント側
次はクライアント側。
こちらもプロジェクトファイルに dbus を足しておきます。
QT = gui quick qml dbus
宣言部
特に必要ありません。
定義部(dbusclient.cpp)
メソッドを呼ぶ
QDbusInterface を使います。
QDBusInterface は QMetaObject の仕組みで動いているのでその機能を使ってみます。
QDBusInterface *iface = Q_NULLPTR;
iface = new QDBusInterface("local.myhost", // from D-Bus Service
"/QtDBusDemo/DBusServer", // from D-Bus Path
"api.dbusserver", // from D-Bus Interface
QDBusConnection::sessionBus());
QMetaObject::invokeMethod(iface, "reset");
プロパティ count へのアクセスする場合はこんな感じ。
## write property
int newCount = 123;
iface->setProperty("count", QVariant(newCount));
## read property
int count = iface->property("count").toInt();
これらを call() を使って直接呼び出すような書き方だとこうなります。
この場合、プロパティへのアクセスは Interface が違うので注意。
## reset
QDBusReply<void> resetReply = iface->call("reset");
## setCount
QDBusReply<void> setCountReply = iface->call("setCount", newCount);
## read property
QDBusInterface *ifaceProperty = Q_NULLPTR;
ifaceProperty = new QDBusInterface("local.myhost", // from D-Bus Service
"/QtDBusDemo/DBusServer", // from D-Bus Path
"org.freedesktop.DBus.Properties",
QDBusConnection::sessionBus());
QDBusReply<QVariant> countReply = d->ifaceProperty->call("Get", "api.dbusserver", "count");
int count = countReply.value().toInt();
シグナルを受けてスロットを動かす
シグナル-スロットの接続は QDBusConnection で設定します。
D-Bus からくるシグナル countChanged(int) を this のスロット countChanged(int) に接続するとこんな感じ。
QObject の シグナル-スロットの connection の書き方と同じですね。スタイルが古いですが。
QDBusConnection bus = QDBusConnection::sessionBus();
bus.connect("local.myhost", // from D-Bus Service
"/QtDBusDemo/DBusServer", // from D-Bus Path
"api.dbusserver", // from D-Bus Interface
QStringLiteral("countChanged"),
this,
SLOT(countChanged(int)));
まとめ
QtDBus を使ってサーバーとクライアント両方を書いてみました。
単に外側からメソッド1個呼びたいだけ、といったケースにサクっと書いてサクっと使うならこれで十分かと思います。
実は、今回のやり方は「わりと手抜きな方法」です。きちんとやるなら、
- XMLで定義を書いて dbusxml2cpp で生成したコードを使う
- サーバー側とクライアント側でヘッダファイルを共有して齟齬がないようにする
などの「正しい手順、堅実な手順」があります。
これらのほうが使い勝手がよく、可読性もよかったりするのですが・・・それらはまた別の機会に。
D-Bus と QtDBus (その2:CLIから叩く) を書きました。
サンプルの dbusserver を CLI から叩いてみるお話です。