rlua を使って Rust に Lua言語 を組み込む

# はじめに

本記事は Rust にスクリプト言語を組み込む方法について調査したメモです。

本記事では弊社での実務に使用することを念頭に、

- 簡単に使えること
- Rust ←→ スクリプト言語間のデータのやり取りが簡単に行えること
- パフォーマンスが悪くないこと
- 安全であること
- スクリプト言語のライブラリが利用できること

の観点で調査を行います。

なお、弊社では Rust からスクリプト言語を実行するケースしかないため、スクリプト言語のモジュールを Rust で実装する方法については調査を行っていません。


# 調査対象のクレート

今回は rlua について調べます。
rlua は Rust に Lua言語 を組み込むためのライブラリです。

https://crates.io/crates/rlua

**Repository**
[amethyst/rlua: High level bindings between Rust and Lua](https://github.com/amethyst/rlua)

**調査時のバージョン**
`rlua = "0.19.2"`

# 前提条件

本記事では他の調査との環境の差異を無くすために、検証はすべて以下のコマンドで起動した docker のコンテナ内で行います。

```sh
docker run --rm -it \
  -v $PWD:/home \
  -w /home \
  rust:1.62.0 \
  /bin/bash
```


# 調査内容
## 導入方法

rlua はデフォルトで Lua言語 の処理系を内包しているため rlua を追加すればすぐに利用可能となります。

```sh
$ 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 で利用することができます。

```rust
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 側でテーブル型として利用することができました。

```rust
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](https://luarocks.org/) というパッケージマネージャを利用して管理するようですが、 rlua 組み込みの Lua ではサクッと利用できるところまでは持っていけませんでした。
（モジュールのインクルードパスの問題な気がするので、もう少しで解決できそうですが。。。）

OS にインストールした Lua であれば利用可能なのですが、手軽に利用したい思いから少し外れるので一旦調査はここまでとします。

## パフォーマンス

先ほどのフィボナッチ数列を求めるプログラムの実行速度を計測します。

<details><summary>計測に利用したコード</summary>

```rust
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)
    );
}
```

</details>

```sh
$ 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 に対するメンバーの習熟度で、この点では他のスクリプト言語に分があると感じています。