Babashka pods で Ruby を実行する簡単なサンプル
きっかけ
最近 Clojure に興味が出て、Babashka を触っているのですが、
Babashka には pods と呼ばれる、標準入出力上を Bencode で他のプログラムとやりとりできる仕組みがあります。
標準入出力上でやりとりするプロトコルに則っていれば、何のプログラムでもよいので、
例えば、Python で SQLite に対して SQL を実行するスクリプトを、Babashka から実行して結果を取得できます。
おもしろそうだったので、Ruby を実行するサンプルを作りました。
できたもの
(require '[babashka.pods :as pods])
(pods/load-pod "./pod_test.rb")
(require '[pod.ruby.test :as ruby])
(ruby/execute! "1 + 2") ;; => 3
のような形で Ruby のプログラムを実行します。
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" という形で、メッセージが来るので、
以下のように、format と namespaces を返す必要があります。
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
上記を踏まえて、以下のようなスクリプトを書きました。
#!/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 を実行するサンプルも作りました。
以上です。
Discussion