@ledsun blog

無味の味は佳境に入らざればすなわち知れず

PyCallでsendメソッドを直接呼び出せるのか?

ruby.wasmからのWebSocket - @ledsun blog にてruby.wasmからJavaScriptWebSocket.sendを直接呼び出せませんでした。

Rubyから他言語のオブジェクトのメソッドを呼び出す先輩にはGitHub - mrkn/pycall.rb: Calling Python functions from the Ruby languageがあります。 PyCallではどのような仕様なっているのでしょうか? PyCallからPythonのオブジェクトのsendメソッドを直接呼び出せるのでしょうか?

結論。直接は呼び出せません。呼び出す時はPyCall.getattrを使います。

sendメソッドを直接呼び出すとmethod_missing

実行環境を準備します。

bundle init
bundle add pycall

次のRubyスクリプトを用意します。

require 'pycall'
client = PyCall.import_module 'http.client'
conn = client.HTTPConnection('www.example.com')
conn.send('GET / HTTP/1.0\n\n')

Pythonの標準ライブラリーに含まれる http.client --- HTTP プロトコルクライアント — Python 3.12.5 ドキュメント にsendメソッドがあります。 これで試してみます。

実行します。

> ruby test_send.rb
/home/ledsun/.rbenv/versions/3.3.4/lib/ruby/gems/3.3.0/gems/pycall-1.5.2/lib/pycall/pyobject_wrapper.rb:54:in `method_missing': undefined method `GET / HTTP/1.0\n\n' for class <class 'http.client.HTTPConnection'> (NoMethodError)
        from test_send.rb:5:in `<main>'

method_missing': undefined methodGET / HTTP/1.0\n\n'

これはRubyObject#send メソッドが呼ばれた時のエラーです。 エラーが起きた箇所のソースコードを見てみます。

https://github.com/mrkn/pycall.rb/blob/master/lib/pycall/pyobject_wrapper.rb#L35-L53

def method_missing(name, *args)
  name_str = name.to_s if name.kind_of?(Symbol)
  name_str.chop! if name_str.end_with?('=')
  case name
  when *OPERATOR_METHOD_NAMES.keys
    op_name = OPERATOR_METHOD_NAMES[name]
    if LibPython::Helpers.hasattr?(__pyptr__, op_name)
      LibPython::Helpers.define_wrapper_method(self, op_name)
      singleton_class.__send__(:alias_method, name, op_name)
      return self.__send__(name, *args)
    end
  else
    if LibPython::Helpers.hasattr?(__pyptr__, name_str)
      LibPython::Helpers.define_wrapper_method(self, name)
      return self.__send__(name, *args)
    end
  end
  super # << ここでエラーが起きています。
end

method_missingが呼ばれています。 これはObject#sendの引数で'GET / HTTP/1.0\n\n'が実行されて起きています。

試しにmethod_missingの先頭にデバッグ出力を足してみます。

def method_missing(name, *args)
  p "method_missing: #{name}"
  name_str = name.to_s if name.kind_of?(Symbol)
  ...

動作確認用のRubyスクリプトを実行します。

►ruby test_send.rb
"method_missing: path"
"method_missing: append"
"method_missing: dict"
"method_missing: list"
"method_missing: slice"
"method_missing: HTTPConnection"
"method_missing: GET / HTTP/1.0\\n\\n"
"method_missing: repr"
/home/ledsun/.rbenv/versions/3.3.4/lib/ruby/gems/3.3.0/gems/pycall-1.5.2/lib/pycall/pyobject_wrapper.rb:53:in `method_missing': undefined method `GET / HTTP/1.0\n\n' for class <class 'http.client.HTTPConnection'> (NoMethodError)
        from test_send.rb:5:in `<main>'

やはり、引数にsendは来ていません。

追記:sendを呼び出すにはPyCall.getattrを使う

コメント欄にて id:mrkn さんからsendメソッドの呼び出し方を教えてもらいました。 さらに、他の間違いも教えてもらっています。 修正したスクリプトが次です。

require 'pycall'
client = PyCall.import_module 'http.client'
conn = client.HTTPConnection.new('www.example.com')
PyCall.getattr(conn, 'send').("GET / HTTP/1.0\r\n\r\n".b)

これを実行するとエラーは起きません。

►ruby test_send.rb

HTTPConnection.sendの戻り値は特に処理していないので、表示は何もされません。