執筆中 (n-ando)
データポート(基本編)では、データポートの基本的な使い方について説明しました。応用編では、もう少し踏み込んだ使い方について解説します。
データポートでは、事前に定義されたデータ型 (例: TimedLong、TimedDouble 等) 以外に、自分で定義したデータ型を使用することもできます。 ただし、自分で新たなデータ型を作る前に、既に似たようなデータ型が定義されていないか確認して、その中に必要なデータ型がない場合にのみ新たなデータ型を定義することをお勧めします。
OpenRTM-aist では、以下の IDLファイルでデータポートに使用するデータ型が定義されています。
データ型を定義する IDLファイルを作成します。データ型は struct キーワードで定義します。以下の基本型や、文字列型、シーケンス型が利用できます。
型 | 意味 | 宣言例 |
short | short型整数 | short shortVariable; |
long | long型整数 | long longVariable; |
unsinged short | short型整数 | unsigned short ushortVariable; |
unsigned long | long型整数 | unsigned long ulongVariable; |
float | 単精度浮動小数点 | float floatVariable; |
double | 倍精度浮動小数点数 | double doubleVariable; |
char | 文字型 | char charVariable; |
wchar | wchar文字型 | char charVariable; |
boolean | bool型 | bool shortVariable; |
octet | octet型 | octet octetVariable; |
longlong | longlong型整数 | longlong longlongVariable; |
ulonglong | unsinged longlong型整数 | ulonglong ulonglongVariable; |
sequence<T> | シーケンス型 | sequence<double> doubleSeqVariable; |
ここでは、MyDataType.idl に MyData というデータ型を定義することにします。 表示のためにスペースを前方に入れていますが、これは実際にIDLファイルを使用する上で不要のため削除してください。
// @file MyDataType.idl #include "BasicDataType.idl" module MyModule { struct MyData { RTC::Time tm; short shortVariable; long longVariable; sequence<double> data; }; };
2行目に
#include "BasicDataType.idl"
RTCBuilder でプロジェクトの作成を行います。
「RTC Builder Project」のアイコンをクリックし、表示したウィンドウにプロジェクト名を入力し「終了」すると、左のウィンドウに生成したプロジェクトファイルが表示します。
そこに独自データ型を配置するidlフォルダーがあるので独自データ型のIDLファイル(MyDataType.idl)を、ドラッグ&ドロップや[右クリック]>[貼り付け]などで置いてください。
プロジェクトフォルダーは、RTCBuilder起動直後に表示する「ディレクトリをワークスペースとして選択」をデフォルトで進めた場合、[ c:\Users\ユーザ名\workspase ]フォルダー以下にあります。
RTCBuilder で独自データ型のidlを使ったコンポーネントの作成を行います。
データポート設定タブを開き、データポート(InPort/OutPort)を定義します。
作成したデータポートで独自データ型を使いたい場合、[Reload] をクリックするとidlフォルダーのMyDataType.idlが読み込まれ、 *データ型 のプルダウンで新たに定義した MyData を選択できます。
その他、コンポーネント作成に必要な項目の設定が終わったら、基本タブに戻り、[コード生成] ボタンをクリックし、コードの生成を行います。
InPort は isNew() でデータの到着の有無を確認して、read() で読みだす、あるいは OutPort は write() でデータを送り出す、ということについてはすでに述べました。
例えば、InPort はデータが来てから、onExecute() 等などの関数内で、isNew() を呼び read() を呼び出すまでデータを取得することはできません。 onExecute() の周期が非常に速かったとしても、データが InPort に到着するタイミングと、実際に処理が行われるタイミングは非同期に行われます。
データが到着してすぐに、すなわち同期的に処理を行いたい場合にはどうすればよいのでしょうか。これを実現する方法として、OpenRTM-aist ではデータポートやコネクタの種々の処理のタイミングで呼び出されるコールバックを定義しています。
コールバックには大きく分けて、1) InPort、2) OutPort、3) コネクタ、4) ポート の4種類のコールバックが用意されています。
InPort には、以下の2種類のコールバックが用意されています。 これらは rtm/PortCallback.h において定義されています。
OnRead | InPort の read() が呼び出された際にコールされる InPort::setOnRead() 関数でセット。 |
OnReadConvert | InPort の read() が呼び出された際にデータを変換するためにコールされる。InPort::setOnReadConvert() 関数でセット。 |
OnRead コールバックは read() が呼び出されたときに、OnReadConvert は read() が呼び出されたとき、呼び出し元にある種の変換を施したデータを返すために使用するコールバックです。
それぞれのコールバックは、rtm/PortCallback.h で定義されているそれぞれのファンクタの基底クラスを継承することにより実装します。
以下にそれぞれの実装例を示します。
#include <rtm/Portcallback.h> template <class T> class MyOnRead : public RTC::OnRead<T> { public: MyOnRead(std::ostream& os) : m_os(os) {}; virtual void operator()() { m_os << "read() 関数が呼ばれました。" << std::endl; std::cout << "read() 関数が呼ばれました。" << std::endl; } private: std::ostream& m_os; }; template <class T> class MyOnReadConvert : public RTC::OnReadConvert<T> { public: virtual T operator()(const T& value) { T tmp; tmp.data = value.data * value.data; return tmp; } };
OnRead を継承した MyOnRead ファンクタでは、コンストラクタで出力ストリーム std::ostream を渡しています。どこかでオープンしたファイル出力ストリーム std::ofstream 等を渡すことを意図しています。 ファンクタの実体である operator()() では、出力ストリームと標準出力に対して、文字列を出力しています。このように、ファンクタでは、予めコンストラクタなどで状態変数を渡すことで、他のオブジェクトに対する呼び出しも実現することができます。
一方 OnReadConvert<T> を継承した MyOnReadConvert は operator()(constT&) のみを実装しています。この関数の引数には、read() が呼ばれたときにInPort 変数に読みだされる前のデータが渡されます。 この関数内で何らかの処理を行い return で返したデータは InPort 変数に書き込まれます。この例では、データ型に data というメンバがあり、かつ乗算演算子が定義されているという前提で自乗を計算して返しています。 適切なメンバがない変数型を使用すればコンパイルエラーになります。
さて、このファンクタを実際にコンポーネントに組み込んでみましょう。InPort を使用しているサンプルとして、ここでは OpenRTM-aist に含まれているサンプルである ConsoleOut を利用します。ConsoleOut は OpenRTM-aist のソースを展開すると、
OpenRTM-aist-<version>/examples/SimpleIO/
の下に、また Linux 等でパッケージ等からインストールすると、
/usr/share/OpenRTM-aist/examples/src/
の下にソースコードがあります。
まず、上記のクラス定義を、ConsoleOut.h に記述します。クラス定義は、本来別のソースに記述した方が良いのですが、ファンクタクラスは、このコンポーネント内でしか使用せず、内容も短いものですので、こういう場合はヘッダ内で実装も含めて定義しても構わないでしょう。
// ConsoleOut.h 中略 // Service Consumer stub headers // <rtc-template block="consumer_stub_h"> // </rtc-template> using namespace RTC; // ここから追加分 template <class T> class MyOnRead : public RTC::OnRead<T> { public: MyOnRead(std::ostream& os) : m_os(os) {}; virtual void operator()() { m_os << "read() 関数が呼ばれました。" << std::endl; std::cout << "read() 関数が呼ばれました。" << std::endl; } private: std::ostream& m_os; }; template <class T> class MyOnReadConvert : public RTC::OnReadConvert<T> { public: virtual T operator()(const T& value) { T tmp; tmp.data = value.data * value.data; return tmp; } }; // ここまで追加分 class ConsoleOut : public RTC::DataFlowComponentBase { 中略 protected: // DataInPort declaration // <rtc-template block="inport_declare"> TimedLong m_in; InPort<TimedLong> m_inIn; 中略 private: //ここから追加分 MyOnRead<TimedLong>* m_onread; MyOnReadConvert<TimedLong>* m_onreadconv; //ここまで追加分 };
まず、ConsoleOut クラスの宣言の前に、コールバックファンクタ MyOnRead とMyOnReadConvert を宣言します。 これらのクラスのポインタ変数をメンバとして持たせるために、private の部分に、それぞれのポインタ変数を宣言します。 このとき、MyOnRead/MyOnReadConvert ともに、クラステンプレートの型引数にこのコンポーネントの InPort の型と同じ、TimedLong を与えていることに注意してください。
OutPort には、以下の2種類のコールバックが用意されています。 これらは rtm/PortCallback.h において定義されています。
OnWrite | OutPort の write() が呼び出された際にコールされる OutPort::setOnWrite() 関数でセット。 |
OnWriteConvert | OutPort の write() が呼び出された際にデータを変換するためにコールされる。OutPort::setOnWriteConvert() 関数でセット。 |
OnWrite コールバックは write() が呼び出された際に、OnWriteConvert はwrite() が呼び出された際に、ある種の変換を施したデータを送信するために使用するコールバックです。
それぞれのコールバックは、InPort と同様 rtm/PortCallback.h で定義されているそれぞれのファンクタの基底クラスを継承することにより実装します。
以下にそれぞれの実装例を示します。
#include <rtm/Portcallback.h> template <class T> class MyOnWrite : public RTC::OnWrite<T> { public: MyOnWrite(std::ostream& os) : m_os(os) {}; virtual void operator()() { m_os << "write() 関数が呼ばれました。" << std::endl; std::cout << "write() 関数が呼ばれました。" << std::endl; } private: std::ostream& m_os; }; template <class T> class MyOnWriteConvert : public RTC::OnWriteConvert<T> { public: virtual T operator()(const T& value) { T tmp; tmp.data = 2 * value.data; return tmp; } };
コールバック用のファンクタの書き方は、InPort の OnRead/OnReadConvert とほぼ同じです。OnWrite を継承した MyOnWrite ファンクタでは、コンストラクタで出力ストリーム std::ostream を渡しています。 どこかでオープンしたファイル出力ストリーム std::ofstream 等を渡すことを意図しています。ファンクタの実体である operator() では、出力ストリームと標準出力に対して、文字列を出力しています。 このように、ファンクタでは、予めコンストラクタなどで状態変数を渡すことで、他のオブジェクトに対する呼び出しも実現することができます。
一方 OnReadConvert<T> を継承した MyOnReadConvert は operator()(constT&) のみを実装しています。この関数の引数には、read() を呼んだときに InPort 変数に読みだされる前のデータが渡されます。この関数内で何らかの処理を行い return で返したデータは InPort 変数に書き込まれます。 この例では、データ型に data というメンバがあり、かつ乗算演算子が定義されているという前提で自乗を計算して返しています。適切なメンバがない変数型を使用すればコンパイルエラーになります。
コネクタはバッファおよび通信路を抽象化したオブジェクトです。図に示すように、OutPort と InPort の間に存在し、OutPort からは write() 関数によりデータの書き込み、InPort からは read() 関数によりデータの読み出しが行われます。 コネクタは、データがどのような手段で OutPort から InPort へ伝送されるかを抽象化し隠蔽します。
OutPort はコネクタ内のバッファに対して、OutPort は複数の InPort へ接続することができますが、一つの接続につき、一つのコネクタが生成されます。(実際には InPort も複数の接続を同時に持つこともできますが、データを区別する方法がないので、通常は用いません。) つまり、接続が3つあれば、コネクタが3つ存在し、それぞれに対して書き込みのステータスが存在することになります。
また、これらの機能のために、OutPort/InPort 一対に対して、それぞれ一つコネクタが存在する必要があることがわかります。 さらに、コネクタをサブスクリプション型に対応した実装レベルでモデル化するにあたり、パブリッシャと呼ばれる非同期通信のためのオブジェクトを導入しました。
データポートは接続が確立されると、1つの接続につき1つのコネクタオブジェクトを生成します。コネクタは、OutPort と InPort をつなぐデータストリームの抽象チャネルです。
ON_BUFFER_WRITE | バッファ書き込み時 |
ON_BUFFER_FULL | バッファフル時 |
ON_BUFFER_WRITE_TIMEOUT | バッファ書き込みタイムアウト時 |
ON_BUFFER_OVERWRITE | バッファ上書き時 |
ON_BUFFER_READ | バッファ読み出し時 |
ON_SEND | InProtへの送信時 |
ON_RECEIVED | InProtへの送信完了時 |
ON_RECEIVER_FULL | InProt側バッファフル時 |
ON_RECEIVER_TIMEOUT | InProt側バッファタイムアウト時 |
ON_RECEIVER_ERROR | InProt側エラー時 |
ON_BUFFER_EMPTY | バッファが空の場合 |
ON_BUFFER_READTIMEOUT | バッファが空でタイムアウトした場合 |
ON_SENDER_EMPTY | OutPort側バッファが空 |
ON_SENDER_TIMEOUT | OutPort側タイムアウト時 |
ON_SENDER_ERROR | OutPort側エラー時 |
ON_CONNECT | 接続確立時 |
ON_DISCONNECT | 接続切断時 |
データポートは、データの送受信を行った際に、ステータスを返します。 ステータスは、rtm/DataPortStatus.h で定義されています。
PORT_OK | 正常終了 |
PORT_ERROR | 異常終了 |
BUFFER_ERROR | バッファエラー |
BUFFER_FULL | バッファフル |
BUFFER_EMPTY | バッファエンプティ |
BUFFER_TIMEOUT | バッファタイムアウト |
SEND_FULL | データを送信したが相手側がバッファフル状態 |
SEND_TIMEOUT | データを送信したが相手側がタイムアウトした |
RECV_EMPTY | データを送信したがデータが空状態 |
RECV_TIMEOUT | データを受信しようとしたがタイムアウトした |
INVALID_ARGS | 不正な引数 |
PRECONDITION_NOT_MET | 事前条件を満たしていない |
CONNECTION_LOST | 接続が切断された |
UNKNOWN_ERROR | 不明なエラー |
独自データポートインターフェースの作成例を示します。
以下の関数、クラスの定義が必要です。
データポートインターフェース(Push型)のプロバイダクラスです。 コンシューマ側で put関数を呼び出した際に、何らかの方法によりプロバイダ側にデータを転送する必要があります。 このサンプルではコンシューマ側の put関数呼び出し時にファイルにデータを書き込み、プロバイダ側でファイルからデータを読み込むことでデータの転送を行っています。
//InPortTestProvider.cpp #include "InPortTestProvider.h" #ifdef WIN32 #pragma warning( disable : 4290 ) #endif namespace RTC { InPortTestProvider::InPortTestProvider(void) : m_buffer(0), m_running(true), m_filename("data.dat") { setInterfaceType("test"); activate(); } InPortTestProvider::~InPortTestProvider(void) { m_running = false; wait(); } //プロバイダ生成時に呼び出される関数 //コネクタプロファイル、ポートのプロパティの情報を受け取る void InPortTestProvider::init(coil::Properties& prop) { } void InPortTestProvider:: setBuffer(BufferBase<cdrMemoryStream>* buffer) { m_buffer = buffer; } void InPortTestProvider::setListener(ConnectorInfo& info, ConnectorListeners* listeners) { m_profile = info; m_listeners = listeners; } void InPortTestProvider::setConnector(InPortConnector* connector) { m_connector = connector; } //別スレッドにより実行される関数 //周期的にファイルからデータを読み込んでバッファに書き込む //この関数はこのサンプルでは必要ですが、独自インターフェースを作成するうえで //必須ではありません int InPortTestProvider::svc() { coil::sleep(1); while (m_running) { std::ifstream fin; fin.open(m_filename, std::ios::in | std::ios::binary); if (fin) { while (!fin.eof()) { int data_size = 0; fin.read((char*)&data_size, sizeof(int)); if (data_size > 0) { CORBA::OctetSeq data; data.length(data_size); fin.read((char*)&data[0], data_size); //cdrMemoryStream型変数にデータを格納してバッファに書き込む //以下の記述方法はomniORB特有なため、TAOやORBexpressに対応する場合は //分ける必要がある cdrMemoryStream cdr; //エンディアンの設定を行う bool endian_type = m_connector->isLittleEndian(); cdr.setByteSwapFlag(endian_type); //データを書き込む cdr.put_octet_array(&(data[0]), data.length()); //バッファに書き込む m_buffer->write(cdr); } } fin.close(); } } return 0; } //コネクタ接続時に呼び出される関数 //この関数はコンシューマ側のsubscribeInterface関数よりも前に呼び出される //このため、publishInterface関数で設定した情報をコンシューマ側のsubscribeInterface関数で //取得することができる //何か問題があった時はfalseを返してコネクタを切断する bool InPortTestProvider:: publishInterface(SDOPackage::NVList& properties) { //データを書き込むファイル名の情報を格納する CORBA_SeqUtil:: push_back(properties, NVUtil::newNV("dataport.test.filename", m_filename.c_str())); return true; } }; extern "C" { //この関数をモジュールロード時に呼び出す必要がある void InPortTestProviderInit(void) { RTC::InPortProviderFactory& factory(RTC::InPortProviderFactory::instance()); factory.addFactory("test", ::coil::Creator< ::RTC::InPortProvider, ::RTC::InPortTestProvider>, ::coil::Destructor< ::RTC::InPortProvider, ::RTC::InPortTestProvider>); } };
データポートインターフェース(Push型)のコンシューマクラスです。InPortProvider を継承する必要があります。 Taskクラスの継承はこのサンプル独自のものなので必須ではありません。
//InPortTestProvider.h #ifndef RTC_INPORTTESTPROVIDER_H #define RTC_INPORTTESTPROVIDER_H #include <rtm/BufferBase.h> #include <rtm/InPortProvider.h> #include <rtm/CORBA_SeqUtil.h> #include <rtm/Manager.h> #include <rtm/ConnectorListener.h> #include <rtm/ConnectorBase.h> #include <fstream> #ifdef WIN32 #pragma warning( disable : 4290 ) #endif namespace RTC { class InPortTestProvider : public InPortProvider, public coil::Task { public: InPortTestProvider(void); virtual ~InPortTestProvider(void); virtual void init(coil::Properties& prop); virtual void setBuffer(BufferBase<cdrMemoryStream>* buffer); virtual void setListener(ConnectorInfo& info, ConnectorListeners* listeners); virtual void setConnector(InPortConnector* connector); virtual bool publishInterface(SDOPackage::NVList& properties); virtual int svc(); private: CdrBufferBase* m_buffer; ConnectorListeners* m_listeners; ConnectorInfo m_profile; InPortConnector* m_connector; bool m_running; std::string m_filename; }; }; extern "C" { DLL_EXPORT void InPortTestProviderInit(void); }; #ifdef WIN32 #pragma warning( default : 4290 ) #endif #endif
データポートインターフェース(Push型)のコンシューマクラスです。Push型の場合は InPort側にコンシューマ、OutPort側にプロバイダを生成します。 コンシューマ側で put関数を呼び出した際に、何らかの方法によりプロバイダ側にデータを転送する必要があります。 このサンプルではコンシューマ側の put関数呼び出し時にファイルにデータを書き込み、プロバイダ側でファイルからデータを読み込むことでデータの転送を行っています。
//InPortTestConsumer.cpp #include <rtm/NVUtil.h> #include "InPortTestConsumer.h" namespace RTC { InPortTestConsumer::InPortTestConsumer(void) : rtclog("InPortTestConsumer") { } InPortTestConsumer::~InPortTestConsumer(void) { RTC_PARANOID(("~InPortTestConsumer()")); } //コネクタプロファイル、ポートのプロパティの情報を受け取る void InPortTestConsumer::init(coil::Properties& prop) { m_properties = prop; } //データ転送時に呼び出される関数 //put関数内でプロバイダ側にデータを転送する処理を記述する InPortConsumer::ReturnCode InPortTestConsumer:: put(const cdrMemoryStream& data) { RTC_PARANOID(("put()")); //このサンプルでは、コンシュマー側でファイルにデータを書き込んで、プロバイダ側で //ファイル内のデータを読み込むことにしている //バイナリファイルを開く m_file.open(m_filename, std::ios::out | std::ios::binary | std::ios::trunc); //データサイズと生データをファイルに書き込む int data_size = data.bufSize(); m_file.write((char*)&data_size, sizeof(int)); m_file.write((char*)data.bufPtr(), data_size); //ファイルを閉じる m_file.close(); return PORT_OK; } //コネクタ接続時に呼び出される関数 //この関数はプロバイダ側のpublishInterface関数よりも後に呼び出される //このため、プロバイダ側のpublishInterface関数で設定した情報を取得することができる //return: 何か問題があった時はfalseを返してコネクタを切断する bool InPortTestConsumer:: subscribeInterface(const SDOPackage::NVList& properties) { //プロバイダ側で設定したファイル名を取得する CORBA::Long index = NVUtil::find_index(properties, "dataport.test.filename"); const char* filename(0); properties[index].value >>= filename; //取得したファイル名のファイルを開く m_filename = filename; m_file.open(m_filename, std::ios::out | std::ios::binary | std::ios::trunc); m_file.close(); return true; } //コネクタ切断時に呼び出される関数 void InPortTestConsumer:: unsubscribeInterface(const SDOPackage::NVList& properties) { } void InPortTestConsumer::publishInterfaceProfile(SDOPackage::NVList& properties) { } }; extern "C" { //コンシューマ登録関数 //この関数をモジュールロード時に呼び出す必要がある void InPortTestConsumerInit(void) { RTC::InPortConsumerFactory& factory(RTC::InPortConsumerFactory::instance()); factory.addFactory("test", ::coil::Creator< ::RTC::InPortConsumer, ::RTC::InPortTestConsumer>, ::coil::Destructor< ::RTC::InPortConsumer, ::RTC::InPortTestConsumer>); } };
データポートインターフェース(Push型)のコンシューマクラスです。InPortConsumer を継承する必要があります。
//InPortTestConsumer.h #ifndef RTC_INPORTTESTCONSUMER_H #define RTC_INPORTTESTCONSUMER_H #include <rtm/InPortConsumer.h> #include <rtm/Manager.h> #include <fstream> namespace RTC { class InPortTestConsumer : public InPortConsumer { public: DATAPORTSTATUS_ENUM InPortTestConsumer(void); virtual ~InPortTestConsumer(void); virtual void init(coil::Properties& prop); virtual ReturnCode put(const cdrMemoryStream& data); virtual void publishInterfaceProfile(SDOPackage::NVList& properties); virtual bool subscribeInterface(const SDOPackage::NVList& properties); virtual void unsubscribeInterface(const SDOPackage::NVList& properties); private: mutable Logger rtclog; coil::Properties m_properties; std::ofstream m_file; std::string m_filename; }; }; extern "C" { DLL_EXPORT void InPortTestConsumerInit(void); }; #endif
OpenRTM-aist のマネージャは「XXX.dll」というダイナミックリンクライブラリをロードした場合に「XXXInit」関数を呼び出します。 このサンプルの場合は「InPortTestInterface.dll」をロードして、以下の「InPortTestInterfaceInit」関数を呼び出します。
//InPortTestInterface.cpp #include "InPortTestConsumer.h" #include "InPortTestProvider.h" extern "C" { DLL_EXPORT void InPortTestInterfaceInit(RTC::Manager* manager) { InPortTestProviderInit(); InPortTestConsumerInit(); } };
1. OpenRTM-aistがインストールされている環境を用意します。
2. ビルドのために CMake設定ファイル (CMakeLists.txt) を作成します。
以下は、独自インターフェースのサンプルをビルドするための CMake設定ファイルです。
//OpenRTM-aistのライブラリを見つけるための記述 cmake_minimum_required (VERSION 2.6) find_package(OpenRTM HINTS /usr/lib64/openrtm-1.1/cmake) if(${OpenRTM_FOUND}) MESSAGE(STATUS "OpenRTM configuration Found") else(${OpenRTM_FOUND}) message(STATUS "Use cmake/Modules/FindOpenRTM.cmake in the project") list(APPEND CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/cmake/Modules) find_package(OpenRTM REQUIRED) endif(${OpenRTM_FOUND}) if (DEFINED OPENRTM_INCLUDE_DIRS) string(REGEX REPLACE "-I" ";" OPENRTM_INCLUDE_DIRS "${OPENRTM_INCLUDE_DIRS}") string(REGEX REPLACE " ;" ";" OPENRTM_INCLUDE_DIRS "${OPENRTM_INCLUDE_DIRS}") endif (DEFINED OPENRTM_INCLUDE_DIRS) if (DEFINED OPENRTM_LIBRARY_DIRS) string(REGEX REPLACE "-L" ";" OPENRTM_LIBRARY_DIRS "${OPENRTM_LIBRARY_DIRS}") string(REGEX REPLACE " ;" ";" OPENRTM_LIBRARY_DIRS "${OPENRTM_LIBRARY_DIRS}") endif (DEFINED OPENRTM_LIBRARY_DIRS) if (DEFINED OPENRTM_LIBRARIES) string(REGEX REPLACE "-l" ";" OPENRTM_LIBRARIES "${OPENRTM_LIBRARIES}") string(REGEX REPLACE " ;" ";" OPENRTM_LIBRARIES "${OPENRTM_LIBRARIES}") endif (DEFINED OPENRTM_LIBRARIES) include_directories(${OPENRTM_INCLUDE_DIRS}) include_directories(${OMNIORB_INCLUDE_DIRS}) add_definitions(${OPENRTM_CFLAGS}) add_definitions(${OMNIORB_CFLAGS}) link_directories(${OPENRTM_LIBRARY_DIRS}) link_directories(${OMNIORB_LIBRARY_DIRS}) //プロジェクト名設定 project (InPortTestInterface) //動的ライブラリを作成する add_library(InPortTestInterface SHARED InPortTestProvider.cpp InPortTestConsumer.cpp InPortTestProvider.h InPortTestConsumer.h InPortTestInterface.cpp) //リンクするライブラリの設定 target_link_libraries(InPortTestInterface ${OPENRTM_LIBRARIES})
3. CMake により Visual Studio のプロジェクトファイルを生成します。
4. Visual Studio でビルドします。
ビルド後に、Debugフォルダーに InPortTestInterface.dll が生成されます。
5. rtc.conf を作成します
rtc.conf を作成して、Manager 起動時にロードするモジュールで InPortTestInterface.dll を指定します。
manager.modules.preload: InPortTestInterface.dll
InPortTestInterface.dll が、RTC を実行するディレクトリーと異なる場合は、別途モジュール探索パスを設定します。
manager.modules.load_path: C:¥workspace¥InPortTestInterface¥build¥Debug
ConsoleInComp.exe -f ../rtc.conf
CORBA の構造体は、以下のように C++ の構造体 "struct" にマッピングされます。
struct Profile { short short_value; long long_value; };
// -*- C++ -*- struct Profile { CORBA::Short short_value; CORBA::Long long_value; };
class Profile_var { };
上記の一覧に型が該当しない場合、その型は固定長になります。
_CORBA_ObjRef_Var(T_ptr p) : pd_objref(p) {}
_ptr型のオブジェクト参照の所有権は_var型に移るため、参照カウントは増減しません。
MyObject_ptr ptr = obj->get_myobject(); // ptr は適切なタイミングで release されなければならない MyObject_var var(ptr); // 所有権は var に移ったため、ptr をリリースする必要はない // var がスコープを抜けるなどして解体されると、参照カウントは release される
Object_var(const T_var& p) : pd_ref(T::_duplicate(p.pd_ref)) {}
代入元の_var型のオブジェクト参照の所有権はコピーされます。
MyObject_var var0 = obj->get_myobject(); // var0 は所有権を持つ MyObject_var var1(var0); // リファレンスカウントはインクリメントされ var1も所有権を持つ
単純にオブジェクト参照の_ptr型ポインタを返します。所有権は移行されません。
T_ptr in() const { return pd_objref; }
通常、関数の in 引数にオブジェクト参照を渡す際に使用します。 オブジェクト参照を与えられた側の関数は、所有権を持たないため関数内で release してはいけません。 関数から戻ってきたあと、_var型変数に依然として所有権が保持されており、_var型変数の解体時にオブジェクトは release されます。
オブジェクト参照を in 引数として受け取る関数を定義する場合は、_ptr型の引数として定義します。 さらに、関数内では、そのオブジェクトのオペレーションを呼ぶだけで、release 等は行ってはいけません。 また、関数内でオブジェクト参照をどこか(グローバル変数、static変数、オブジェクトのメンバー等)に保存したい場合は、所有権をコピーするため duplicate関数で複製する必要があります。
void myfunc(MyObject_ptr obj) { obj->function(); CORBA::release(obj); // ×これはしてはいけない m_obj = obj; // ×所有権を持っていない m_obj = MyObject::duplicate(obj); // ○所有権を複製 } {// 別のコンテキスト MyObject_var obj(hoge->get_object()); myfunc(obj.in()); // 所有権は移行されない } // スコープを抜けたので参照カウントがデクリメントされる
現在所有している参照を release して、リファレンスのポインタを nil にセットして返します。 すなわち、out()関数呼び出し以前にもしオブジェクト参照を保持している場合は、その所有権を放棄し参照は破棄されます。
T_ptr& out() { T_Helper::release(pd_objref); pd_objref = T_Helper::_nil(); return pd_objref; }
通常、関数の out引数にオブジェクト参照を渡す際に使用します。 すなわち、関数から戻ってきたあと、この変数に新たにオブジェクト参照が保持されていることが期待されます。 このとき、オブジェクト参照の所有権はこの変数に保持されていると考えます。 out() で変数を渡された関数側では、何らかの形でオブジェクト参照を生成または複製して、引数に所有権を渡す必要があります。
オブジェクト参照を out引数として受け取る関数を定義する場合は、_ptr型参照の引数として定義します。 関数内では、引数は必ずnilオブジェクト参照であり、かつ通常何らかのオブジェクトを所有権を与えて代入することが期待されます。
void myfunc(MyObject_ptr& obj) { assert(CORBA::is_nil(obj)); // 必ず nil オブジェクトを指定する obj->function(); // nil なのでオペレーションは呼べない // m_obj は_var型のメンバ変数 obj = m_obj; // ×所有権を複製していない。 // return後、関数の外で勝手に release されるかもしれない。 obj = MyObject::duplicate(obj); // ○所有権を複製している。 // return後、関数の外で release されても、オブジェクト参照は解体されない。 return; } {// 別のコンテキスト MyObject_var obj; obj = get_object(); // △out変数として使うので渡す前には何も入れない方がよい myfunc(obj.out()); // オブジェクトが代入され返ってきたはず assert(!CORBA::is_nil(obj)); obj->function(); } // スコープを抜けたので参照カウントがデクリメントされる。
リファレンスへのポインタの参照を返します。
T_ptr& inout() { return pd_objref; }
通常、関数の inout引数にオブジェクト参照を渡す際に使用します。 すなわち、関数内では何らかのオブジェクト参照が引数に入っていることが期待され、オブジェクトの所有権は関数側に移行します。 また、関数は何らかのオブジェクト参照をこの引数に与えて返すことが期待され、引数すなわち呼び出し元の変数に所有権を与えます。 関数内では引数にオブジェクト参照を新たにセットする場合は、まず release してから新たなオブジェクト参照を生成または複製して引数に所有権を渡す必要があります。
オブジェクト参照を inout引数として取る関数は、設計の観点からあまり推奨されません。 もし、inout引数として取る関数を定義する必要がある場合は、_ptr型参照の引数として定義します。
void myfunc(MyObject_ptr& obj) { if (!CORBA::is_nil(obj)) { obj->function(); // obj が nil でなければオペレーションを呼ぶことができる。 } CORBA::release(obj); // releaseする責任はこの関数にある /* * この関数内で、obj に新たなオブジェクト参照がある場合に限り、 * 引数を受け取った直後に、_var変数に代入しておくことで、 * 関数終了時に自動的に参照カウントをデクリメント * するテクニックを使用してもよい。 * MyObject_var deleter = obj; */ // MyObject_var m_obj とする obj = m_obj; // × 所有権を複製していない // return 後、関数の外で release されるかもしれない。 obj = MyObject::_duplicate(m_obj); // ○ obj にも MyObject の所有権が与えられた // return 後、関数の外で release されても、オブジェクト参照は解体されない。 } {// 別のコンテキスト MyObject_var obj; obj = get_object(); // obj は所有権を持っている myfunc(obj); // 関数内で releae される // obj の指すものは入れ替わっているかもしれない。 } // スコープを抜けたので参照カウントがデクリメントされる。
現在持っているオブジェクト参照の所有権を放棄してポインタを返します。
T_ptr _retn() { T_ptr tmp = pd_objref; pd_objref = T_Helper::_nil(); return tmp; }
通常、関数の戻り値にオブジェクト参照を返す場合に使用されます。 所有権は関数の呼び出し側に渡るので、呼び出し側ではオブジェクト参照を破棄する必要があります。 従って、呼び出し側で release するか、_var型変数で受ける必要があります。
逆に、戻り値でオブジェクト参照を返す場合、呼び出し側で release することにより参照カウントがデクリメントされるため、関数内では _duplicate() などで所有権を複製しておく必要があります。
MyObject_ptr myfunc() { MyObject_var ret; ret = m_obj; // ×所有権がretに移ってしまう。 // return 後、関数の外で release されるかもしれない。 ret = MyObject::_duplicate(m_obj); // ○所有権の複製 // return 後、release が呼ばれても、m_obj は所有権を保持し続ける。 return ret._retn(); } { // 別のコンテキスト MyObject_var obj; obj = myfunc(); // オブジェクトの所有権を取得 obj->function(); MyObject_ptr ptr; ptr = myfunc(); //オブジェクトの所有権を取得 ptr->function(); CORBA::release(ptr); // 参照カウントをデクリメント } // スコープを抜けたので参照カウントがデクリメントされる
関数 | 型 | release責任 | 関数内 |
in | T_ptr | 呼出側 | オペレーション呼出 |
out | T_ptr& | 呼出側 | _duplicate 代入 |
inout | T_ptr& | in:関数, out:呼出側 | release後, _duplicate代入 |
_retn | T_ptr | 呼出側 | _duplicateしてreturn |
ポインタへの代入。
複製なし、解放せず。
{ MyObject_var var; var = myfunc(); // オブジェクトの所有権を取得 MyObject_ptr ptr ptr = MyObject::_duplicate(var); //オブジェクトの所有権を取得(参照カウントのインクリメント) // ptr = var; // これは参照カウントエラーを引き起こす恐れがある。呼び出し後、ptrとvarは同じオブジェクト // をさすであろうが、参照カウントの保守はなされな。varは、その対象オブジェクトの所持を維持 // する。また、ptrがそれが以前に指していたオブジェクトやプロキシへの唯一のポインタであった // とすれば、メモリリークが生じる。 CORBA::release(ptr); // 参照カウントをデクリメント } // var に関しては、スコープを抜けたので参照カウントがデクリメントされる
_var が保有しているオブジェクトに対して release() されるが、引数で渡された _ptr型のオブジェクトに対しては duplicate() されません。
複製なし、解放あり。
inline T_var& operator= (T_ptr p) { T_Helper::release(pd_objref); pd_objref = p; return *this; }
{ MyObject_ptr ptr; ptr = myfunc(); // オブジェクトの所有権を取得 MyObject_var obj; obj = MyObject::_duplicate(ptr); //オブジェクトの所有権を取得(参照カウントのインクリメント) CORBA::release(ptr); // 参照カウントをデクリメント } // objに関しては、スコープを抜けたので参照カウントがデクリメントされる
_var が保有しているオブジェクトに対して release() がコールされ、 かつ、引数で渡されたオブジェクトに対しても duplicate() がコールされる。
複製あり、解放あり。
inline T_var& operator= (const T_var& p) { if( &p != this ) { T_Helper::duplicate(p.pd_objref); T_Helper::release(pd_objref); pd_objref = p.pd_objref; } return *this; }
{ MyObject_var var1; var1 = myfunc(); // オブジェクトの所有権を取得 MyObject_var var2; var2 = var1; //オブジェクトの所有権を取得(参照カウントは自動でインクリメントされる) } // var1, var2に関しては、スコープを抜けたので参照カウントがデクリメントされる
_narrow()処理の過程において、_narrow()の呼び出しが成功した場合、その対象オブジェクトのリファレンスカウントはインクリメントされますが、失敗した場合はインクリメントされません。
デクリメントは行われない。
RTC::RTObject_ptr RTC::RTObject::_narrow(::CORBA::Object_ptr obj) { if( !obj || obj->_NP_is_nil() || obj->_NP_is_pseudo() ) return _nil(); _ptr_type e = (_ptr_type) obj->_PR_getobj()->_realNarrow(_PD_repoId); return e ? e : _nil(); }
void* omniObjRef::_realNarrow(const char* repoId) { // Attempt to narrow the reference using static type info. void* target = _ptrToObjRef(repoId); if( target ) { if (!lid || (lid && !lid->deactivated() && lid->servant() && lid->servant()->_ptrToInterface(repoId))) { omni::duplicateObjRef(this); } else { omniObjRef* objref; omniIOR* ior; ior = pd_ior->duplicateNoLock(); } objref = omni::createObjRef(repoId,ior,1,0); } else { if( _real_is_a(repoId) ) { omniObjRef* objref; omniIOR* ior; { ior = pd_ior->duplicateNoLock(); } { objref = omni::createObjRef(repoId,ior,1,_identity()); } } } return target; }
クライアントが呼び出しからオブジェクト参照を受信するならば、そのクライアントはそのオブジェクト参照が不要となったときにはそれを開放しなくてはならない。
(引用: 『CORBA分散オブジェクト Orbixを用いて』 P.98 オブジェクト参照のためのメモリ管理)
呼び出し側に渡す参照の所有権は放棄される(つまり、その参照カウントは一つデクリメントされる。したがって、通常は、参照を返す前に適当な_duplicate()関数を呼び出すことになる)
(引用: 『CORBA分散オブジェクト Orbixを用いて』 P.98 オブジェクト参照のためのメモリ管理)
コンシューマからプロバイダを呼び出す際には、サービスポートが接続されていて、かつ相手の RTC が Active 状態になっている必要があります。コンシューマは、コンシューマが接続されているか、相手の RTC がアクティブかどうかを以下の方法で確認することができます。
CORBA コンシューマ型 (C++ では RTC::CorbaComsumer<T>) は以下の3つの状態を取ることができます。
RTC::CorbaComsumer<T> が nil かどうかは、CORBA の標準関数:CORBA::is_nil() で確認することができます。 さらに、オブジェクトがアクティブかどうかは、CORBA オブジェクトのメンバ関数、_non_existent() で確認することができます。
こういった情報は CORBA のマニュアルやドキュメントから得ることができます。一番確実なのは OMG から入手できる CORBA の仕様書を読むことです。
しかしながら、これらの標準仕様書はページ数も多いので、例えば VisiBroker などの CORBA 製品のマニュアルを利用するとよいかもしれません。
以上の情報から以下のようなサンプルコードが書けます。
RTC::ReturnCode_t MyServiceConsumer::onExecute(RTC::UniqueId ec_id) { try { if (CORBA::is_nil(m_myservice0._ptr())) { std::cout << "[nil] object reference is not assigned." << std::endl; } else { if (m_myservice0->_non_existent()) { std::cout << "[inactive] provider is inactive." << std::endl; } else { std::cout << "[active] provider is active." << std::endl; } } } catch (...) { std::cout << "Unknown exception." << std::endl; } coil::sleep(1); return RTC::RTC_OK; }
このコードの onExecute 関数を OpenRTM-aist のサンプル SimpleService の MyServiceConsumer.cpp の oExecute 関数と入れ替えて MyServiceProviderComp とともに実行してみてください。 MyServiceConsumerComp をアクティブ化すると、MyServiceProviderComp のサービスポートが接続されるまではコンシューマ (m_myserivce0) の状態は nil です。MyServiceProviderComp のポートを接続すると、inactive 状態になり、その後 MyServiceProvider をアクティブ化すると active 状態になります。
以上のようにして、相手の RTC の状態をサービスポートを通して知ることもできます。
RTCBuilder でプロジェクトの作成を行います。
「RTC Builder Project」のアイコンをクリックし、表示したウィンドウにプロジェクト名を入力し「終了」すると、左のウィンドウに生成したプロジェクトファイルが表示します。
そこに独自データ型を配置するidlフォルダーがあるので、idlファイルをドラッグ&ドロップや[右クリック]>[貼り付け]などで置いてください。
ここではサンプルコンポーネントのSampleServiceコンポーネントが使用するIDLファイル(MyService.idl)を使います。 IDLファイル(MyService.idl)の保存先は以下です。プロジェクトフォルダーは、RTCBuilder起動直後に表示する「ディレクトリをワークスペースとして選択」をデフォルトで進めた場合、[ c:\Users\ユーザ名\workspase ]フォルダー以下にあります。
RTCBuilder で独自idlを使ったコンポーネントの作成を行います。
サービスポート設定タブを開き、[Add Port]を選択しサービスポート(ServicePort)を定義します。
[ Add Interface ] ボタンを2回クリックし[ Service Interface ] を表示・選択すると[ Service Interface Profile ]を表示します。
[Reload] をクリックするとidlフォルダー内のidlファイルが読み込まれ、 *インターフェース型 のプルダウンで新たに定義した独自インターフェースを選択できます。
その他、コンポーネント作成に必要な項目の設定が終わったら、基本タブに戻り、[コード生成] ボタンをクリックし、コードの生成を行います。
コンフィギュレーション(基本編)では、コンフィギュレーションの基本的な使い方について説明しました。応用編では、もう少し踏み込んだ使い方について解説します。
コンフィギュレーションパラメータのコールバックの利用について説明します。
コンフィギュレーションには、以下のコールバックがあります。
以下のようにしてコールバックを設定します。
class MyOnUpdate : public RTC::OnUpdateCallback { public: MyOnUpdate(ConfigurationTest *obj) { myobj = obj; } virtual void operator()(const char* config_set) { RTC::ExecutionContextList_var ecs; ecs = myobj->get_owned_contexts(); ecs[(CORBA::ULong)0]->set_rate(myobj->m_interval); std::cout << "OnUpdateCallback\t" << config_set << std::endl; } private: ConfigurationTest *myobj; };
class MyOnUpdateParam : public RTC::OnUpdateParamCallback { public: MyOnUpdateParam(ConfigurationTest *obj) { myobj = obj; } virtual void operator()(const char* config_set, const char* config_param) { RTC::ExecutionContextList_var ecs; ecs = myobj->get_owned_contexts(); ecs[(CORBA::ULong)0]->set_rate(myobj->m_interval); std::cout << "OnUpdateParamCallback\t" << config_set << "\t" << config_param << std::endl; } private: ConfigurationTest *myobj; };
class MyOnSetConfigurationSet : public RTC::OnSetConfigurationSetCallback { public: MyOnSetConfigurationSet(ConfigurationTest *obj) { myobj = obj; } virtual void operator()(const coil::Properties& config_set) { RTC::ExecutionContextList_var ecs; ecs = myobj->get_owned_contexts(); ecs[(CORBA::ULong)0]->set_rate(myobj->m_interval); std::cout << "OnSetConfiguration\t" << config_set.getName() << "\t" << config_set.getValue() << std::endl; } private: ConfigurationTest *myobj; };
class MyOnAddConfigurationAdd : public RTC::OnAddConfigurationAddCallback { public: MyOnAddConfigurationAdd(ConfigurationTest *obj) { myobj = obj; } virtual void operator()(const coil::Properties& config_set) { RTC::ExecutionContextList_var ecs; ecs = myobj->get_owned_contexts(); ecs[(CORBA::ULong)0]->set_rate(myobj->m_interval); std::cout << "OnAddConfigurationAdd\t" << config_set.getName() << "\t" << config_set.getValue() << std::endl; } private: ConfigurationTest *myobj; };
class MyOnRemoveConfigurationSet : public RTC::OnRemoveConfigurationSetCallback { public: MyOnRemoveConfigurationSet(ConfigurationTest *obj) { myobj = obj; } virtual void operator()(const char* config_set) { RTC::ExecutionContextList_var ecs; ecs = myobj->get_owned_contexts(); ecs[(CORBA::ULong)0]->set_rate(myobj->m_interval); std::cout << "OnRemoveConfigurationSet\t" << config_set << std::endl; } private: ConfigurationTest *myobj; };
class MyOnActivateSet : public RTC::OnActivateSetCallback { public: MyOnActivateSet(ConfigurationTest *obj) { myobj = obj; } virtual void operator()(const char* config_id) { RTC::ExecutionContextList_var ecs; ecs = myobj->get_owned_contexts(); ecs[(CORBA::ULong)0]->set_rate(myobj->m_interval); std::cout << "OnActivateSet\t" << config_id << std::endl; } private: ConfigurationTest *myobj; };
参考までに、コールバックが呼ばれると実行周期がコンフィギュレーションパラメーター Interval に設定されるという例を示します。 アクティブ、非アクティブに遷移したときにコールバックにより、コンフィギュレーションパラメーターが変更されることを確認します。
コンフィギュレーション(初期編) を参照して、初期パラメーターを設定します。
static const char* configurationtest_spec[] = { "implementation_id", "ConfigurationTest", "type_name", "ConfigurationTest", "description", "Configuration Test Component", "version", "1.0.0", "vendor", "hogehoge", "category", "TEST", "activity_type", "PERIODIC", "kind", "DataFlowComponent", "max_instance", "1", "language", "C++", "lang_type", "compile", // Configuration variables "conf.default.Interval", "1000", "conf.default.Test", "0", // Widget "conf.__widget__.Interval", "text", "conf.__widget__.Test", "text", // Constraints "conf.__constraints__.Interval", "0 < x < 10000", "" };
RTC::ReturnCode_t ConfigurationTest::onInitialize() { this->m_configsets.setOnUpdate(new MyOnUpdate(this)); this->m_configsets.setOnUpdateParam(new MyOnUpdateParam(this)); this->m_configsets.setOnSetConfigurationSet(new MyOnSetConfigurationSet(this)); this->m_configsets.setOnRemoveConfigurationSet(new MyOnRemoveConfigurationSet(this)); this->m_configsets.setOnAddConfigurationSet(new MyOnAddConfigurationAdd(this)); this->m_configsets.setOnActivateSet(new MyOnActivateSet(this)); bindParameter("Interval", m_interval, "1000"); bindParameter("Test", m_test, "0"); return RTC::RTC_OK; }
m_configsets は ConfigAdminクラス (コンフィギュレーション情報管理オブジェクト)で、ConfigAdmin.h にコールバックの定義があります。
まず、RTC を起動して、RTC を配置します。コンソールには以下のように表示されます。
RTC::ReturnCode_t ConfigurationTest::onActivated(RTC::UniqueId ec_id) { coil::Properties cproperties("default"); cproperties.setProperty("Interval", "1"); this->m_configsets.setConfigurationSetValues(cproperties); this->m_configsets.activateConfigurationSet("default"); std::cout << "Interval:\t" << m_interval << std::endl; this->m_configsets.update("default","Test"); std::cout << "Interval:\t" << m_interval << std::endl; this->m_configsets.update("default"); std::cout << "Interval:\t" << m_interval << std::endl; return RTC::RTC_OK; }
Interval を「1000」から「1」に設定します。
cproperties.setProperty("Interval", "1");
コンフィギュレーションセットを取得し追加します。このときに OnSetConfiguration が呼ばれます。
this->m_configsets.setConfigurationSetValues(cproperties);
コンフィギュレーションセットをアクティブ化します。このときに OnSetActivateSet が呼ばれますが、まだコンフィギュレーションパラメーターは変更されていません。
this->m_configsets.activateConfigurationSet("default");
もう一つのコンフィギュレーションパラメーター Test のみをアップデートします。Interval は「1000」のままです。
this->m_configsets.update("default","Test");
コンフィギュレーションセットを更新し Interval に「1」を設定します。
this->m_configsets.update("default");
次に、アクティブ化したまま RTSystemEditor で Interval を「2」に変更します。コンソールには以下のように表示されます。
続いて、RTC を非アクティブにします。コンソールには以下のように表示されます。
RTC::ReturnCode_t ConfigurationTest::onDeactivated(RTC::UniqueId ec_id) { coil::Properties cproperties("default"); cproperties.setProperty("Interval", "800"); this->m_configsets.setConfigurationSetValues(cproperties); this->m_configsets.activateConfigurationSet("default"); std::cout << "Interval:\t" << m_interval << std::endl; this->m_configsets.update("default","Interval"); std::cout << "Interval:\t" << m_interval << std::endl; this->m_configsets.update("default"); std::cout << "Interval:\t" << m_interval << std::endl; return RTC::RTC_OK; }
アクティブにしたときと異なるのは、Interval を 800 にした箇所と、m_configsets.update("default","Interval") として Interval のみ更新しているところです。 今回は、m_configsets.update("default","Interval") で値が更新されているか確認できます。
続いて、RTSyetemEditor でコンフィギュレーションセットを追加します。 図で [追加] ボタンをクリックした後に、[適用] ボタンをクリックすると OnAddConfigurationAddCallback を呼ぶことができます。 コンソールには以下のように表示されます。
同様に、コンフィギュレーションセットを削除することで OnRemoveConfigurationSetCallback を呼ぶことができます。
基本的に、パラメーターの変更は RTC のアクティビティが呼び出されるまではコンフィギュレーションパラメーターが外部で変更されても反映されませんが、コールバックを使うことでいろいろな設定ができるようになります。
RTC には、サービスポート以外に、SDO サービスと呼ばれるサービスインターフェースを追加することができます。
SDO は Super Distributed Object の略であり、OMG で標準化された分散コンポーネントの規格一つです。 RTC の実態である RTObject は、実は SDO のオブジェクトを継承していて、RTC は SDO のオブジェクトの一種であると言えます。 SDO では、コンポーネントの基本的なインターフェースが定義されています。 SDO のコンポーネントが持つサービスインターフェースは、SDOService インターフェースと呼ばれ、インターフェース定義を継承することになっています。 実は、RTC のポートや実行コンテキストも SDOService を継承しており、SDO サービスの一種となっています。
サービスポートと SDO サービスの違いは何でしょうか?
どちらも、RTC の外側に対してサービスを提供 (Provided) したり、外部のサービスを利用 (Required) するものです。 大きな違いは、サービスポートは RTC の内部のロジック(RTC 開発者が実装するコアロジック)の詳細にアクセスするための(あるいは、コアロジックから外部のサービスにアクセスするための)インターフェースを提供するのに対して、SDO サービスは、RTC 自身、すなわちコアロジックを包含するコンポーネントの機能の詳細にアクセスする(コンポー ネントの機能から外部のサービスにアクセスする)インターフェースを提供します。
SDO サービスの具体的な使われ方は以下のようなものです。
例えば、OpenRTM の拡張機能として ComponentObserver と呼ばれるものがあります。これは、外部のツールなどが、コンポーネント (RTC) 自身に何らかの状態変化があった際に、ポーリングをしなくとも通知を受け取ることができる仕組みです。
RTC の状態や、プロファイル、EC の状態、ポートの接続・切断を含む状態の変化、コンフィギュレーションの変更などに変更があった場合に、ツール等がその変更の通知を受け取ることができます。
これらの状態変化は、RTC の get_component_profile()、EC の get_profile()関数などを周期的に呼ぶ(ポーリングする)ことで、外部から知ることは可能です。しかし、RTC の様々な変化を知るために、複数のツールや外部の RTC から get_xxx() などの多数の関数を周期的に呼ぶことは非効率であり、変化の見地も最悪ケースではポーリングの周期の分の遅延が発生します。
ツールなどが、あらかじめコールバックオブジェクトをRTCに与えておき、変化があった場合にのみRTC側からそのオブジェクトの関数を即座に呼べば、遅延もなく変化が起きた場合にのみ関数がコールされるため効率的です。
また、こうした機能は RTC のコアロジックとは関係なく、RTC のフレームワークそのものに関連するサービス機能です。したがって、このようなサービスインターフェースは SDO サービスとして実装することが適切です。
なお、ComponentObserver のケースでは、RTC 側ではツールが提供するサービスオブジェクトの関数を呼ぶことで、その機能を実現します。すなわち、サービスの実装はツール側に存在し、RTC 側ではツールのサービスを利用することになります。したがって、このケースでは、RTC 側は SDO サービスのコンシューマ (Required インターフェース) を実装することとなります。
逆に、RTC 側がサービスを提供し、ツールなど外部からそのサービスを利用するケースも考えられます。この場合は、SDOサービスのプロバイダ (Provided インターフェース) を実装することになります。
SDO サービスプロバイダ、SDO サービスコンシューマ共に、通常は共有オブジェクトの形で提供され、所定の方法で RTC のプロセスからロード、ファクトリへの登録、インスタンス化されてサービスの提供または利用が開始されます。
SDO サービスは、1つのRTCに対して1種類につき1つの SDO サービスがインスタンス化され対応付けられます。プロセス単位であらかじめ定められたサービスがインスタンス化されます。
rtc.conf に設定可能な SDO サービス関連のオプションは次のようになっています。
SDO サービスプロバイダ関係の設定 | |
sdo.service.provider.available_services | 読み出しのみ。利用可能なサービスのリスト |
sdo.service.provider.enabled_services | 読み込まれた SDO サービスプロバイダのうち、有効にするもの。すべて有効の場合は ALL を指定 |
sdo.service.provider.providing_services | 読み出しのみ。利用されている SDO サービスのリスト。 |
SDO サービスコンシューマ関係の設定 | |
sdo.service.consumer.available_services | 読み出しのみ。利用可能な SDO サービスコンシューマのリスト。 |
sdo.service.consumer.enabled_services | 読み込まれた SDO サービスコンシューマのうち、有効にするもの。すべて有効の場合は ALL を指定 |
次節からは、SDO サービスの RTC 側でのプロバイダ、コンシューマの実装方法について説明します。
この節ではSDOサービスのコンシューマの実装方法について説明します。
SDOサービスコンシューマは、ツールなど外部に存在するサービスインターフェースをコールすることで機能するようなサービスを実現する手段です。
前述の ComponentObserver のように、RTC側から何かを通知したり、RTC側で外部のサービスを利用したりする場合にSDOサービスコンシューマを実装します。
まずはRTCのためのSDOコンシューマを実装する前に、外部にSDOサービスを実装する必要があります。現在は、CORBAのサーバとして、SDOPackage.idlのSDOServiceインターフェースを継承し、通常のCORBAサービスとして実装することになります。
この実装方法は、通常のCORBAサービスの実装の方法となりますので、ここでは割愛します。
前述のとおり、オブジェクトは通常、共有オブジェクト (so, DLL) としてコンパイル・リンクされます。このオブジェクトがRTCのプロセスにロードされ動作する実際のライフサイクルは以下の通りとなります。
[RTC] [SDO consumer] [Configuration] [SDO service] [Other] | : | | | | : get_configuration() | | |<---------:-------------------------------|------------| | : | | | | : | add_service_profile(prof) | | : create() |<----------------|------------| | |<------------| | | | | call_sdo_service() | | | |-------------|---------------->| | | | call_sdo_service2() | | | |-------------|---------------->| | | | | : | | | | | | | | | | remove_service_profile(id) | | | delete() |<----------------|------------| | x<------------| | | | | x x
SDOサービスコンシューマを実装する際には、SdoServiceConsumerBase 基底クラスを継承した一つのクラスを作成します。
#include <rtm/SdoServiceConsumerBase.h> class MySdoServiceConsumer : SdoServiceConsumerBase {
このクラスの実装に当たっては、少なくとも以下の純粋仮想関数および、グローバルなモジュール初期化関数を実装する必要があります。
以下に、各関数の詳細な振る舞いを示す。
関数プロトタイプ bool init(RTObject_impl& rtobj, const SDOPackage::ServiceProfile& profile)
初期化関数。与えられた RTObject および ServiceProfile から、当該オブジェクトを初期化します。外部からSDOサービスが ServiceProfile とともにアタッチされると、SDOコンシューマがインスタンス化され、その直後に SDO サービスがアタッチされた RTC と与えられた ServiceProfile を引数としてこの関数が呼ばれる。
関数内では、ServiceProfile 内の SDO サービスリファレンスを CorbaConsumer クラス等を利用しオブジェクト内に保持するとともに、properties から設定内容を読み込みサービス固有の設定等を行う。与えられたサービスのオブジェクトリファレンスが不正、あるいは properties の内容が不正、等の場合は戻り値に false を返す。
関数プロトタイプ bool reinit(const SDOPackage::ServiceProfile& profile)
再初期化関数。ServiceProfile は設定情報更新のため同一IDで呼び出されることが有りますが、その際にこの関数が新たな ServiceProfile とともに呼び出されます。関数内では、設定の変更など再初期化処理を実装します。
関数プロトタイプ const SDOPackage::ServiceProfile& getProfile() const
設定されたプロファイルを返す関数です。
関数プロトタイプ const SDOPackage::ServiceProfile& getProfile() const
終了処理。コンシューマがデタッチされる際に呼び出される関数です。関数内では終了処理を実装します。
関数プロトタイプ DLL_EXPORT void ComponentObserverConsumerInit()
この関数は共有オブジェクト (.so や .dll) のエントリポイントとなります。この関数内では、RTC::SdoServiceConsumerFactory に対して、当該SDOコンシューマオブジェクトのインターフェースIDおよび生成(Creator)・破壊(Desctuctor)関数(ファンクタ)を登録します。
以下は、典型的なInit() 関数の実装例です。
extern "C" { void MySdoServiceConsumerInit() { RTC::SdoServiceConsumerFactory& factory = RTC::SdoServiceConsumerFactory::instance(); factory.addFactory(CORBA_Util::toRepositoryId<OpenRTM::MySdoService>(), ::coil::Creator< ::RTC::SdoServiceConsumerBase, ::RTC::MySdoServiceConsumer>, ::coil::Destructor< ::RTC::SdoServiceConsumerBase, ::RTC::MySdoServiceConsumer>); } };
SdoServiceConsumer は通常共有オブジェクトとしてコンパイル・リンクされます。
共有オブジェクトのエントリポイントは通常コンパイルされたファイル名の basename + "Init" となります。(別の名称の場合、rtc.confのオプションで別途指定する必要があります。)
以下に、クラス名、ファイル名、エントリポイント関数名の推奨例を示します。
この節では SDO サービスのプロバイダの実装方法について説明します。
SDO サービスプロバイダは、自らサービスを外部のツールやアプリケーション・RTC などに対して提供する主体となります。
前述のとおり、SDO サービスプロバイダのオブジェクトは通常、共有オブジェクト (so、DLL) としてコンパイル・リンクされます。 このオブジェクトが RTC のプロセスにロードされ動作する実際のライフサイクルは以下の通りとなります。
[RTC] [SDO service] [Other] | : | | instantiate : | |------------->: | | init() | | |------------->| | | | get_service_profiles() | |<--------------------------------------| | | get_sdo_service() | |<--------------------------------------| | | use service | | |<-----------------------| | | | | finalize() | | |------------->x | x x |
SDO サービスプロバイダを実装する際には、SdoServiceProviderBase 基底クラスおよび、CORBA サーバントスケルトンクラスを継承した一つのクラスを作成します。
#include <rtm/SdoServiceProviderBase.h> class MySdoServiceConsumer : SdoServiceProviderBase, {
このクラスの実装に当たっては、少なくとも以下の純粋仮想関数および、グローバルなモジュール初期化関数を実装する必要があります。
以下に、各関数の詳細な振る舞いを示す。
関数プロトタイプ bool init(RTObject_impl& rtobj, const SDOPackage::ServiceProfile& profile)
初期化関数。与えられた RTObject および ServiceProfile から、当該オブジェクトを初期化します。このサービスが sdo.service.provider.enabled_services で有効化されていれば、この関数は対応するRTCがインスタンス化された直後に呼び出されます。
ServiceProfile には以下の情報が入った状態で呼び出されます。
ServiceProfile.id | 当該サービスのIFR型 |
ServiceProfile.interface_type | 当該サービスのIFR型 |
ServiceProfile.service | 当該サービスのオブジェクト参照 |
ServiceProfile.properties | rtc.conf や <component>.conf 等で与えられた SDOサービス固有のオプションが渡される。confファイル内では、<pragma>.<module_name>.<interface_name> というプリフィックスをつけたオプションとして与えることができ、properties 内には、このプリフィックスを除いたオプションが key:value 形式で含まれている。 |
関数内では、主に properties から設定内容を読み込みサービス固有の設定等を行います。与えられた ServiceProfileの内容が不正、あるいはその他の理由で当該サービスをインスタンス化しない場合は false を返します。その場合、finalize() が呼び出されその後オブジェクトは削除されます。それ以外の場合は true を返すと、サービスオブジェクトは RTC 内に保持されます。
関数プロトタイプ bool reinit(const SDOPackage::ServiceProfile& profile)
再初期化関数。ServiceProfile は設定情報更新のため同一IDで呼び出されることが有りますが、その際にこの関数が新たな ServiceProfile とともに呼び出されます。関数内では、設定の変更など再初期化処理を実装します。
関数プロトタイプ const SDOPackage::ServiceProfile& getProfile() const
設定されたプロファイルを返す関数です。
関数プロトタイプ const SDOPackage::ServiceProfile& getProfile() const
終了処理。RTCオブジェクトが解体されるか、init()処理においてfalseが返された場合は、この関数が呼び出されたのちオブジェクトは解体されます。関数内では終了処理を実装します。
関数プロトタイプ DLL_EXPORT void ComponentObserverProviderInit()
この関数は共有オブジェクト (.so や .dll) のエントリポイントとなります。この関数内では、RTC::SdoServiceProviderFactory に対して、当該 SDOプロバイダオブジェクトのインターフェースIDおよび生成(Creator)・破壊(Desctuctor)・関数(ファンクタ)を登録します。
以下は、典型的な Init() 関数の実装例です。
extern "C" { void MySdoServiceProviderInit() { RTC::SdoServiceProviderFactory& factory = RTC::SdoServiceProviderFactory::instance(); factory.addFactory(CORBA_Util::toRepositoryId<OpenRTM::MySdoService>(), ::coil::Creator< ::RTC::SdoServiceProviderBase, ::RTC::MySdoServiceProvider>, ::coil::Destructor< ::RTC::SdoServiceProviderBase, ::RTC::MySdoServiceProvider>); } };
SdoServiceProvider は通常共有オブジェクトとしてコンパイル・リンクされます。
共有オブジェクトのエントリポイントは通常コンパイルされたファイル名の basename + "Init" となります。(別の名称の場合、rtc.confのオプションで別途指定する必要があります。)
以下に、クラス名、ファイル名、エントリポイント関数名の推奨例を示します。