MessagePack-RPCを使ってRubyからPython（またはPythonからRuby）のメソッドを呼ぶ

MessagePack-RPCを使ってRubyからPython（またはその逆）のメソッドを呼ぶ方法についてのまとめ。

- Rubyを主に使っているが、Pythonのnumpyやmatplotlibなどの科学技術計算ライブラリを使いたい
- Pythonを主に使っているが、一部でRailsアプリを操作したい

といった用途で使えば、それぞれの言語の良いところを利用できる可能性がある。

# 前提

- Ruby 2.3.1
    - msgpack-rpc 0.5.4
- Python 3.5.2
    - msgpack-rpc-python 0.4

# MessagePack-RPCとは

Remote Procedure Call のプロトコル。MessagePackを使ってシリアライズする。

仕様の詳細についてはここを参照。

- https://github.com/msgpack-rpc/msgpack-rpc

様々な言語でMessagePack-RPCを実装したライブラリが公開されている。
Ruby,Pythonで実装したものが以下の２つ。他にも色々な言語の実装が公開されている。

- https://github.com/msgpack-rpc/msgpack-rpc-ruby
- https://github.com/msgpack-rpc/msgpack-rpc-python

ちなみにneovimのプラグインはMessagePack-RPCを使って実装できるらしい。
MessagePack-RPCの仕様に沿ってさえいればよいので、様々な言語でプラグインを実装できるというメリットがある。

Ruby版は公式のgithubページを見てもほとんどREADMEに情報がないので、ここで簡単に使い方をまとめておく。

# ミニマルな例

まずはRuby版のサーバーの例

```rb:server.rb

require 'msgpack/rpc'

class MyHandler
  def add(x,y)
    return x+y
  end
end

svr = MessagePack::RPC::Server.new
svr.listen('localhost', 18800, MyHandler.new)
svr.run
```

Ruby版クライアントの例

```rb:client.rb
require 'msgpack/rpc'
c = MessagePack::RPC::Client.new('localhost',18800)
result = c.call(:add, 1, 2)
puts result
```

以下のように実行する。

```
ruby server.rb &   # serverを起動
ruby client.rb     # 演算の結果が表示される
```

全く同じようにPythonでも記述可能。

```py3:server.py
import msgpackrpc

class MyHandler(object):

    def add(self, x, y):
        return x+y

svr = msgpackrpc.Server( MyHandler(), unpack_encoding='utf-8' )
svr.listen( msgpackrpc.Address('localhost',18800) )
svr.start()
```

```py3:client.py
import msgpackrpc

client = msgpackrpc.Client(msgpackrpc.Address("localhost", 18800), unpack_encoding='utf-8')
result = client.call('add', 1, 2)
print( result )
```

当然ながら、RubyのserverとPythonのclient （または逆）を実行しても全く同じ結果になる。

Pythonの場合には気をつけるべき点として、ClientまたはServerを初期化する時にデフォルトでは`unpack_encoding='None'`となっていて、転送されてくるデータをバイト列として解釈する。
上記のサンプルでは数値を送受信しているだけなので問題ないが、文字列を送受信したい場合には `unpack_encoding='utf-8'` を指定する必要がある。そうしないと受信したデータはバイト列となり、プログラム内で明示的に `.decode('utf-8')` を呼ぶ必要がある。
バイナリデータを送りたい場合を除いて、デフォルトで `unpack_encoding='utf-8'` を指定しておくのが良さそう。

# 非同期実行

server側の処理は一瞬で終わる場合ばかりとは限らず、時間がかかる処理を実行したい場合も考えられる。
そのようなケースのために非同期実行の仕組みも用意されている。

`call_async` で呼んだメソッドはすぐに処理が返り、`future`オブジェクトが返される。
Futureオブジェクトは`#get`が呼ばれた時点でserverから結果が返ってきていればその値を返す。
もしserverから結果が返ってきていなければ、serverから結果が得られるまで待つ。

以下のサンプルコードを見るのが早いだろう。

```rb:async_client.rb
require 'msgpack/rpc'
c = MessagePack::RPC::Client.new('localhost',18800)

puts "async call"
future1 = c.call_async(:delayed_add, 1, 1, 2)
future2 = c.call_async(:delayed_add, 1, 2, 3)
puts future2.get     # 順番はかならずしもcallした順番でなくてもよい
puts future1.get
```

```rb:async_server.rb
require 'msgpack/rpc'

class MyHandler
  def delayed_add(t,x,y)
    puts "delayed_add is called"
    as = MessagePack::RPC::AsyncResult.new
    Thread.new do
      sleep t
      as.result(x+y)
    end
    as
  end
end
```

Pythonについても同じように `future.get()` で結果を取得できる。

```py3:async_client.py
import msgpackrpc

client = msgpackrpc.Client(msgpackrpc.Address("localhost", 18800), unpack_encoding='utf-8')
future = client.call_async('delayed_add', 3, 1, 2)
print( future.get() )
```

```py3:async_server.py
import msgpackrpc
import threading, time

class MyHandler(object):
    def delayed_add(self, t, x, y):
        print("delayed_add is called")
        ar = msgpackrpc.server.AsyncResult()
        def sleep_add():
            time.sleep(t)
            ar.set_result(x+y)
        thread = threading.Thread(target=sleep_add)
        thread.start()
        return ar

svr = msgpackrpc.Server( MyHandler(), unpack_encoding='utf-8' )
svr.listen( msgpackrpc.Address('localhost',18800) )
svr.start()
```


# Rubyからmatplotlibを呼ぶ

もう少し実用的な例として、Rubyの数値配列をPythonのmatplotlibで描画する例を示す。

```py3:plot_server.py
import msgpackrpc
import matplotlib.pyplot as plt

svr = msgpackrpc.Server( plt, unpack_encoding='utf-8' )
svr.listen( msgpackrpc.Address('localhost',18800) )
svr.start()

```

```rb:plot_client.rb
require 'msgpack/rpc'
c = MessagePack::RPC::Client.new('localhost',18800)
c.timeout = Float::INFINITY

xs = -3.14.step(3.14, 0.1).to_a
ys = xs.map {|x| Math.sin(x) }
c.call( :scatter, xs, ys )
c.call( :show )
```

以下のようなwindowが出て、プロットが描画される。

![image](https://qiita-image-store.s3.amazonaws.com/0/28792/2b86ca53-46f6-72fc-88f9-4d40dcde8859.png)

注意点として、描画されたウインドウが閉じられるまでserverから処理が返ってこない。
そのままではタイムアウトしてclient側で例外が発生するので、`c.timeout = Float::INFINITY`としている。

ここではシェルから手動でからRubyとPythonのプロセスを起動しているが、Rubyのプログラム内でPythonを外部プロセスとして起動した方が、直接Rubyからplotしているかのようなインターフェースになってすっきりしそう。

# PythonからRailsのメソッドを呼ぶ

逆方向の例として、PythonからRailsアプリの中の情報を取得する

```rb:rails_server.rb
require 'msgpack/rpc'
require File.join( ENV['RAILS_ROOT'], 'config/environment' )

Object.class_eval do
  def to_msgpack(*args)  # to_msgpackが定義されていない場合、to_sを呼んでからmsgpackに変換する仕様にする
    to_s.to_msgpack(*args)
  end
end

class MyRailsHandler
  def get_book(name)
    Book.where(name: name).first.as_json
    # as_jsonでHashにする。
  end
end

svr = MessagePack::RPC::Server.new
svr.listen('localhost', 18800, MyRailsHandler.new)
svr.run
```

```py3:rails_client.py
import msgpackrpc

client = msgpackrpc.Client(msgpackrpc.Address("localhost", 18800), unpack_encoding='utf-8')
result = client.call('get_book', 'echo')   # resultはdictionary
print( result )
```

ポイントは、Objectに対して`to_msgpack(*args)`を定義すること。
`to_msgpack`が定義されていないクラスに対しては、ここで定義した`Object#to_msgpack(*args)`が呼ばれる。これによりTimeオブジェクトなどもシリアライズできるようになる。

# RPCでは実現が難しい処理

今まで見てきたような１つのメソッドを呼び出すだけの単純な処理ならばRPCで処理できる。しかし、以下のような場合はRPCでは簡単には解決しなさそう

- 関数にblockを渡したい場合
    - 例えば `array.map {|x| f(x) }` を実行したいときにfをRPCで渡すことはできない
    - msgpackでシリアライズできないので原理的に無理
- メソッドチェーンする場合
    - 例えば、Rubyで良く見かける `Books.where(author: "foo").asc(:price).first` というような処理。PythonからRailsのAPIを呼びたい場合に制限が多くなりそう。
    - 何度かリクエストを送っても良いが、中間の状態をserver側で保持しなくてはならないのが気持ち悪い