投稿時刻: 2020-08-30 19:58:00
タグ: RustLuaPythonJavaScript

Rust に様々なスクリプト言語を組み込む

はじめに

Rust 製のプログラムに各種スクリプト言語 (Lua, Python, JavaScript) を組み込みたい場合にどう書けばよいのかについて調べてみた。今回はライブラリとして rlua, pyo3, rusty_v8, quick-js を試す。

ただしぶっちゃけるとこの記事はほぼ自分用の備忘録であり(毎度この言い訳してんな)、基本的には rlua のコードリーディングがメインとなっている。また「これらのライブラリを使うための説明」というよりは「これらのライブラリの実装を知りたい人向けの説明」になっているかもしれない。というかそもそも説明と呼べるレベルなのか怪しい。あらかじめご承知おきください。

ソースコード

今回使ったソースコードは以下のリポジトリに置いてある。

型注釈はかなり多めに書いているので実際に書く際には省略できる部分もある。

ライブラリの紹介

rlua

rlua は Rust に Lua を組み込むためのライブラリである。Lua は C 言語で書かれた仕様もランタイムも小さめの言語であり、他の言語に組み込むのに非常に向いている。メタテーブルという機構を使うとプロトタイプベースのオブジェクト指向っぽい書き方ができたり演算子をオーバーロードできたり結構色んなことができる。見かけは Ruby (というより Pascal)っぽいが実際に扱った感覚としては JavaScript に近い。

rlua は High level bindings を標榜しており、Lua C API の薄いラッパーであることよりも Rust API として安全である(unsafe な API を使った場合を除き Undefined Behavior にならない)ことを重視している。

pyo3

pyo3 は Rust に Python を組み込むためのライブラリである。Python 言語自体についての説明はもういいよね……。なお標準実装である CPython だけでなく PyPy にも対応している。

pyo3 にはプロシージャルマクロとそれを使うための属性が用意されており、これによって Rust の関数や構造体を Python のモジュール・クラス・関数として簡単にエクスポートできるようになっている。

rusty_v8

rusty_v8 は Rust に V8 を組み込むためのライブラリである。V8 は C++ で書かれた非常に高速な JavaScript 処理系であり Chromium 系ブラウザや Node.js でも利用されている。

rusty_v8 は deno (Node.js の作者を中心に実装されている新たな JavaScript / TypeScript ランタイム)の GitHub Organization において独立した crate として開発されている。V8 の C++ API がほぼ忠実に Rust API として移植されている印象。むしろドキュメントや例があまり多くは書かれていないので V8 の C++ API については熟知しているのが前提っぽい。

quick-js

quick-js は Rust に QuickJS を組み込むためのライブラリである。QuickJS は小さいランタイムながら ES2020 仕様にほぼ対応した C 言語製の JavaScript 処理系である。詳細については以下の記事を参照(手前味噌)。

quick-js は現状公開 API はあまり多くはないが言語を組み込む上で最低限のことはできる。crate 名は quickjs ではないことに注意(ハイフンが必要)。

Rust 側から各種スクリプト側の関数を呼ぶ

Rust から各種スクリプトで定義した関数を呼ぶ例。 Lua (programming language) - Wikipedia (en) の C API に書かれている例と同じことを Rust でやってみる。

rlua

fn lua_from_rust() -> rlua::Result<()> {
    use rlua::{Context, Function, Lua, Table};

    let lua = Lua::new();
    let result = lua.context(|ctx: Context| {
        ctx.load("function foo(x, y) return x + y end").exec()?;
        let globals: Table = ctx.globals();
        let foo: Function = globals.get("foo")?;
        let result: i32 = foo.call((5, 3))?;
        Ok(result)
    })?;
    println!("Result: {}", result);
    assert_eq!(8, result);
    Ok(())
}

pyo3

fn py_from_rust() -> pyo3::PyResult<()> {
    use pyo3::{
        types::{PyAny, PyDict, PyModule},
        GILGuard, Python,
    };

    let gil: GILGuard = Python::acquire_gil();
    let py: Python = gil.python();
    let globals: &PyDict = PyModule::import(py, "__main__")?.dict();
    py.run(
        r#"
def foo(x, y):
    return x + y
"#,
        Some(globals),
        None,
    )?;
    let foo: &PyAny = globals.get_item("foo").unwrap();
    let result: &PyAny = foo.call1((5, 3))?;
    let result: i32 = result.extract()?;
    println!("Result: {}", result);
    assert_eq!(8, result);
    Ok(())
}
  • 詳しくは以下のページを見ればよい

  • Python::acquire_gil()GILGuard (C API でいう PyGILState_STATE)を取得する
  • 実際には gil.python() で取得できる Python (struct) を通じて Python 側とやりとりする

    • Python は親 (GILGuard) の参照と同じライフタイムを持っており、Copy, Clone を derive している
  • conversion.rsFromPyObject / IntoPy (trait) が定義されている

  • &PyAny (&Py*) と PyObject (Py<*>) の違い等については GIL, mutability and object types - PyO3 user guide を見ればよい

    • &Py* のライフタイムは GIL 由来のそれに規定されているが Py<*> はそうではない
    • PyAny の定義は any.rs にある

rusty_v8

pub fn prepare() {
    use rusty_v8::{new_default_platform, Platform, UniqueRef, V8};
    let platform: UniqueRef<Platform> = new_default_platform().unwrap();
    V8::initialize_platform(platform);
    V8::initialize();
}
  • まず Platform を初期化しておく必要がある

    • 2 回以上初期化すると落ちることに注意
fn js_from_rust() -> Option<()> {
    use rusty_v8::{
        self as v8, undefined, Context, ContextScope, Function, HandleScope, Integer, Isolate, Local, Object,
        Primitive, Script, Value,
    };
    use std::convert::TryInto;

    let isolate = &mut Isolate::new(Default::default());
    let scope = &mut HandleScope::new(isolate);
    let context = Context::new(scope);
    let scope = &mut ContextScope::new(scope, context);

    let code = v8::String::new(scope, "function foo(x, y) { return x + y; }")?;
    let script: Local<Script> = Script::compile(scope, code, None)?;
    script.run(scope)?;

    let global: Local<Object> = context.global(scope);
    let foo_key = v8::String::new(scope, "foo")?;
    let foo: Local<Value> = global.get(scope, foo_key.into())?;
    let foo: Local<Object> = foo.to_object(scope)?;
    let foo: Local<Function> = foo.try_into().ok()?;
    let undefined: Local<Primitive> = undefined(scope);
    let x = Integer::new(scope, 5);
    let y = Integer::new(scope, 3);
    let result = foo.call(scope, undefined.into(), &[x.into(), y.into()])?;
    let result = result.to_int32(scope)?.value() as i32;
    println!("Result: {}", result);
    assert_eq!(8, result);
    Some(())
}

quick-js

fn js_from_rust() -> Result<(), Box<dyn Error>> {
    use quick_js::{Context, JsValue};
    use std::convert::TryInto;

    let ctx: Context = Context::new()?;
    ctx.eval("function foo(x, y) { return x + y; }")?;
    let result: JsValue = ctx.call_function("foo", vec![5, 3])?;
    let result: i32 = result.try_into()?;
    println!("Result: {}", result);
    assert_eq!(8, result);
    Ok(())
}
  • quick-js については examples/eval.rs がほぼ全てである

  • Context::new()Context (C API でいう JSRuntime *JSContext *)ができるのでそれを介してよしなにやっていく

各種スクリプト側から Rust 側の関数を呼ぶ

各種スクリプトから Rust で定義した(純粋な)関数を呼ぶ例。

rlua

fn rust_from_lua() -> rlua::Result<()> {
    use rlua::{Context, Function, Lua, Table};

    let lua = Lua::new();
    let result = lua.context(|ctx: Context| {
        let foo: Function = ctx.create_function(|_ctx: Context, (x, y): (i32, i32)| Ok(x + y))?;
        let globals: Table = ctx.globals();
        globals.set("foo", foo)?;
        let result: i32 = ctx.load("foo(5, 3)").eval()?;
        Ok(result)
    })?;
    println!("Result: {}", result);
    assert_eq!(8, result);
    Ok(())
}
  • create_function'static + Send + Fn(Context<'lua>, A) -> Result<R> を Lua の関数 (Function) にできる

pyo3

fn rust_from_py() -> pyo3::PyResult<()> {
    use pyo3::{
        prelude::pyfunction,
        types::{PyAny, PyDict, PyModule},
        wrap_pyfunction, GILGuard, Python,
    };

    let gil: GILGuard = Python::acquire_gil();
    let py: Python = gil.python();
    let globals: &PyDict = PyModule::import(py, "__main__")?.dict();

    #[pyfunction]
    fn foo(x: i32, y: i32) -> i32 {
        x + y
    }

    globals.set_item("foo", wrap_pyfunction!(foo)(py))?;
    let result: &PyAny = py.eval("foo(5, 3)", Some(globals), None)?;
    let result: i32 = result.extract()?;
    println!("Result: {}", result);
    assert_eq!(8, result);
    Ok(())
}
  • pyfunction 属性を付けると proc_macro の力で Rust の関数が Python に渡せるようになる

    • 渡す際には wrap_pyfunction! マクロを使う
  • グローバルに渡さずモジュール(PyModulepymodule 属性)を使う方が多分一般的(詳しくは公式ページを参照)

rusty_v8

fn rust_from_js() -> Option<()> {
    use rusty_v8::{
        self as v8, Context, ContextScope, Function, FunctionCallbackArguments, HandleScope, Integer, Isolate, Local,
        Object, ReturnValue, Script, Value,
    };

    fn foo(scope: &mut HandleScope, args: FunctionCallbackArguments, mut rv: ReturnValue) {
        let x = args.get(0).to_int32(scope).unwrap().value() as i32;
        let y = args.get(1).to_int32(scope).unwrap().value() as i32;
        let result = Integer::new(scope, x + y);
        rv.set(result.into());
    }

    let isolate = &mut Isolate::new(Default::default());
    let scope = &mut HandleScope::new(isolate);
    let context = Context::new(scope);
    let scope = &mut ContextScope::new(scope, context);

    let foo = Function::new(scope, foo)?;

    let global: Local<Object> = context.global(scope);
    let key = v8::String::new(scope, "foo")?;
    global.create_data_property(scope, key.into(), foo.into())?;

    let code = v8::String::new(scope, "foo(5, 3)")?;
    let script: Local<Script> = Script::compile(scope, code, None)?;
    let result: Local<Value> = script.run(scope)?;
    let result = result.to_int32(scope)?.value() as i32;
    println!("Result: {}", result);
    assert_eq!(8, result);
    Some(())
}
  • fn (scope: &mut HandleScope, args: FunctionCallbackArguments, mut rv: ReturnValue) というシグネチャの関数を JavaScript の関数 (Function) にできる

    • thisargs.this() で取得できる

quick-js

fn rust_from_js() -> Result<(), Box<dyn Error>> {
    use quick_js::Context;

    let ctx: Context = Context::new()?;
    ctx.add_callback("foo", |x: i32, y: i32| x + y)?;
    let result: i32 = ctx.eval_as("foo(5, 3)")?;
    println!("Result: {}", result);
    assert_eq!(8, result);
    Ok(())
}
  • add_callbackimpl Callback<F> + 'static を QuickJS の関数にできる

    • Callback は trait
    • F: Fn(Arguments) -> R + Sized + RefUnwindSafe などが Callback を impl する (callback.rs)

各種スクリプト側に Rust 側のデータを管理させる

スクリプト処理系の GC に Rust 側で生成したデータを明け渡したい場合など。 例では疑似乱数発生器をスクリプト側に渡してスクリプト側で乱数を得るシチュエーションを想定している。

都合により rlua と pyo3 の場合のみ。理由については後述。

rlua

fn rust_prng_from_lua() -> rlua::Result<()> {
    use rand::{Rng, SeedableRng};
    use rand_xorshift::XorShiftRng;
    use rlua::{Context, Function, Lua, Table, UserData, UserDataMethods};
    use std::cell::RefCell;

    struct PRNG {
        rng: Box<RefCell<XorShiftRng>>,
    }

    impl PRNG {
        fn new() -> Self {
            PRNG {
                rng: Box::new(RefCell::new(XorShiftRng::from_seed([0; 16]))),
            }
        }

        fn gen(&self) -> i32 {
            self.rng.as_ref().borrow_mut().gen::<i32>()
        }
    }

    impl UserData for PRNG {
        fn add_methods<'lua, M: UserDataMethods<'lua, Self>>(methods: &mut M) {
            methods.add_method("gen", |_, prng, ()| Ok(prng.gen()));
        }
    }

    impl Drop for PRNG {
        fn drop(&mut self) {
            println!("dropped!")
        }
    }

    let lua = Lua::new();
    let result = lua.context(|ctx: Context| {
        let prng: Function = ctx.create_function(|_, ()| Ok(PRNG::new()))?;
        let globals: Table = ctx.globals();
        globals.set("PRNG", prng)?;
        ctx.load("prng = PRNG()").exec()?;
        let result: i32 = ctx.load("prng:gen()").eval()?;
        println!("Result: {}", result);
        assert_eq!(1788228419, result);
        let result: i32 = ctx.load("prng:gen()").eval()?;
        ctx.load("prng = nil").exec()?;
        Ok(result)
    })?;
    lua.gc_collect()?; // dropped!
    println!("Result: {}", result);
    assert_eq!(195908298, result);
    Ok(())
}

pyo3

fn rust_prng_from_py() -> pyo3::PyResult<()> {
    use pyo3::{
        prelude::{pyclass, pymethods},
        types::{PyDict, PyModule, PyType},
        GILGuard, PyObject, Python,
    };
    use rand::{Rng, SeedableRng};
    use rand_xorshift::XorShiftRng;
    use std::cell::RefCell;

    #[pyclass]
    struct PRNG {
        rng: Box<RefCell<XorShiftRng>>,
    }

    #[pymethods]
    impl PRNG {
        #[new]
        fn new() -> Self {
            PRNG {
                rng: Box::new(RefCell::new(XorShiftRng::from_seed([0; 16]))),
            }
        }

        fn gen(&self) -> i32 {
            self.rng.as_ref().borrow_mut().gen::<i32>()
        }
    }

    impl Drop for PRNG {
        fn drop(&mut self) {
            println!("dropped!")
        }
    }

    let gil: GILGuard = Python::acquire_gil();
    let py: Python = gil.python();
    let prng_cls: &PyType = py.get_type::<PRNG>();
    let globals: &PyDict = PyModule::import(py, "__main__")?.dict();
    globals.set_item("PRNG", prng_cls)?;

    py.run("prng = PRNG()", Some(globals), None)?;
    let result: i32 = py.eval("prng.gen()", Some(globals), None)?.extract()?;
    println!("Result: {}", result);
    assert_eq!(1788228419, result);
    let result: i32 = py.eval("prng.gen()", Some(globals), None)?.extract()?;
    py.run("prng = None", Some(globals), None)?; // dropped!
    println!("Result: {}", result);
    assert_eq!(195908298, result);
    Ok(())
}
  • pyclass, pymethods 属性を付けると proc_macro の力で Rust のデータを Python に渡せるようになる
  • グローバル変数を使うのではなくモジュール(PyModulepymodule 属性)を使う方が多分一般的(詳しくは公式以下略)

rusty_v8

quick-js

  • 現状ではできない?
  • JSValue に Opaque として *mut c_void を仕込む必要があるが、そのために Class ID の払い出しが必要
  • 払い出された Class ID を自由関数から取得するためになんやかんやする必要がある

    • Runtime の Opaque として TypeId - Class ID のマップを持たせるとか(rlua はこのような方針
    • 1 Runtime - 1 Thread と決め打ちするなら Thread Local Storage を使うとか

おわりに

C++ や Rust のようなデストラクタ (Drop) の走るタイミングが確定的な言語だと RAII イディオムが使いやすいが、こういった他言語の組み込みにおいては特に解放が必要なリソースを扱いまくるのでそういうコードがスッと書けてよい。Rust の場合ライフタイムのおかげで「ある Context や Scope が生きている場合にのみ使えるリソース」みたいなやつの生存期間を静的に解析できてなおよい(ライブラリ側の設計にもよるが)。

元々 rlua の API と似た感じで QuickJS を扱いたいというモチベーションがあって自分でライブラリを書いていた。しかしこのようなバインディングをうまくやるライブラリを書く側になってみると FFI すなわち unsafe まみれの世界と付きあわなければならないし、C にはライフタイム(に関する言語機能)もクロージャもない 1 し、インタプリタを別スレッドで動かしたい場合も考えられるし……。というわけで相当な Rust 力、というより高いレベルの低レベルプログラミング力(?)が求められて苦戦しまくっている。そんなこんなで設計の参考になりそうなソースコードとして rlua 以外のライブラリについても調べた。この記事はその副産物である。

Rust で既存ライブラリを使って CLI アプリケーションとか Web (API) サーバとかはまあそれなりに書けるようになってきたが、ライブラリを作る側となるとかなり壁が高いということを実感している。引き続き地道にやっていきます……。


  1. Blocks なんてものもあったね……。

Rust に様々なスクリプト言語を組み込む
投稿時刻: 2020-08-30 19:58:00
タグ: RustLuaPythonJavaScript