投稿時刻: 2020-08-30 19:58:00
タグ: RustLuaPythonJavaScript
Rust に様々なスクリプト言語を組み込む
はじめに
Rust 製のプログラムに各種スクリプト言語 (Lua, Python, JavaScript) を組み込みたい場合にどう書けばよいのかについて調べてみた。今回はライブラリとして rlua, pyo3, rusty_v8, quick-js を試す。
ただしぶっちゃけるとこの記事はほぼ自分用の備忘録であり(毎度この言い訳してんな)、基本的には rlua のコードリーディングがメインとなっている。また「これらのライブラリを使うための説明」というよりは「これらのライブラリの実装を知りたい人向けの説明」になっているかもしれない。というかそもそも説明と呼べるレベルなのか怪しい。あらかじめご承知おきください。
ソースコード
今回使ったソースコードは以下のリポジトリに置いてある。
型注釈はかなり多めに書いているので実際に書く際には省略できる部分もある。
ライブラリの紹介
rlua
-
rlua - crates.io: Rust Package Registry
- amethyst/rlua: High level Lua bindings to Rust
rlua = "0.17.0"
rlua は Rust に Lua を組み込むためのライブラリである。Lua は C 言語で書かれた仕様もランタイムも小さめの言語であり、他の言語に組み込むのに非常に向いている。メタテーブルという機構を使うとプロトタイプベースのオブジェクト指向っぽい書き方ができたり演算子をオーバーロードできたり結構色んなことができる。見かけは Ruby (というより Pascal)っぽいが実際に扱った感覚としては JavaScript に近い。
rlua は High level bindings を標榜しており、Lua C API の薄いラッパーであることよりも Rust API として安全である(unsafe
な API を使った場合を除き Undefined Behavior にならない)ことを重視している。
pyo3
-
pyo3 - crates.io: Rust Package Registry
- PyO3/pyo3: Rust bindings for the Python interpreter
pyo3 = "0.11.1"
pyo3 は Rust に Python を組み込むためのライブラリである。Python 言語自体についての説明はもういいよね……。なお標準実装である CPython だけでなく PyPy にも対応している。
pyo3 にはプロシージャルマクロとそれを使うための属性が用意されており、これによって Rust の関数や構造体を Python のモジュール・クラス・関数として簡単にエクスポートできるようになっている。
rusty_v8
-
rusty_v8 - crates.io: Rust Package Registry
- denoland/rusty_v8: V8 javascript bindings for Rust
rusty_v8 = "0.9.1"
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 - crates.io: Rust Package Registry
- theduke/quickjs-rs: Rust wrapper for the quickjs Javascript engine.
quick-js = "0.3.4"
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(())
}
-
詳しくは以下のページを見ればよい
-
Lua::new()
で Lua インタプリタ(C API でいうlua_State *
)ができる -
実際には
Context
を通じて Lua 側とやりとりする- これは実質
lua_State *
だが親 (Lua
) の参照と同じライフタイムを持っており、Copy
,Clone
を derive している
- これは実質
-
value.rs
にValue
(enum) やToLua
/FromLua
/ToLuaMulti
/FromLuaMulti
(trait) が定義されているconversion.rs
やmulti.rs
に変換方法が書かれているTable
の実装Function
の実装-
Lua の管理するオブジェクトへの参照を表す
LuaRef
はtypes.rs
で定義されている- これは
Context
を内包している
- これは
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.rs
にFromPyObject
/IntoPy
(trait) が定義されている- 型変換については Type Conversions - PyO3 user guide を見ればよい
-
&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(())
}
-
以下のコードから使い方を類推している
-
まず V8 の仕組み自体をよく知っていないといけなさそう
- 基本的に C++ API に忠実な定義だが一部の
isolate
を取る関数がscope
を取る関数になっていたりする(ライフタイム等の都合?) -
V8 で使われる値やオブジェクトの型は独自の階層構造を持っており、それが rusty_v8 の API にも反映されている
- 関数引数の型に合わせるために
Local<T>
をLocal<Value>
やLocal<Name>
等にアップキャストする必要があり、そのためにinto()
している - ダウンキャストには
try_into()
が使える Deref
,From
,TryFrom
をimpl
するためのマクロがdata.rs
に定義されている
- 関数引数の型に合わせるために
Local<T>
が帰ってくるほとんどの API 呼び出しの戻り値はResult
でなくOption
であることに注意-
値の寿命を
HandleScope
より長くする必要がある場合はEscapableHandleScope
を使いescape
を行う必要がある(と V8 公式の解説に書かれている)
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!
マクロを使う
- 渡す際には
- グローバルに渡さずモジュール(
PyModule
とpymodule
属性)を使う方が多分一般的(詳しくは公式ページを参照)
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
) にできるthis
はargs.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_callback
でimpl Callback<F> + 'static
を QuickJS の関数にできるCallback
は traitF: 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(())
}
-
UserData
trait を実装してadd_methods
で Lua 側に公開するメソッドを足していけばよい- こうすることで なんやかんやあって trait の
add_methods
が呼ばれた後lua_newuserdata
が呼ばれる などする
- こうすることで なんやかんやあって trait の
-
drop
内でprintln!
するのは一般にはよろしくないのでやめましょう- 実行時に cannot access stdout during shutdown とか言われて panic する場合がある
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 に渡せるようになる- グローバル変数を使うのではなくモジュール(
PyModule
とpymodule
属性)を使う方が多分一般的(詳しくは公式以下略)
rusty_v8
- 現状ではできない?
- Object の Internal Field に External (実質
*mut c_void
) を仕込めないといけないのではないか(多分) -
Pull Request は既にある
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) サーバとかはまあそれなりに書けるようになってきたが、ライブラリを作る側となるとかなり壁が高いということを実感している。引き続き地道にやっていきます……。