はじめに
本記事は Rust にスクリプト言語を組み込む方法について調査したメモです。
本記事では弊社での実務に使用することを念頭に、
- 簡単に使えること
- Rust ←→ スクリプト言語間のデータのやり取りが簡単に行えること
- パフォーマンスが悪くないこと
- 安全であること
- スクリプト言語のライブラリが利用できること
の観点で調査を行います。
なお、弊社では Rust からスクリプト言語を実行するケースしかないため、スクリプト言語のモジュールを Rust で実装する方法については調査を行っていません。
調査対象のクレート
今回は rlua について調べます。
rlua は Rust に Lua言語 を組み込むためのライブラリです。
Repository
amethyst/rlua: High level bindings between Rust and Lua
調査時のバージョン
rlua = "0.19.2"
前提条件
本記事では他の調査との環境の差異を無くすために、検証はすべて以下のコマンドで起動した docker のコンテナ内で行います。
docker run --rm -it \
-v $PWD:/home \
-w /home \
rust:1.62.0 \
/bin/bash
調査内容
導入方法
rlua はデフォルトで Lua言語 の処理系を内包しているため rlua を追加すればすぐに利用可能となります。
$ cargo add rlua
Updating crates.io index
Adding rlua v0.19.2 to dependencies.
Features:
+ builtin-lua54
+ rlua-lua54-sys
- builtin-lua51
- builtin-lua53
- lua-compat-mathlib
- rlua-lua51-sys
- rlua-lua53-sys
- system-lua51
- system-lua53
- system-lua54
もし、 OS にインストールされた Lua が使用したい場合には system-lua54
などの feature フラグを指定します。
Rust からスクリプト言語を実行する
簡単なスクリプトの実行
まずは簡単なスクリプトの例です。
Rust と Lua のやり取りは lua.context
というブロック内で実行するようになっており、 globals.set("n", n)?
のように書くことで Rust のデータを Lua で利用することができます。
fn fibonacci_lua(n: i64) -> Result<i64> {
let lua = Lua::new();
lua.context(|lua_ctx| {
let globals = lua_ctx.globals();
globals.set("n", n)?;
lua_ctx
.load(
r#"
a, b = 1, 0
for i = 1, n do
a, b = b, a + b
end
return b
"#,
)
.eval()
})
}
serde_json::Value 型のデータを渡す
Lua では JSON のような型はテーブル型というもので扱うようです。
先ほどのフィボナッチ数列の例のように globals.set
でデータを渡すためには ToLua
を実装していれば良いようなので、以下のようなコードを書くことで Rust の serde_json::Value の値を Lua 側でテーブル型として利用することができました。
use rlua::{Lua, Result, ToLua};
use serde_json::json;
fn json_to_table<'lua>(
lua_ctx: &rlua::Context<'lua>,
value: &serde_json::Value,
) -> Result<rlua::Value<'lua>> {
Ok(match value {
serde_json::Value::Null => rlua::Value::Nil,
serde_json::Value::Bool(v) => rlua::Value::Boolean(*v),
serde_json::Value::Number(v) => rlua::Value::Number(v.as_f64().unwrap()),
serde_json::Value::String(v) => rlua::Value::String(lua_ctx.create_string(v)?),
serde_json::Value::Array(v) => rlua::Value::Table(
lua_ctx.create_sequence_from::<rlua::Value<'lua>, Vec<rlua::Value<'lua>>>(
v.iter()
.map(|x| json_to_table(lua_ctx, x))
.collect::<Result<Vec<_>>>()?,
)?,
),
serde_json::Value::Object(v) => rlua::Value::Table(
lua_ctx
.create_table_from::<String, rlua::Value<'lua>, Vec<(String, rlua::Value<'lua>)>>(
v.iter()
.map(|(k, v)| Ok((k.to_string(), json_to_table(lua_ctx, v)?)))
.collect::<Result<Vec<_>>>()?,
)?,
),
})
}
struct MyJsonStruct(serde_json::Value);
impl<'lua> ToLua<'lua> for MyJsonStruct {
fn to_lua(self, lua: rlua::Context<'lua>) -> Result<rlua::Value<'lua>> {
json_to_table(&lua, &self.0)
}
}
fn use_json_in_lua(data: MyJsonStruct) -> Result<bool> {
let lua = Lua::new();
lua.context(|lua_ctx| {
let globals = lua_ctx.globals();
globals.set("data", data)?;
lua_ctx
.load(
r#"
print(data)
for k, v in pairs(data) do
print(k, v)
end
return data["age"] >= 20
"#,
)
.eval()
})
}
fn main() {
let john = json!({
"name": "John Doe",
"age": 43,
"phones": [
"+44 1234567",
"+44 2345678"
]
});
dbg!(use_json_in_lua(MyJsonStruct(john)));
}
スクリプト言語のライブラリを利用する
Lua言語のライブラリは LuaRocks というパッケージマネージャを利用して管理するようですが、 rlua 組み込みの Lua ではサクッと利用できるところまでは持っていけませんでした。
(モジュールのインクルードパスの問題な気がするので、もう少しで解決できそうですが。。。)
OS にインストールした Lua であれば利用可能なのですが、手軽に利用したい思いから少し外れるので一旦調査はここまでとします。
パフォーマンス
先ほどのフィボナッチ数列を求めるプログラムの実行速度を計測します。
計測に利用したコード
use rlua::{Lua, Result};
use std::time::Instant;
fn fibonacci_rs(n: i64) -> i64 {
let (mut a, mut b) = (1, 0);
for _ in 0..n {
(a, b) = (b, a + b);
}
return b;
}
fn fibonacci_lua(n: i64) -> Result<i64> {
let lua = Lua::new();
lua.context(|lua_ctx| {
let globals = lua_ctx.globals();
globals.set("n", n)?;
lua_ctx
.load(
r#"
a, b = 1, 0
for i = 1, n do
a, b = b, a + b
end
return b
"#,
)
.eval()
})
}
fn main() {
let start = Instant::now();
for n in 1..=64 {
let _ = fibonacci_rs(n);
}
println!(
"Rust の実行時間\t:{:?}",
Instant::now().duration_since(start)
);
let start = Instant::now();
for n in 1..=64 {
let _ = fibonacci_lua(n).unwrap();
}
println!(
"Lua の実行時間\t:{:?}",
Instant::now().duration_since(start)
);
}
$ cargo run --release
Compiling rlua-example v0.1.0 (/home/rlua-example)
Finished release [optimized] target(s) in 0.68s
Running `target/release/rlua-example`
Rust の実行時間 :60ns
Lua の実行時間 :5.07335ms
より効率の良い書き方はありそうですが、気にするほど遅くは無さそうです。
安全性
先ほどからのサンプルに登場する通り、lua.context
は rlua::Result
型を返します。
安全なだけではなく、lua.context
内の問題については非常に分かりやすいエラーメッセージが得られるので、デバッグも捗ります。
まとめ
rlua はパフォーマンス・安全性も問題なく、導入も容易で非常に実用的なクレートだと思いました。
懸念点があるとすれば Lua に対するメンバーの習熟度で、この点では他のスクリプト言語に分があると感じています。