LoginSignup
0
0

ゼロコピーでプロセス間通信が可能なiceoryxの使い方:(その2)定周期通信

Posted at

テストコード

iceoryxの導入についてはゼロコピーでプロセス間通信が可能なiceoryxの使い方:(その1)導入をご参照下さい。本記事で示すコードは、本家に含まれるiceoryx_examples/icehelloを元にしています。

トピック定義用ヘッダファイルmytopic.h、publisherソースファイルhello_publisher.cpp、subscriberソースファイルhello_subscriber.cppの三つを作成します。いずれも短いコードなので、先に全体を示します。

mytopic.h

#ifndef MYTOPIC_H
#define MYTOPIC_H

#include <iostream>
#include <cstdint>
#include <cstring>

#define STRSIZ 8

struct MyTopic{
  enum{ MYTOPIC_TYPE_INT, MYTOPIC_TYPE_DOUBLE, MYTOPIC_TYPE_STR } type;
  union{
    int ivalue;
    double dvalue;
    char string[STRSIZ];
  } value;
 public:
  int set(int _ivalue){
    type = MYTOPIC_TYPE_INT;
    value.ivalue = _ivalue;
    return value.ivalue;
  }
  double set(double _dvalue){
    type = MYTOPIC_TYPE_DOUBLE;
    value.dvalue = _dvalue;
    return value.dvalue;
  }
  char *set(const char _str[]){
    this->type = MYTOPIC_TYPE_STR;
    strncpy( value.string, _str, STRSIZ );
    return value.string;
  }
  const char *typestr(){
    static const char *str[] = { "int   ", "double", "string" };
    return str[type];
  }
  void output() const {
    switch( type ){
    case MYTOPIC_TYPE_INT:    std::cout << "int,    value = " << value.ivalue; break;
    case MYTOPIC_TYPE_DOUBLE: std::cout << "double, value = " << value.dvalue; break;
    case MYTOPIC_TYPE_STR:    std::cout << "string, value = " << value.string; break;
    default: ;
    }
    std::cout << std::endl;
  }
};

#define SERVICE_NAME     "MyTopic"
#define SERVICE_INSTANCE "Hello"
#define SERVICE_EVENT    "Periodic"

#endif // MYTOPIC_H

hello_publisher.cpp

#include <iox/signal_watcher.hpp>
#include <iceoryx_posh/popo/publisher.hpp>
#include <iceoryx_posh/runtime/posh_runtime.hpp>
#include <iostream>
#include <cstring>

#include "mytopic.h"

void set_value(MyTopic &topic, int counter)
{
  switch( counter % 3 ){
  case 0: topic.set( counter ); break;
  case 1: topic.set( 3.14159 ); break;
  case 2: topic.set( "Hello" ); break;
  default: ;
  }
}

int main(int argc, char *argv[])
{
  constexpr char app_name[] = "hello-publisher";
  iox::runtime::PoshRuntime::initRuntime( app_name );
  iox::popo::Publisher<MyTopic> publisher( { SERVICE_NAME, SERVICE_INSTANCE, SERVICE_EVENT } );

  for(int counter=0; !iox::hasTerminationRequested(); counter++ ){
    auto loan_result = publisher.loan();
    if( loan_result.has_value() ){
      auto &topic = loan_result.value();
      set_value( *topic, counter );
      std::cout << "publish [" << counter << "] type = " << topic->typestr() << std::endl;
      topic.publish();
    } else{
      std::cerr << "failed to loan topic (#" << loan_result.error() << ")" << std::endl;
    }
    std::this_thread::sleep_for( std::chrono::seconds( 1 ) );
  }
  return EXIT_SUCCESS;
}

hello_subscriber.cpp

#include <iox/signal_watcher.hpp>
#include <iceoryx_posh/popo/subscriber.hpp>
#include <iceoryx_posh/runtime/posh_runtime.hpp>
#include <iostream>

#include "mytopic.h"

int main(int argc, char *argv[])
{
  constexpr char app_name[] = "hello-subscriber";
  iox::runtime::PoshRuntime::initRuntime( app_name );
  iox::popo::Subscriber<MyTopic> subscriber( { SERVICE_NAME, SERVICE_INSTANCE, SERVICE_EVENT } );

  while( !iox::hasTerminationRequested() ){
    auto take_result = subscriber.take();
    if( take_result.has_value() ){
      std::cout << "subscribed from ";
      ((MyTopic)*take_result.value()).output();
    } else{
      if( take_result.error() == iox::popo::ChunkReceiveResult::NO_CHUNK_AVAILABLE ){
        std::cerr << "topic not published." << std::endl;
      } else{
        std::cerr << "communication error." << std::endl;
      }
    }
    std::this_thread::sleep_for( std::chrono::milliseconds( 500 ) );
  }
  return EXIT_SUCCESS;
}

以下、順を追って中身を説明していきます。

トピック定義用ヘッダファイルmytopic.h

publisherが発行=subscriberが購読するトピックの構造を定義するヘッダファイルです。

構造体MyTopicは、共用体を使ってint型、double型、char配列の三種類を受け付けます(深い意味はありません)。どの型の値が収まっているかは列挙型で判別します。

  enum{ MYTOPIC_TYPE_INT, MYTOPIC_TYPE_DOUBLE, MYTOPIC_TYPE_STR } type;
  union{
    int ivalue;
    double dvalue;
    char string[STRSIZ];
  } value;

与えられた変数とその型を表す列挙型変数typeを同時にセットするメソッドset()をオーバーロードしています。

  int set(int _ivalue){
    type = MYTOPIC_TYPE_INT;
    value.ivalue = _ivalue;
    return value.ivalue;
  }
  double set(double _dvalue){
    type = MYTOPIC_TYPE_DOUBLE;
    value.dvalue = _dvalue;
    return value.dvalue;
  }
  char *set(const char _str[]){
    this->type = MYTOPIC_TYPE_STR;
    strncpy( value.string, _str, STRSIZ );
    return value.string;
  }

おまけで、格納されているデータの型を表す文字列出力関数typestr()と適当な出力メソッドoutput()も用意しました。

  const char *typestr(){
    static const char *str[] = { "int   ", "double", "string" };
    return str[type];
  }
  void output() const {
    switch( type ){
    case MYTOPIC_TYPE_INT:    std::cout << "int,    value = " << value.ivalue; break;
    case MYTOPIC_TYPE_DOUBLE: std::cout << "double, value = " << value.dvalue; break;
    case MYTOPIC_TYPE_STR:    std::cout << "string, value = " << value.string; break;
    default: ;
    }
    std::cout << std::endl;
  }

publisherとsubscriberは、サービス名・インスタンス名・イベント名(が何かは実はよく分かってないのですが)にそれぞれ対応する三つの文字列のユニークな組み合わせで特定されます。そこでこれらを、それぞれSERVICE_NAMESERVICE_INSTANCESERVICE_EVENTとして定義しておきます。

#define SERVICE_NAME     "MyTopic"
#define SERVICE_INSTANCE "Hello"
#define SERVICE_EVENT    "Periodic"

publisherソースファイルhello_publisher.cpp

publisherプログラムです。

set_value()はトピックに値をセットする関数です。

void set_value(MyTopic &topic, int counter)
{
  switch( counter % 3 ){
  case 0: topic.set( counter ); break;
  case 1: topic.set( 3.14159 ); break;
  case 2: topic.set( "Hello" ); break;
  default: ;
  }
}

中身には深い意味はありません。

iox::runtime::PoshRuntime::initRuntime()を呼んで、通信用のランタイムを初期化します(文字列app_nameは何でも良いです)。

  constexpr char app_name[] = "hello-publisher";
  iox::runtime::PoshRuntime::initRuntime( app_name );

MyTopicを継承したiox::pop::Publisherクラスを、テンプレートを用いて作成します。インスタンスには三つの文字列のユニークな組を割り当てます。subscriberはこれらを使って、自分が購読したいトピックのpublisherを特定します。

  iox::popo::Publisher<MyTopic> publisher( { SERVICE_NAME, SERVICE_INSTANCE, SERVICE_EVENT } );

周期的にトピック発行するループに入ります。終了判定に使っているiox::hasTerminationRequested()は、プロセス中断イベントが発生したときにtrueになります(実行時はCtrl-Cでこれを発生させて下さい)。

ループの中の処理は仕組みが複雑で、筆者もよく理解出来ていません。コードを見てみましょう。

    auto loan_result = publisher.loan();
    if( loan_result.has_value() ){
      auto &topic = loan_result.value();
      set_value( *topic, counter );
      std::cout << "publish [" << counter << "] type = " << topic->typestr() << std::endl;
      topic.publish();
    } else{
      (省略)
    }

おおよそ次のような流れかと想像します。

  1. まずloan()メソッドで空のトピックを作り、データ格納領域を確保する(返り値の型はiox::popo::Sampleで、include/iceoryx/v2.90.0/iceorx_posh/popo/sample.hppに説明らしき事柄が書かれています)。
  2. 確保に成功したかどうかhas_value()メソッドで判定する。
  3. 成功していれば、iox::popo::Sampleインスタンスの中のトピック書き込み領域のポインタtopicvalue()メソッドで取得する。
  4. topicが指す領域はMyTopicインスタンスとして扱える(ので、set_value()に渡すことが出来る)。
  5. topic自体はiox::popo::Publisherのインスタンスになっており、これ固有のメソッド呼び出しには->でなく.を用いる(ここが分かりにくい!)。publish()が発行用メソッドで、これを呼び出した後はtopicの中身は空に戻る。

iox::popo::Publisheriox::popo::Sampleも様々なクラスの継承やテンプレートで定義されているので、両者の関係をコードだけから読み解くのは結構骨が折れそうです。筆者は諦めました。使うだけならば、上記の流れを押さえれば十分かと思います。

発行ループはsleep_for()メソッドで適当に時間間隔をとっています。

    std::this_thread::sleep_for( std::chrono::seconds( 1 ) );

subscriberソースファイルhello_subscriber.cpp

subscriberプログラムです。

iox::runtime::PoshRuntime::initRuntime()で通信用ランタイムを初期化し、MyTopicを継承したiox::popo::Subscriberクラスをテンプレートで作成する(インスタンスには三つの文字列のユニークな組を割り当てる)ところまでは、publisherとほとんど同じです。

  constexpr char app_name[] = "hello-subscriber";
  iox::runtime::PoshRuntime::initRuntime( app_name );
  iox::popo::Subscriber<MyTopic> subscriber( { SERVICE_NAME, SERVICE_INSTANCE, SERVICE_EVENT } );

周期的にトピック購読するループに入ります。終了判定にはpublisherと同じくiox::hasTerminationRequested()を使っています。

購読部のコードは次のようになっています。

    auto take_result = subscriber.take();
    if( take_result.has_value() ){
      std::cout << "subscribed from ";
      take_result.value()->output();
    } else{
      (省略)
    }

take()メソッドはpubliserのloan()メソッドと異なり、既に値が入ったトピックを取得するものです。返り値の型はiox::popo::Sampleですので、取得に成功したかどうかはpublisherと同様にhas_value()メソッドで判定出来るし、トピックが書き込まれた領域のポインタはvalue()メソッドで取得出来ます。このポインタが指すのはMyTopicではなくconst MyTopicになりますので(データ保護の観点から合理的な仕様だと思います)、output()メソッドをconst修飾しています。

購読ループもsleep_for()メソッドで適当に時間間隔をとっています。

    std::this_thread::sleep_for( std::chrono::milliseconds( 500 ) );

時間間隔を500ミリ秒にしているのは適当ですが、元のicehelloを参考にしています。一度購読したトピックは空になるので、publisher側の時間間隔が1秒ですから2回に1回は「値が無いよ」と言うことになります。実際に実行すると、その通りの出力結果が得られるはずです。

前記事で示したmakefileのTARGET

TARGET=hello_publisher hello_subscriber

とすれば、本記事のプログラムがコンパイル出来ます。ターミナルを二つ起ち上げて通信させてみて下さい。バックグラウンドで /usr/local/bin/iox-roudi を走らせておくことをお忘れなく。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0