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

# テストコード

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

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

mytopic.h
```c++
#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
```c++
#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
```c++
#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配列の三種類を受け付けます（深い意味はありません）。どの型の値が収まっているかは列挙型で判別します。

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

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

```c++
  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()`も用意しました。

```c++
  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_NAME`、`SERVICE_INSTANCE`、`SERVICE_EVENT`として定義しておきます。

```c++
#define SERVICE_NAME     "MyTopic"
#define SERVICE_INSTANCE "Hello"
#define SERVICE_EVENT    "Periodic"
```

# publisherソースファイルhello_publisher.cpp

publisherプログラムです。

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

```c++
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`は何でも良いです）。

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

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

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

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

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

```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`インスタンスの中のトピック書き込み領域のポインタ`topic`を`value()`メソッドで取得する。
  4. `topic`が指す領域は`MyTopic`インスタンスとして扱える（ので、`set_value()`に渡すことが出来る）。
  5. `topic`自体は`iox::popo::Publisher`のインスタンスになっており、これ固有のメソッド呼び出しには`->`でなく`.`を用いる（ここが分かりにくい！）。`publish()`が発行用メソッドで、これを呼び出した後は`topic`の中身は空に戻る。

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

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

```c++
    std::this_thread::sleep_for( std::chrono::seconds( 1 ) );
```

# subscriberソースファイルhello_subscriber.cpp

subscriberプログラムです。

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

```c++
  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()`を使っています。

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

```c++
    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()`メソッドで適当に時間間隔をとっています。

```c++
    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 を走らせておくことをお忘れなく。