JRubyで動くRTコンポーネントを作る

近いうちに、といいつつ5ヶ月も過ぎてしまいました。JRubyでのRTコンポーネントの書き方です。
今回はRTC Builder は使わずに、一から手でRTコンポーネントを書きます……といっても、基本的にはJythonと同じコードをJRubyで書くだけです。

準備

カレントディレクトリと、OpenRTM-aist-Java のjarファイルに対してCLASSPATHを設定します。

export CLASSPATH=.:/usr/local/lib/OpenRTM-aist/1.0/jar/OpenRTM-aist-1.0.0.jar:/usr/local/lib/OpenRTM-aist/1.0/jar/commons-cli-1.1.jar

Jython の時と同様に、OpenRTM-aist-Java のサンプルから MyService.idl をコピーしてコンパイルします。

 % idlj -fall MyService.idl
 % cd SimpleService
 % javac *.java

SimpleService ディレクトリの下に、.java ファイルとそれに対応した .class ファイルができます。

MyServiceProvider.rb の作成

最初に、RTコンポーネントとして必要なJavaライブラリをインポートします。

require 'java'

begin
  java_import 'jp.go.aist.rtm.RTC.util.Properties'
  java_import 'jp.go.aist.rtm.RTC.Manager'
  java_import 'jp.go.aist.rtm.RTC.ModuleInitProc'
  java_import 'jp.go.aist.rtm.RTC.DataFlowComponentBase'
  java_import 'jp.go.aist.rtm.RTC.RtcNewFunc'
  java_import 'jp.go.aist.rtm.RTC.RtcDeleteFunc'
  java_import 'jp.go.aist.rtm.RTC.port.CorbaPort'
  java_import 'jp.go.aist.rtm.RTC.port.CorbaConsumer'
rescue
  puts !$
  puts 'retry...'
  retry
end

ときどき java_import が失敗して例外を吐きますが、retry すれば通ります。

次に、別ファイルで定義した MyServiceSVC_impl クラスを読み込みます。
これは Python でいう MyService_idl_example.py に相当し、MyServicePOA クラスを継承して、分散オブジェクトのオペレーション(関数)を実装します。(リストはあとで)

require 'MyServiceSVC_impl'

続いてRTコンポーネントのモジュール定義部分です。

$myservice_provider_spec = ["implementation_id", "MyServiceProvider",
                            "type_name",         "MyServiceProvider",
                            "description",       "MyService Provider Sample component",
                            "version",           "1.0.0",
                            "vendor",            "You & Me",
                            "category",          "Test",
                            "activity_type",     "DataFlowComponent",
                            "max_instance",      "10",
                            "language",          "JRuby",
                            "lang_type",         "script",
                            ""]

Python,Jythonのものとほぼ同じですが、languageがJRubyとなり、変数名の頭にグローバル変数を示す「$」が必要です。
次に、MyServiceProvider クラスを定義します。

class MyServiceProvider < DataFlowComponentBase
  def initialize(manager)
    super(manager)
    @myServicePort = CorbaPort.new("MyService")

    # initialization of Consumer
    @myservice0 = MyServiceSVC_impl.new
  end

  def onInitialize
    # Set service consumers to Ports
    @myServicePort.registerProvider("myservice0", "MyService", @myservice0)

    # Set CORBA Service Ports
    addPort(@myServicePort)

    super
  end
end

このクラスには、必要に応じて onExecute などのメソッドを定義することができます。

次に、インスタンス生成クラスと、削除クラスを作成します。

class MyServiceNewFunc
  include RtcNewFunc
  def createRtc(manager)
    MyServiceProvider.new(manager)
  end
end

class MyServiceDeleteFunc
  include RtcDeleteFunc
  def deleteRtc(rtcBase)
    rtcBase = nil
  end
end

Javaインターフェースである RtcNewFunc と RtcDeleteFunc を、include で継承するのが Ruby 流です。
この二つのクラスを、マネージャに登録するMyServiceProviderInit 関数の定義はこうなります。

def MyServiceProviderInit(manager)
  spec = $myservice_provider_spec.to_java(:String)
  profile = Properties.new(spec)
  manager.registerFactory(profile,
                          MyServiceNewFunc.new,
                          MyServiceDeleteFunc.new)
end

モジュール定義の配列を to_java(:String) でJava文字列の配列に変換することで、Properties のコンストラクタに渡せるようになります。

そして、モジュール初期化クラス MyModuleInitProc を定義します。

class MyModuleInitProc
  include ModuleInitProc
  def myModuleInit(manager)
    MyServiceProviderInit(manager)
    # Create a component
    manager.createComponent("MyServiceProvider")
  end
end

最後にmainにあたるコードを書きます。

mgr = Manager.init(ARGV)
mgr.setModuleInitProc(MyModuleInitProc.new)
mgr.activateManager
mgr.runManager

MyServiceSVC_impl.rb

MyServiceSVC_impl.rb の内容は以下のようになります。
MyServicePOAのサブクラスで、echo, set_value, get_value, get_echo_history, get_value_history の5つの命令が呼ばれたときの処理を定義します。
各処理がbegin~rescue でくくってあるのは、エラーが出たとき、原因がこの実装部分にあるかどうかをはっきりさせるためです。

require 'java'
begin
  java_import 'SimpleService.MyServicePOA'
rescue
  puts $!
  puts 'retry...'
  retry
end

class MyServiceSVC_impl < MyServicePOA
  def initialize
    @echo_list = []
    @value_list = []
    @my_value = 0
    @value_list << @my_value
  end

  def echo(msg)
    begin
      @echo_list << msg
      10.times do
        puts "Message: #{msg}"
        sleep 1
      end
      msg
    rescue
       puts $!
    end
  end

  def get_echo_history
    begin
      @echo_list.each.with_index do |msg, i|
         puts "#{i}: #{msg}"
      end
      @echo_list
    rescue
      puts $!
    end
  end

  def set_value(value)
    begin
      @value_list << value
      @my_value = value
      puts "Current value: #{@my_value}"
    rescue
      puts $!
    end
  end

  def get_value
    begin
      @my_value
    rescue
      puts $!
    end
  end

  def get_value_history
    begin
      @value_list.each.with_index do |msg, i|
         puts "#{i}: #{msg}"
      end
      @value_list
    rescue
      puts $!
    end
  end
end

実行

実行は

 % jruby  MyServiceProvider.rb

とします。これだけでは画面上には何も出ません。
RTシステムエディタでlocalhostのhost_cxt下で、MyServiceProvider0|rtc が有効になっていればひとまずOKです。
Jython版の MyServiceConsumer.py と接続して、動作テストをしてみてもいいでしょう。

MyServiceConsumer.rb

MyServiceProvider.rb の場合とほぼ同じですが、MyServiceSVC_impl.rb などの外部ファイルは必要なく、1ファイルで完結します。

require 'java'

begin
  java_import 'jp.go.aist.rtm.RTC.util.Properties'
  java_import 'jp.go.aist.rtm.RTC.Manager'
  java_import 'jp.go.aist.rtm.RTC.ModuleInitProc'
  java_import 'jp.go.aist.rtm.RTC.DataFlowComponentBase'
  java_import 'jp.go.aist.rtm.RTC.RtcNewFunc'
  java_import 'jp.go.aist.rtm.RTC.RtcDeleteFunc'
  java_import 'jp.go.aist.rtm.RTC.port.CorbaPort'
  java_import 'jp.go.aist.rtm.RTC.port.CorbaConsumer'
  java_import 'SimpleService.MyService'
rescue
  puts $!
  puts "retry..."
  retry
end

require 'readline'

$myserviceconsumer_spec = ["implementation_id", "MyServiceConsumer",
                          "type_name",         "MyServiceConsumer",
                          "description",       "MyService Consumer Sample component",
                          "version",           "1.0.0",
                          "vendor",            "You & Me",
                          "category",          "Test",
                          "activity_type",     "DataFlowComponent",
                          "max_instance",      "1",
                          "language",          "JRuby",
                          "lang_type",         "script",
                          ""]

class MyServiceConsumer < DataFlowComponentBase
    def initialize(manager)
      super(manager)
      @myServicePort = CorbaPort.new("MyService")

      # initialization of Consumer
      @myservice0 = CorbaConsumer.new(MyService.java_class)
    end
    
    def onInitialize
      # Set service consumers to Ports
      @myServicePort.registerConsumer("myservice0", "MyService", @myservice0)
  
      # Set CORBA Service Ports
      addPort(@myServicePort)
  
      super
    end
    
  def onExecute(ec_id)
    begin
      puts "Command list: "
      puts " echo [msg]       : echo message."
      puts " set_value [value]: set value."
      puts " get_value        : get current value."
      puts " get_echo_history : get input messsage history."
      puts " get_value_history: get input value history."

      line = Readline.readline("> ", true)
      if line and not line.empty?
        argv = line.split
        argv[-1].rstrip! if argv[-1]
      end

      if argv[0] == "echo" and argv.length > 1
        retmsg = @myservice0._ptr.echo(argv[1])
        puts "echo() return: ", retmsg
        return super
      end

      if argv[0] == "set_value" and argv.length > 1
        val = argv[1].to_f
        @myservice0._ptr.set_value(val)
        puts "Set remote value: ", val
        return super
      end
          
      if argv[0] == "get_value"
        retval = @myservice0._ptr.get_value
        puts "Current remote value: ", retval
        return super
      end
      
      if argv[0] == "get_echo_history"
        echo_history = @myservice0._ptr.get_echo_history
        echo_history.length.times do |i|
          puts "#{i}: #{echo_history[i]}"
        end
        return super
      end
      
      if argv[0] == "get_value_history"
        value_history = @myservice0._ptr.get_value_history
        puts value_history, value_history.length
        value_history.length.times do |i|
          puts "#{i}: #{value_history[i]}"
        end
        return super
      end
      
      puts "Invalid command or argument(s)."
      super
    end
  rescue
    puts $!
    super
  end
end

class MyServiceNewFunc 
  include RtcNewFunc
  def createRtc(manager)
      MyServiceConsumer.new(manager)
  end  
end

class MyServiceDeleteFunc 
  include RtcDeleteFunc
  def deleteRtc(rtcBase)
      rtcBase = nil
  end  
end

def MyServiceConsumerInit(manager)
    spec = $myserviceconsumer_spec.to_java(:String)
    profile = Properties.new(spec)
    manager.registerFactory(profile,
                            MyServiceNewFunc.new,
                            MyServiceDeleteFunc.new)
end

class MyModuleInitProc 
  include ModuleInitProc
  
  def myModuleInit(manager)
      MyServiceConsumerInit(manager)
      # Create a component
      manager.createComponent("MyServiceConsumer")
  end
end

mgr = Manager.init(ARGV)
mgr.setModuleInitProc(MyModuleInitProc.new)
mgr.activateManager()
mgr.runManager()

実行は

 % jruby MyServiceConsumer.rb

とします。

MyServiceProvider.rb と MyServiceConsumer.rb を実行して、RTシステムエディタで接続、アクティベートすると、端末にコマンドの入力を求めるプロンプトが出ます。

それぞれ、Jython版やJava版、Python版、C++版のサンプルと差し替えても動作します。