🌉

Babashka pods で Ruby を実行する簡単なサンプル

に公開

きっかけ

最近 Clojure に興味が出て、Babashka を触っているのですが、
Babashka には pods と呼ばれる、標準入出力上を Bencode で他のプログラムとやりとりできる仕組みがあります。
https://github.com/babashka/pods

標準入出力上でやりとりするプロトコルに則っていれば、何のプログラムでもよいので、
例えば、Python で SQLite に対して SQL を実行するスクリプトを、Babashka から実行して結果を取得できます。
https://github.com/babashka/pods/tree/master/examples/pod-lispyclouds-sqlite

おもしろそうだったので、Ruby を実行するサンプルを作りました。

できたもの

https://youtu.be/wKdP2_6YtoA

(require '[babashka.pods :as pods])
(pods/load-pod "./pod_test.rb")

(require '[pod.ruby.test :as ruby])

(ruby/execute! "1 + 2") ;; => 3

のような形で Ruby のプログラムを実行します。
https://github.com/tkmfujise/babashka-pod-ruby-test

Bencode のデコード/エンコードには、ruby-bencode という Gem を使用しました。

仕組み

Clojure

(require '[babashka.pods :as pods])

は、おまじないです。

(pods/load-pod "./pod_test.rb")

には、スクリプトの場所を指定します。
スクリプトは実行可能にしておく必要があります($ chmod +x)。

その後、pod を require します。

(require '[pod.ruby.test :as ruby])

pod.ruby.test には、後述する describe メッセージの name で指定したものが入ります。

(ruby/execute! "1 + 2")

の、execute! 部分は、後述する describe メッセージの vars で指定し、呼び出された場合は invoke メッセージの var に入って来ます。

Bencode

以下、標準入出力の Bencode のやりとりです。
何をしているか Bencode を見てもわからないので、デコード結果も添えています。

1. describe: Babashka => Ruby

(pods/load-pod "./pod_test.rb")

を実行すると、describe メッセージが発行されます。

標準入力
d2:id36:27317a7f-9686-4740-9520-e9c17ffe9bc32:op8:describee
BEncode.load 'd2:id36:27317a7f-9686-4740-9520-e9c17ffe9bc32:op8:describee'
=>
{"id" => "27317a7f-9686-4740-9520-e9c17ffe9bc3",
 "op" => "describe"}

"op" => "describe" という形で、メッセージが来るので、
以下のように、formatnamespaces を返す必要があります。

2. describe: Babashka <= Ruby

{
  "format" => "json",
  "namespaces" => [
     {
       "name" => "pod.ruby.test",
       "vars" => [{"name" => "execute!"}]
     }
  ]
}.bencode
=> "d6:format4:json10:namespacesld4:name13:pod.ruby.test4:varsld4:name8:execute!eeeee"
標準出力
d6:format4:json10:namespacesld4:name13:pod.ruby.test4:varsld4:name8:execute!eeeee

3. invoke: Babashka => Ruby

(ruby/execute! "1 + 2")

を実行すると、invoke メッセージが発行されます。

標準入力
d4:args9:["1 + 2"]2:id36:7903caae-e9a1-4041-9834-37fdfffd85572:op6:invoke3:var22:pod.ruby.test/execute!e
BEncode.load 'd4:args9:["1 + 2"]2:id36:7903caae-e9a1-4041-9834-37fdfffd85572:op6:invoke3:var22:pod.ruby.test/execute!e'
=> 
{"args" => "[\"1 + 2\"]",
 "id" => "7903caae-e9a1-4041-9834-37fdfffd8557",
 "op" => "invoke",
 "var" => "pod.ruby.test/execute!"}

"op" => "invoke" という形で、メッセージが来るので、
以下のように、id, status, value を返す必要があります。
value は、JSON にする必要があります。

4. invoke: Babashka <= Ruby

{
  "id" => "7903caae-e9a1-4041-9834-37fdfffd8557",
  "status" => ["done"],
  "value" => "3"
}.bencode
=> "d2:id36:7903caae-e9a1-4041-9834-37fdfffd85576:statusl4:donee5:value1:3e"
標準出力
d2:id36:7903caae-e9a1-4041-9834-37fdfffd85576:statusl4:donee5:value1:3e

以上がおおまかな流れです。

Ruby

上記を踏まえて、以下のようなスクリプトを書きました。

pod_test.rb
#!/usr/bin/env ruby
require 'bundler/setup'
require 'bencode'
require 'json'

NAMESPACE = 'pod.ruby.test'
VARS = %w(execute!)

# CAUTION: flush しなくてもいいように sync = true にした。
$stdout.sync = true

def perform(var, args)
  case var
  when 'execute!'
    instance_eval(args[0])
  end
rescue => e
  { err: e }
end


def handle_message(msg)
  case msg["op"]
  when 'describe'
    {
      format: 'json',
      namespaces: [
        {
          name: NAMESPACE,
          vars: VARS.map{|v| { name: v } },
        }
      ]
    }
  when 'invoke'
    # CAUTION: var には、NAMESPACE が入っているので除去する
    var = msg['var'].delete_prefix("#{NAMESPACE}/")
    args = JSON.parse(msg['args'])

    # CAUTION: value は、JSON で渡す。
    {
      id:     msg['id'],
      value:  perform(var, args).to_json,
      status: ['done'],
    }
  else
    { err: "Unknown op" }
  end
end


stream = BEncode::Parser.new(STDIN)
while !stream.eos?
  begin
    msg = stream.parse!
    return if msg.nil?
    result = handle_message(msg).bencode
    print result # CAUTION: puts ではなく print を使う。
  rescue => e
    STDERR.puts({ err: e.message }.bencode)
    exit 1
  end
end

上記を保存して、chmod +x で実行可能にすると、

(pods/load-pod "./pod_test.rb")

で呼び出せるようになります。

やりとりは単純なんですが、あまり情報が無かったので、ログを出力しながら四苦八苦してました。
誰かの参考になれば幸いです。

追記(2025/10/3)

Ruby と同じようなコードで、Julia を実行するサンプルも作りました。
https://github.com/tkmfujise/babashka-pod-julia-test/

以上です。

Discussion