テストコード
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_NAME
、SERVICE_INSTANCE
、SERVICE_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{
(省略)
}
おおよそ次のような流れかと想像します。
- まず
loan()
メソッドで空のトピックを作り、データ格納領域を確保する(返り値の型はiox::popo::Sample
で、include/iceoryx/v2.90.0/iceorx_posh/popo/sample.hppに説明らしき事柄が書かれています)。 - 確保に成功したかどうか
has_value()
メソッドで判定する。 - 成功していれば、
iox::popo::Sample
インスタンスの中のトピック書き込み領域のポインタtopic
をvalue()
メソッドで取得する。 -
topic
が指す領域はMyTopic
インスタンスとして扱える(ので、set_value()
に渡すことが出来る)。 -
topic
自体はiox::popo::Publisher
のインスタンスになっており、これ固有のメソッド呼び出しには->
でなく.
を用いる(ここが分かりにくい!)。publish()
が発行用メソッドで、これを呼び出した後はtopic
の中身は空に戻る。
iox::popo::Publisher
もiox::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 を走らせておくことをお忘れなく。