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 トレイトは basebase_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_idlevel の値も出力するようにしてみます。

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