目的
Rustでプログラム実行中に後から渡されるプログラムを実行したいです。プラグインみたいな感じです。
Rustだけで実現するのは難しいので(wasmあたりを使うとできるという噂もありますが)、他のプログラム言語を利用します。
ここではRubyを使います。
Rubyを使うものは当然Rubyがインストールされている必要があり、シングルバイナリで実行できないのが難点です。
そこでmrubyを使うことになるんですが、以前からあったものは古いmrubyしか対応していなくて微妙でした。
今回利用するminutusというライブラリは最新のmrubyをサポートしていて、mrubyの知識があまりなくてもサクッと利用できるのがよかったです。
開発の意図などは作者のブログを参考にしてください。Minutus という mruby の Rust バインディングを作った
コードの目的
Rubyのコードでは以下の2点を確認しました。
- 引数を渡せる
- HTTP通信ができる
コード
以下を使うことで任意のmrubyのモジュールが組み込めます。今回は通信が確認できればいいんですが、色々入ってますが、気にしないでください。
MRuby::Build.new do |conf|
toolchain
conf.gembox 'default'
conf.gem github: 'mattn/mruby-json'
conf.gem :git => 'https://github.com/iij/mruby-pack.git'
conf.gem :git => 'https://github.com/iij/mruby-digest.git'
conf.gem :git => 'https://github.com/mattn/mruby-json.git'
conf.gem :git => 'https://github.com/mattn/mruby-uv.git'
conf.gem :git => 'https://github.com/mattn/mruby-http.git'
conf.gem :git => 'https://github.com/matsumoto-r/mruby-simplehttp.git'
conf.gem :git => 'https://github.com/matsumoto-r/mruby-httprequest.git'
conf.gem :git => 'https://github.com/matsumoto-r/mruby-oauth.git'
conf.gem :git => 'https://github.com/matsumoto-r/mruby-sleep.git'
conf.gem :git => 'https://github.com/matsumoto-r/mruby-zabbix.git'
conf.gem :git => 'https://github.com/matsumoto-r/mruby-growthforecast.git'
conf.gem :git => 'https://github.com/y-ken/fluent-logger-mruby'
end
Rubyのコードは以下のようになります。
メソッド形式になっていてRustから引数を渡すことになります。
コメントアウトの除くときちんと標準出力に結果が表示されました。
def execute(url)
h = HttpRequest.new
b = h.get(url).body
# p b
j = JSON.parse(b)
j.to_json
end
[package]
name = "mruby"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
minutus = { version = "0.4.0", features = ["mruby_3_1_0", "link_mruby"] }
serde_json = "1"
[build-dependencies]
minutus = { version = "0.4.0", features = ["mruby_3_1_0", "link_mruby"] }
fn main() {
println!("cargo:rerun-if-changed=bulid.rs");
println!("cargo:rerun-if-changed=build_config.rb");
minutus::MRubyManager::new()
.build_config(&std::env::current_dir().unwrap().join("build_config.rb"))
.mruby_version("3.1.0")
.run();
}
use std::time::SystemTime;
minutus::define_funcall!{
fn execute(self, url: String) -> String;
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let evaluator = minutus::Evaluator::build();
let script = std::fs::read_to_string("some_script.rb")?;
evaluator.evaluate(&script)?;
let main = evaluator.evaluate("self")?;
for i in 0..5 {
let start = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_millis();
let retval = main.execute(format!("https://httpbin.org/get?i={}", i))?;
let end = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_millis();
let value = serde_json::from_str::<serde_json::Value>(&retval)?;
println!("{}", serde_json::to_string(&value)?);
println!("{}", end - start);
}
Ok(())
}
結果
{"args":{"i":"0"},"headers":{"Accept":"*/*","Host":"httpbin.org","X-Amzn-Trace-Id":"Root=1-634c8dac-79737ca70d71764e2129a0f8"},"origin":"150.91.1.193","url":"https://httpbin.org/get?i=0"}
6140
{"args":{"i":"1"},"headers":{"Accept":"*/*","Host":"httpbin.org","X-Amzn-Trace-Id":"Root=1-634c8dad-2b6d385521217ec222b742df"},"origin":"150.91.1.193","url":"https://httpbin.org/get?i=1"}
976
{"args":{"i":"2"},"headers":{"Accept":"*/*","Host":"httpbin.org","X-Amzn-Trace-Id":"Root=1-634c8dae-764a40ac0159daff07e84dc7"},"origin":"150.91.1.193","url":"https://httpbin.org/get?i=2"}
968
{"args":{"i":"3"},"headers":{"Accept":"*/*","Host":"httpbin.org","X-Amzn-Trace-Id":"Root=1-634c8daf-6689f5be0cbb4348632cc014"},"origin":"150.91.1.193","url":"https://httpbin.org/get?i=3"}
969
{"args":{"i":"4"},"headers":{"Accept":"*/*","Host":"httpbin.org","X-Amzn-Trace-Id":"Root=1-634c8db0-43cf22690ec6f43710a87ef5"},"origin":"150.91.1.193","url":"https://httpbin.org/get?i=4"}
1051
どうももっさりしています。何が原因かみてみます。
JSONを疑る
本来ならJSONをRubyのオブジェクトにして加工したいこともあるんですが、今は外してみました。
def execute(url)
h = HttpRequest.new
h.get(url).body
end
5980
1004
1001
951
956
変わらないです。
通信を疑る
def execute(url)
# h = HttpRequest.new
# h.get(url).body
# p b
# j = JSON.parse(b)
# j.to_json
{ url: url }.to_json
end
0
0
0
0
0
あ、これですね。
通信先を疑る
time curl "https://httpbin.org/get?i=0"
{
"args": {
"i": "0"
},
"headers": {
"Accept": "*/*",
"Host": "httpbin.org",
"User-Agent": "curl/7.68.0",
"X-Amzn-Trace-Id": "Root=1-634c964f-36852b3112cfa5923e8ac2f2"
},
"origin": "150.91.1.193",
"url": "https://httpbin.org/get?i=0"
}
real 0m5.759s
user 0m0.013s
sys 0m0.007s
犯人はこれでした。早いときもあれば遅いときもあります。プログラムで最初だけ思いのかと思ったらそうでは無くて、接続先の問題のようでした。
まとめ
mruby自体のパフォーマンスは問題なさそうです。
組み込みも簡単ですが、コンパイル時に利用するライブラリが決まっていないといけないので、後からmrubyでライブラリが必要になった時は再ビルドする必要があります。