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を使ってシリアライズする。
仕様の詳細についてはここを参照。
様々な言語でMessagePack-RPCを実装したライブラリが公開されている。
Ruby,Pythonで実装したものが以下の2つ。他にも色々な言語の実装が公開されている。
ちなみにneovimのプラグインはMessagePack-RPCを使って実装できるらしい。
MessagePack-RPCの仕様に沿ってさえいればよいので、様々な言語でプラグインを実装できるというメリットがある。
Ruby版は公式のgithubページを見てもほとんどREADMEに情報がないので、ここで簡単に使い方をまとめておく。
ミニマルな例
まずはRuby版のサーバーの例
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版クライアントの例
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でも記述可能。
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()
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から結果が得られるまで待つ。
以下のサンプルコードを見るのが早いだろう。
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
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()
で結果を取得できる。
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() )
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で描画する例を示す。
import msgpackrpc
import matplotlib.pyplot as plt
svr = msgpackrpc.Server( plt, unpack_encoding='utf-8' )
svr.listen( msgpackrpc.Address('localhost',18800) )
svr.start()
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が出て、プロットが描画される。
注意点として、描画されたウインドウが閉じられるまでserverから処理が返ってこない。
そのままではタイムアウトしてclient側で例外が発生するので、c.timeout = Float::INFINITY
としている。
ここではシェルから手動でからRubyとPythonのプロセスを起動しているが、Rubyのプログラム内でPythonを外部プロセスとして起動した方が、直接Rubyからplotしているかのようなインターフェースになってすっきりしそう。
PythonからRailsのメソッドを呼ぶ
逆方向の例として、PythonからRailsアプリの中の情報を取得する
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
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では実現が難しい処理
今まで見てきたような1つのメソッドを呼び出すだけの単純な処理ならばRPCで処理できる。しかし、以下のような場合はRPCでは簡単には解決しなさそう
- 関数にblockを渡したい場合
- 例えば
array.map {|x| f(x) }
を実行したいときにfをRPCで渡すことはできない - msgpackでシリアライズできないので原理的に無理
- 例えば
- メソッドチェーンする場合
- 例えば、Rubyで良く見かける
Books.where(author: "foo").asc(:price).first
というような処理。PythonからRailsのAPIを呼びたい場合に制限が多くなりそう。 - 何度かリクエストを送っても良いが、中間の状態をserver側で保持しなくてはならないのが気持ち悪い
- 例えば、Rubyで良く見かける