C++の応用 - D-Bus

提供:MochiuWiki - SUSE, Electronic Circuit, PCB
ナビゲーションに移動 検索に移動

概要

D-Busは、オープンソースのプロセス間通信(IPC:Inter Process Communication)機構であり、freedesktop.orgプロジェクトの一部である。
IPCとは、1台のコンピュータ上で動作する複数のプログラムの間で情報を交換するシステムのことである。

IPCには、パイプ、名前付きパイプ、シグナル、共有メモリ、Unixソケット、ループバックソケット等がある。
D-Busもリンク層はUnixソケットで動作しているが、手順とフォーマット(プレゼンテーション層)が既定されていることが、「生の」Unixソケットとは異なる。

開発当初はGNOME等のGUIの制御を目的としていが、今日では、GUIに限らず幅広いソフトウェアで使用されており、
デスクトップの通知、メディアプレーヤー制御、XDGポータル等、多くのfreedesktop.org標準がD-Busをベースに構築されている。

IPCとは、あるプロセスから別のプロセスへ情報を取得する方法を説明するために使用することができる。
これは、データの交換、メソッドの呼び出し、イベントのリスニング等がある。

  • デスクトップにおけるIPCの使用例
    • スクリプト(ユーザが共通の環境でスクリプトを実行して、実行中の様々なソフトウェアと対話または制御する)
    • 集中型サービスへのアクセスの提供
    • 協調型ソフトウェアの複数のインスタンス間の調整


  • D-Busの使用例
    • freedesktop.orgのnotification仕様
      これは、ソフトウェアは通知を中央サーバ(Plasma等)に送信して、中央サーバは通知を表示して、通知が閉じられたりアクションが実行されたりといったイベントを送り返すものである。


  • IPCの他の使用例
    • ユニークなソフトウェアのサポート
      これは、ソフトウェアの起動時に、まず、同じソフトウェアの他の実行中のインスタンスを確認して、
      もし存在すれば、IPCを介して実行中のインスタンスにメッセージを送信して、自分自身を表示して終了させる。


D-Busは、言語やツールキットに囚われないため、あらゆるプロバイダのソフトウェアやサービスが相互作用することができる。
デーモン(軽量サービスプロバイダ)とそれを利用したいソフトウェアが、必要なサービス以上のことを知らなくても通信できるようにするためによく利用される。

D-Busに仕様を詳しく知りたい場合は、freedesktopの公式Webサイトを参照すること。


sdbus-c++ライブラリ

sdbus-c++ライブラリとは

sdbus-c++ライブラリは、C++で理解できるAPIを提供するように設計されたLinux向けの高水準C++ D-Busライブラリである。
SystemdによるC D-Bus実装であるsd-busの上に、抽象化の別のレイヤーを追加する。

sdbus-c++ライブラリは、主にdbus-c++を置き換えるものとして開発されている。
dbus-c++は、現在多くの未解決のバグ、並行性の問題、固有の設計の複雑さと制限に悩まされている。

sdbus-c++ライブラリはsd-busライブラリを使用しているが、必ずしもSystemdに制約されているわけではなく、Systemd以外の環境でも完璧に使用することができる。

sdbus-c++ライブラリの詳細な使用方法を知りたい場合は、sdbus-c++ライブラリの公式Webサイトを参照すること。

sdbus-c++ライブラリのライセンス

sdbus-c++ライブラリは、LGPL 2.1でライセンスされている。
ただし、ライブラリのヘッダファイルではLGPL Exception version 1.0ライセンスもあり、これは、以下に示すようなことを許可している。

LGPL 2.1に対する追加的な許可として、「ライブラリを使用する作品」のオブジェクトコード形式は、ライブラリの一部であるヘッダファイルの素材を組み込んでもよい。
あなたは、そのようなオブジェクトコードを、あなたが選択した条件の下で頒布することができる。

(i) ライブラリのヘッダファイルが変更されていないこと。
(ii) 組み込まれる内容が、数値パラメータ、データ構造のレイアウト、アクセサ、マクロ、インライン関数、およびテンプレートに限定されていること。
(iii) LGPL 2.1の第6項の条項を遵守していること。

ただし、そのような改変が、
(i) 数値パラメータ
(ii) データ構造のレイアウト
(iii) アクセサ
(iv) 長さが5行以下の小さなマクロ、テンプレート、インライン関数に限定される場合
はこの限りではない。

さらに、ライブラリの改変バージョンにこの追加許可を適用する必要ない。


sdbus-c++ライブラリのインストール

パッケージ管理システムからインストール
# SUSE
sudo zypper install sdbus-cpp-devel


ソースコードからインストール

sdbus-c++ライブラリのビルドに必要なライブラリをインストールする。

# SUSE
sudo zypper install pkg-config make cmake gcc gcc-c++ libcap-devel libexpat-devel libmount-devel systemd-devel
                    doxygen  # ドキュメントもビルドする場合


sdbus-c++ライブラリのGithubにアクセスして、ソースコードをダウンロードする。
ダウンロードしたファイルを解凍する。

tar xf sdbus-cpp-<バージョン>.tar.gz
cd sdbus-cpp-<バージョン>


または、git cloneコマンドを実行して、ソースコードをダウンロードすることもできる。

git clone https://github.com/Kistler-Group/sdbus-cpp.git
cd sdbus-cpp


sdbus-c++ライブラリをビルドおよびインストールする。

cmake -DCMAKE_C_COMPILER=<GCC 8以降のGCCコンパイラのパス>   \
      -DCMAKE_CXX_COMPILER=<G++ 8以降のG++コンパイラのパス> \
      -DCMAKE_INSTALL_PREFIX=<sdbus-c++ライブラリのインストールディレクトリ>  \
      -DCMAKE_BUILD_TYPE=Release        \
      -DSDBUSCPP_BUILD_CODEGEN=ON       \
      -DSDBUSCPP_BUILD_EXAMPLES=ON      \
      -DSDBUSCPP_BUILD_DOCS=ON          \
      -DSDBUSCPP_BUILD_DOXYGEN_DOCS=ON  \  # Doxygenでドキュメントを生成する場合
      ..

make -j $(nproc)
make install


使用例 : Systemdサービスユニットの開始

以下の例では、sdbus-c++ライブラリを使用して、sudo systemctl start smbコマンドと同等の操作を行っている。
D-Bus経由でSystemdと直接通信するため、より柔軟で、プログラム内から制御できるようになっている。

ただし、サンプルコードを実行するには、適切な権限が必要となる。

 #include <iostream>
 #include <string>
 #include <sdbus-c++/sdbus-c++.h>
 
 // Systemdサービスはシグナルを送信しないため、今回は使用しない
 // #include <atomic>
 // #include <chrono>
 // #include <thread>
 
 // std::atomic<bool> g_keep_running(false);
 
 // UnitStateChangedシグナルハンドラ
 // ただし、Systemdサービスはシグナルを送信しないため、今回は定義しない
 // void onUnitStateChanged(sdbus::Signal signal)
 // {
 //     std::string unitName, newState, oldState;
 //     signal >> unitName >> newState >> oldState;
 //     std::cout << "Unit " << unitName << " changed state from " << oldState << " to " << newState << std::endl;
 
 //     if (unitName == "smb.service" && (newState == "active" || newState == "failed")) {
 //         g_keep_running.store(true);
 //     }
 // }
 
 // smb.serviceが正常に開始されているかどうかを確認する
 bool checkServiceStatus(sdbus::IProxy &systemdProxy, sdbus::InterfaceName &interfaceName, const std::string &serviceName)
 {
    try {
       // smb.serviceのユニットパスを取得する
       sdbus::MethodName methodName{"GetUnit"};
       auto method = systemdProxy.createMethodCall(interfaceName, methodName);
       method << serviceName;
       auto reply = systemdProxy.callMethod(method);
 
       sdbus::ObjectPath unitPath;
       reply >> unitPath;
 
       // 取得したsmb.serviceのユニットパスを使用して、smb.serviceの開始状態を取得
       /// D-Busサービス名は、"org.freedesktop.systemd1"
       /// D-Busオブジェクト名は、smb.serviceのユニットパス
       auto unitProxy = sdbus::createProxy(systemdProxy.getConnection(), std::move(sdbus::ServiceName{"org.freedesktop.systemd1"}), unitPath);
 
       /// D-Busインターフェース名は、"org.freedesktop.DBus.Properties"
       /// D-Busインターフェースメソッド名は、"Get"
       auto getProperty = unitProxy->createMethodCall(sdbus::InterfaceName{"org.freedesktop.DBus.Properties"}, sdbus::MethodName{"Get"});
       
       /// D-Busインターフェースメソッド"Get"の引数を指定
       getProperty << "org.freedesktop.systemd1.Unit" << "ActiveState";
 
       /// D-Busインターフェースメソッドを実行して、戻り値を取得
       auto propertyReply = unitProxy->callMethod(getProperty);
 
       /// 戻り値は文字列型であるため、文字列型に変換
       /// smb.serviceが正常に開始されている場合は、文字列は"active"となる
       sdbus::Variant variant;
       propertyReply >> variant;
       std::string state = variant.get<std::string>();
 
       return (state == "active");
    }
    catch (const sdbus::Error &e) {
       // sdbus::Errorをキャッチしてエラーメッセージを表示
       std::cerr << "Error checking service status: " << e.getName() << " - " << e.getMessage() << std::endl;
       return false;
    }
    catch (const std::exception &e) {
       // std::exceptionをキャッチして一般エラーを処理
       std::cerr << "Error: " << e.what() << std::endl;
       return false;
    }
 }
 
 int main(int argc, char *argv[])
 {
    // システムバスへの接続を作成
    auto connection = sdbus::createSystemBusConnection();
 
    // Systemdサービスのマネージャーインターフェースへのプロキシオブジェクトを作成
    sdbus::ServiceName destination("org.freedesktop.systemd1");
    sdbus::ObjectPath  objectPath("/org/freedesktop/systemd1");
    auto systemdProxy = sdbus::createProxy(*connection, std::move(destination), std::move(objectPath));
 
    // D-Busインターフェース名を指定
    sdbus::InterfaceName interfaceName("org.freedesktop.systemd1.Manager");
 
    // UnitStateChangedシグナルのハンドラを登録
    // ただし、Systemdサービスはシグナルを送信しないため、今回は設定しない
    //sdbus::SignalName    signalName("UnitStateChanged");
    //systemdProxy->registerSignalHandler(interfaceName, signalName, &onUnitStateChanged);
 
    try {
       // "StartUnit"はSystemdサービスのメソッド名
       sdbus::MethodName methodName{"StartUnit"};
       auto method = systemdProxy->createMethodCall(interfaceName, methodName);
 
      // Systemdサービスはシグナルを送信しないため、今回は定義しない
       //systemdProxy->uponSignal("JobRemoved")
       //              .onInterface("org.freedesktop.systemd1.Manager")
       //              .call(&onUnitStateChanged);
 
       // D-Busサービスを実行する
       // "smb.service"は開始するSystemdサービス名
       // "replace"はSystemdサービスの起動モード
       method << "smb.service" << "replace";
       auto reply = systemdProxy->callMethod(method);
 
       // または、以下に示す方法でもD-Busサービスを実行することができる
       //systemdProxy->callMethod("StartUnit")
       //              .onInterface("org.freedesktop.systemd1.Manager")
       //              .withArguments("smb.service", "replace");
 
       // Systmedサービスでsmb.service (D-Busサービス) を開始する場合は、ジョブ番号の値が返る
       sdbus::ObjectPath jobPath;
       reply >> jobPath;
       std::cout << "StartUnit job path: " << jobPath << std::endl;
 
       // イベントループを開始
       // Systemdサービスはシグナルを送信しないため、今回は使用しない
       //while (g_keep_running) {
       //   connection->processPendingEvent();
       //   std::this_thread::sleep_for(std::chrono::milliseconds(100));  // 100[mS]のスリープ
       //                                                                 // これにより、CPUの過剰な使用を避けつつ適度な応答性を維持
       //}
 
       // smb.service (D-Busサービス) が正常に開始されたかどうかを確認する
       std::string serviceName = "smb.service";
       if (checkServiceStatus(*systemdProxy, interfaceName, serviceName)) {
          std::cout << "Service " << serviceName << " state: " << "avtive" << std::endl;
       }
    }
    catch (const sdbus::Error &e) {
       // sdbus::Errorをキャッチしてエラーメッセージを表示
       std::cerr << "Error starting smb.service: " << e.getName() << " - " << e.getMessage() << std::endl;
       return -1;
    }
    catch (const std::exception &e) {
       // std::exceptionをキャッチして一般エラーを処理
       std::cerr << "Error: " << e.what() << std::endl;
       return -1;
    }
 
    return 0;
 }


processPendingEventメソッドの意味を以下に示す。

  • イベント処理
    D-Busシステムから到着した保留中のイベントを1つ処理する。
    これには、受信したシグナルやメソッド呼び出しの応答などが含まれる。
  • ノンブロッキング動作
    processPendingEventメソッドは、通常、ノンブロッキングで動作する。
    つまり、処理すべきイベントがない場合、即座に制御を返す。
  • シングルイベント処理
    1回の呼び出しで1つのイベントのみを処理する。
    複数のイベントを処理するには、このメソッドを繰り返し呼び出す必要がある。


使用例 : ユーザ定義のD-Busサービスの実行

D-Busサービス

以下に示すようなユーザ定義のD-Busサービスがあるとする。

  • D-Busサービス名
    org.example.mochiu
  • D-Busオブジェクト名
    /org/example/mochiu
  • D-Busインターフェース名
org.example.mochiu.method
  • D-Busインターフェースメソッド名
    func1
    • func1の引数
      第1引数 int型
      第2引数 std::stringクラスの参照
    • func1の戻り値
      int型


上記のD-Busサービスにおいて、指定したD-Busサービス、オブジェクト、インターフェース、メソッドを使用して実行している。

D-Busインターフェースの定義ファイル (XMLファイル)

また、D-Busインターフェースの定義ファイル (XMLファイル) は、以下に示すようなものとする。

 <!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN"
 "http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
 <node>
   <!-- D-Busインターフェースの定義 -->
   <interface name="org.example.mochiu.method">
     <!-- func1メソッドの定義 -->
     <!-- func1メソッドは、2つの入力引数 (int型とstd::string型) を受け取り、int型の値を返すことを定義している -->
     <method name="func1">
       <!-- 第1引数 : int型 -->
       <arg name="arg1" type="i" direction="in"/>
       <!-- 第2引数 : std::string型 (ここでは参照型として使用する) -->
       <arg name="arg2" type="s" direction="in"/>
       <!-- 戻り値 : int型 -->
       <arg name="result" type="i" direction="out"/>
     </method>
   </interface>
 </node>


std::stringクラスの参照渡しは明示的に行っていないが、これは、sdbus-c++ライブラリが内部で適切に処理するためである。
sdbus-c++ライブラリは効率的な引数の受け渡しを行うよう設計されているため、大きなオブジェクトは自動的に参照として扱われる。

sdbus-c++-xml2cppツール (スタブジェネレータ) の実行

次に、上記のDBusインターフェース定義ファイルからC++ヘッダファイルを自動生成する。

sdbus-c++ライブラリには、XMLファイルからC++のソースコードを生成するツールであるsdbus-c++-xml2cppツール (スタブジェネレータ) が存在する。
このツールは、D-Busインタフェースを記述したXMLファイルを受け取り、そのインタフェースを経由して呼び出すことができるC++ヘッダファイルを自動生成するものである。
この自動生成されたC++ヘッダファイルは、D-Bus経由の呼び出しをサービスのプロバイダに転送して、結果を呼び出し元に返す役割がある。

サービスを実装するサーバは、--adaptorオプションで生成されたヘッダファイルのインターフェイスクラスから派生したクラスを定義して、そのメソッドを実装する必要がある。

以下のコマンド例は、sdbus-c++-xml2cppツールを実行して、D-Busのサーバ側 (ヘルパー実行ファイル) のC++ヘッダファイルを自動生成している。

sdbus-c++-xml2cpp <D-Busインターフェース定義ファイル名 (XMLファイル)> \
                  --adaptor=<生成するアダプタのヘッダファイル名>

# 例 :
sdbus-c++-xml2cpp org.example.mochiu.xml    \
                  --adaptor=MochiuAdaptor.h


D-Busのサーバ側 (ヘルパー実行ファイル)

次に、D-Busのサーバ側 (ヘルパー実行ファイル) を作成する。

上記で自動生成されたC++ヘッダファイルを、ヘルパー実行ファイルのソースコードにインクルードする。

 // 例: MochiuHelper.hファイル
 
 #include "MochiuAdapter.h"  // 生成されたアダプタヘッダをインクルード


 // 例: MochiuHelper.hファイル
 
 #ifndef MOCHIUHELPER_H
 #define MOCHIUHELPER_H
 
 #include <sdbus-c++/sdbus-c++.h>
 #include <iostream>
 #include "MochiuAdapter.h"  // 生成されたアダプタヘッダをインクルード
 
 // アダプタ名を明示的に指定しない場合は、以下に示すようなクラス継承の記述でも可能
 // class MochiuHelper : public org::example::mochiu::method_adaptor
 class MochiuHelper : public MochiuAdaptor
 {
 public:
    MochiuHelper(sdbus::IConnection& connection, std::string objectPath) : MochiuAdaptor(connection, std::move(objectPath))
    {
    }
 
    // func1メソッドの実装
    int func1(const int32_t& arg1, const std::string& arg2) override
    {
       std::cout << "func1 called with args: " << arg1 << ", " << arg2 << std::endl;
       return arg1 * 2; // 例: 入力値の2倍を返す
    }
 };
 
 #endif // MOCHIUHELPER_H


 // 例: MochiuHelper.cppファイル
 
 #include <sdbus-c++/sdbus-c++.h>
 #include <iostream>
 #include <future>
 #include <chrono>
 #include "MochiuHelper.h"
 
 // イベントループを実行し、60秒後に終了するための関数
 void runEventLoopWithTimeout(std::shared_ptr<sdbus::IConnection> connection)
 {
    // 60秒後に終了するための非同期タスクを作成
    auto future = std::async(std::launch::async, []{
       std::this_thread::sleep_for(std::chrono::seconds(60));
    });
 
    // イベントループを開始
    while (future.wait_for(std::chrono::seconds(0)) == std::future_status::timeout) {
       connection->processPendingRequest();
    }
 
    std::cout << "60 seconds timeout reached. Exiting..." << std::endl;
 }
 
 int main()
 {
    try {
       // システムバスを接続する場合
       auto connection = sdbus::createSystemBusConnection();
 
       // セッションバスを接続する場合
       //auto connection = sdbus::createSessionBusConnection();
 
       // D-Busサービス名を指定
       connection->requestName("org.example.mochiu");
 
       // D-Busオブジェクトの作成
       MochiuHelper mochiuHelper(*connection, "/org/example/mochiu");
 
       // タイムアウト付きでイベントループを実行
       runEventLoopWithTimeout(connection);
    }
    catch (const sdbus::Error& e) {
       std::cerr << "D-Bus error: " << e.what() << std::endl;
       return -1;
    }
    catch (const std::exception& e) {
       std::cerr << "Error: " << e.what() << std::endl;
       return -1;
    }
 
    return 0;
 }


D-Busのクライアント側 (呼び出し側)
 #include <sdbus-c++/sdbus-c++.h>
 #include <iostream>
 #include <cstring>
 
 int main()
 {
    try {
       // システムバスへの接続を作成する場合
       auto connection = sdbus::createSystemBusConnection();
 
       // セッションバスへの接続を作成する場合
       //auto connection = sdbus::createSessionBusConnection();
 
       // 指定されたD-Busサービスとオブジェクトのプロキシオブジェクトを作成
       auto proxy = sdbus::createProxy(*connection,
                                       sdbus::ServiceName{"org.example.mochiu"},  // D-Busサービス名
                                       sdbus::ObjectPath{"/org/example/mochiu"}   // D-Busオブジェクト名
       );
 
       // func1メソッドの引数
       int arg1 = 42;
       std::string arg2 = "Hello, D-Bus!";
 
       // D-Busインターフェースメソッド (func1) を呼び出して、戻り値を受け取る
       auto result = proxy->callMethod(sdbus::MethodName{"func1"})                           // D-Busインターフェースメソッド名
                            .onInterface(sdbus::InterfaceName{"org.example.mochiu.method"})  // D-Busインターフェース名
                            .withArguments(arg1, arg2)                                       // 引数を指定
                            .returnValue<int>();                                             // 戻り値の型を指定
 
       std::cout << "Method 'func1' called successfully." << std::endl;
       std::cout << "Result: " << result << std::endl;
    }
    catch (const sdbus::Error &e) {
       // sdbus::Errorをキャッチしてエラーメッセージを表示
       std::cerr << "D-Bus error: " << e.what() << std::endl;
       return -1;
    }
    catch (const std::exception &e) {
       // std::exceptionをキャッチして一般エラーを処理
       std::cerr << "Error: " << e.what() << std::endl;
       return -1;
    }
 
    return 0;
 }


CMakeを使用する場合
 cmake_minimum_required(VERSION 3.16)
 project(<プロジェクト名>)
 
 set(CMAKE_CXX_STANDARD 17)
 
 find_package(sdbus-c++ REQUIRED)
 
 add_executable(<ターゲット名>
    main.cpp
 )
 
 target_link_libraries(<ターゲット名> PRIVATE
    SDBusCpp::sdbus-c++
 )