rusty_v8 を使って Rust から JavaScript を実行
Node.js の製作者が新たに作り直した Deno という JavaScript/TypeScript 実行環境があります。
Deno の内部では、V8 JavaScript エンジンの呼び出しに rusty_v8 という Rust 用バインディングを使っていたので、今回はこの rusty_v8 を使って Rust コード内で JavaScript コードを実行してみました。
今回のサンプルコードは http://github.com/fits/try_samples/tree/master/blog/20200705/
設定
rusty_v8 を使うための Cargo 用の dependencies 設定は以下のようになります。
Cargo.toml
・・・
[dependencies]
rusty_v8 = "0.6"
JavaScript コード実行
以下の JavaScript コードを実行し結果(1 ~ 5 の合計値)を出力する処理を Rust で実装してみます。
実行する JavaScript コード
const vs = [1, 2, 3, 4, 5] console.log(vs) vs.reduce((acc, v) => acc + v, 0)
基本的に、下記 V8 API による手順と同様の処理を rusty_v8 の API で実装すればよさそうです。
V8 エンジンのインスタンスである Isolate
(独自のヒープを持ち他のインスタンスとは隔離される)、GC で管理するオブジェクトへの参照をまとめて管理する HandleScope
、サンドボックス化された実行コンテキストの Context
(組み込みのオブジェクト・関数を管理)をそれぞれ作成していきます。
そして、JavaScript のコードを Script::compile
でコンパイルして、run
で実行します。
run の戻り値は Option<Local<Value>>
となっているので、ここでは to_string
を使って JavaScript の String(Option<Local<rusty_v8::String>>
)として取得し、rusty_v8::String を to_rust_string_lossy
を使って Rust の String へ変換して出力しています。
src/sample1.rs
use rusty_v8 as v8; fn main() { let platform = v8::new_default_platform().unwrap(); v8::V8::initialize_platform(platform); v8::V8::initialize(); let isolate = &mut v8::Isolate::new(Default::default()); let scope = &mut v8::HandleScope::new(isolate); let context = v8::Context::new(scope); let scope = &mut v8::ContextScope::new(scope, context); // JavaScript コード let src = r#" const vs = [1, 2, 3, 4, 5] console.log(vs) vs.reduce((acc, v) => acc + v, 0) "#; v8::String::new(scope, src) .map(|code| { println!("code: {}", code.to_rust_string_lossy(scope)); code }) .and_then(|code| v8::Script::compile(scope, code, None)) //コンパイル .and_then(|script| script.run(scope)) //実行 .and_then(|value| value.to_string(scope)) // rusty_v8::Value を rusty_v8::String へ .iter() .for_each(|s| println!("result: {}", s.to_rust_string_lossy(scope))); }
実行
複数の実行ファイルに対応した下記 Cargo.toml を使って実行します。
Cargo.toml
[package] ・・・ default-run = "sample1" [dependencies] rusty_v8 = "0.6" [[bin]] name = "sample1" path = "src/sample1.rs" [[bin]] name = "sample2" path = "src/sample2.rs" [[bin]] name = "sample3" path = "src/sample3.rs"
sample1(src/sample1.rs)の実行結果は以下の通りです。
console.log
に関しては何も処理されていませんが、JavaScript の実行結果は取得できています。
sample1 実行結果
> cargo run ・・・ Running `target\debug\sample1.exe` code: const vs = [1, 2, 3, 4, 5] console.log(vs) vs.reduce((acc, v) => acc + v, 0) result: 15
Inspector 機能
次に、console.log
されたログメッセージを Rust から出力するようにしてみます。
V8 で console.log (デバッグコンソールへのログ出力)のようなデバッグ機能を処理するには Inspector という機能を使うようです。
rusty_v8 では、console.log 等の呼び出し時に V8InspectorClientImpl
トレイトの console_api_message
が呼びされるようになっているため、これを実装した構造体のインスタンスから V8Inspector
を作成して context_created
を実行する事で実現できます。
context_created のシグネチャは fn context_created(&mut self, context: Local<Context>, context_group_id: i32, human_readable_name: StringView)
となっており、context_group_id
引数へ指定した値が、console_api_message の context_group_id 引数の値となります。(human_readable_name
の用途に関してはよく分からなかった)
また、console_api_message の level
引数の値はログレベル(V8 API の MessageErrorLevel) のようです。
ちなみに、V8InspectorClientImpl トレイトは base
と base_mut
の実装が必須でした。(console_api_message は空実装されている)
src/sample2.rs
use rusty_v8 as v8; use rusty_v8::inspector::*; struct InspectorClient(V8InspectorClientBase); impl InspectorClient { fn new() -> Self { Self(V8InspectorClientBase::new::<Self>()) } } impl V8InspectorClientImpl for InspectorClient { fn base(&self) -> &V8InspectorClientBase { &self.0 } fn base_mut(&mut self) -> &mut V8InspectorClientBase { &mut self.0 } fn console_api_message(&mut self, _context_group_id: i32, _level: i32, message: &StringView, _url: &StringView, _line_number: u32, _column_number: u32, _stack_trace: &mut V8StackTrace) { // ログメッセージの出力 println!("{}", message); } } fn main() { ・・・ let isolate = &mut v8::Isolate::new(Default::default()); // V8Inspector の作成 let mut client = InspectorClient::new(); let mut inspector = V8Inspector::create(isolate, &mut client); let scope = &mut v8::HandleScope::new(isolate); let context = v8::Context::new(scope); let scope = &mut v8::ContextScope::new(scope, context); // context_created の実行 inspector.context_created(context, 1, StringView::empty()); let src = r#" const vs = [1, 2, 3, 4, 5] console.log(vs) vs.reduce((acc, v) => acc + v, 0) "#; ・・・ }
実行結果は以下の通り。
console.log(vs)
を処理して 1,2,3,4,5
が出力されるようになりました。
sample2 実行結果
> cargo run --bin sample2 ・・・ Running `target\debug\sample2.exe` code: const vs = [1, 2, 3, 4, 5] console.log(vs) vs.reduce((acc, v) => acc + v, 0) 1,2,3,4,5 result: 15
最後に、context_group_id
と level
の値も出力するようにしてみます。
src/sample3.rs
・・・ impl V8InspectorClientImpl for InspectorClient { ・・・ fn console_api_message(&mut self, context_group_id: i32, level: i32, message: &StringView, _url: &StringView, _line_number: u32, _column_number: u32, _stack_trace: &mut V8StackTrace) { println!( "*** context_group_id={}, level={}, message={}", context_group_id, level, message ); } } fn main() { ・・・ inspector.context_created(context, 123, StringView::empty()); let src = r#" console.log('log') console.debug('debug') console.info('info') console.error('error') console.warn('warn') "#; v8::String::new(scope, src) .and_then(|code| v8::Script::compile(scope, code, None)) .and_then(|script| script.run(scope)) .and_then(|value| value.to_string(scope)) .iter() .for_each(|s| println!("result: {}", s.to_rust_string_lossy(scope))); }
実行結果は以下の通りです。
sample3 実行結果
> cargo run --bin sample3 ・・・ Running `target\debug\sample3.exe` *** context_group_id=123, level=4, message=log *** context_group_id=123, level=2, message=debug *** context_group_id=123, level=4, message=info *** context_group_id=123, level=8, message=error *** context_group_id=123, level=16, message=warn result: undefined