LoginSignup
12
9

More than 5 years have passed since last update.

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

Last updated at Posted at 2016-11-07

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版のサーバーの例

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版クライアントの例

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でも記述可能。

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()
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から結果が得られるまで待つ。

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

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
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() で結果を取得できる。

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() )
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で描画する例を示す。

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()

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

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

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

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

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

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
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では実現が難しい処理

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

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

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
9