🎱

Rusty V8 で JavaScript のコードを実行する

2024/09/30に公開

Announcing Stable V8 Bindings for Rust

先日、Announcing Stable V8 Bindings for Rust というブログが公開され、JavaScriptエンジン 「V8」のバインディングライブラリ「Rusty V8」で初の安定版 129.0.0 をリリースしたと Deno の Ryan Dahl 氏が発表しました。

V8 とは

まず、V8 JavaScript engine について、簡単に説明します。

V8 は Google が提供するオープンソースの JavaScript エンジンで、C++ で実装されており、主に Chrome ブラウザなどで利用されています。

V8 エンジンは JavaScript の標準化された言語機能の仕様である ECMAScriptWebAssembly を実装しています。

Rusty V8 とは

Rusty V8 は V8 の C++ API に対する Rust のバインディングライブラリです。

このライブラリを使用することで組み込みデバイスやサーバーレス環境などで高性能で安全な独自の JavaScript ランタイムを構築することができます。

Rusty V8 は、Rust の所有権モデルを活用して、メモリの安全性を確保しています。

また、WebAssembly モジュールの実行もサポートされており、V8 のデバッガやプロファイラも利用可能とのことです。

なお、バージョンは V8 との同期を保つため Chrome のバージョン管理に準拠し「Chrome 129」に対応した 129.0.0 となっています。

cargo add v8
Cargo.toml
[dependencies]
v8 = "129.0.0"

Rusty V8 で JavaScript を実行する

Math を実行してみる

JavaScript で数学的な定数と関数を提供するビルトインオブジェクトである Math を実行してみます。

Math.PI プロパティは円周率πを表す定数であり、Math.sin メソッドはラジアン値から正弦を計算する関数です。

次のサンプルコードでは、90度における正弦を計算しています。

90 度の正弦は 1 なので、JavaScript のコードの実行結果は 1 を返します。

main.rs
use v8::V8;

fn main() {
    // V8 を初期化
    let platform = v8::new_default_platform(0, false).make_shared();
    V8::initialize_platform(platform);
    V8::initialize();

    // Isolate を作成
    let isolate = &mut v8::Isolate::new(v8::CreateParams::default());

    // HandleScope を作成
    let handle_scope = &mut v8::HandleScope::new(isolate);

    // コンテキストを作成
    let context = v8::Context::new(handle_scope, v8::ContextOptions::default());
    let scope = &mut v8::ContextScope::new(handle_scope, context);

    // JavaScript コードをコンパイルして実行
    let source = v8::String::new(scope, "Math.sin(Math.PI * 90 / 180)").unwrap();
    let script = v8::Script::compile(scope, source, None).unwrap();
    let result = script.run(scope).unwrap();
    let result = result.to_string(scope).unwrap();

    println!("Result: {}", result.to_rust_string_lossy(scope));
    // => Result: 1

    // シャットダウン
    unsafe {
        V8::dispose();
    }
    V8::dispose_platform();
}

JavaScript ファイルを読み込んで関数を実行

別に保存された JavaScript ファイルを読み込んで、定義した関数を実行するサンプルコードを実行してみます。

今回は Rust のコードから引数を渡して add 関数を実行します。

index.js
function add(a, b) {
    return a + b;
}
main.rs
use std::fs;
use std::path::Path;
use v8::V8;

fn main() {
    // V8 を初期化
    let platform = v8::new_default_platform(0, false).make_shared();
    V8::initialize_platform(platform);
    V8::initialize();

    // Isolate を作成
    let isolate = &mut v8::Isolate::new(v8::CreateParams::default());

    // HandleScope を作成
    let handle_scope = &mut v8::HandleScope::new(isolate);

    // コンテキストを作成
    let context = v8::Context::new(handle_scope, v8::ContextOptions::default());
    let scope = &mut v8::ContextScope::new(handle_scope, context);

    // JavaScript ファイルを読み込む
    let file_path = Path::new("index.js");
    let code = fs::read_to_string(file_path).expect("Unable to read JavaScript file.");

    // JavaScript コードをコンパイルして実行
    let source = v8::String::new(scope, &code).unwrap();
    let script = v8::Script::compile(scope, source, None).unwrap();
    let _ = script.run(scope).unwrap();

    // add 関数を取得
    let global = context.global(scope);
    let add_key = v8::String::new(scope, "add").unwrap();
    let add_val = global.get(scope, add_key.into()).unwrap();
    let add_func = v8::Local::<v8::Function>::try_from(add_val).expect("Function not found.");

    // 引数を作成
    let arg_a = v8::Number::new(scope, 40.0);
    let arg_b = v8::Number::new(scope, 2.0);
    let args = &[arg_a.into(), arg_b.into()];

    // add 関数を呼び出して結果を得る
    let result = add_func.call(scope, global.into(), args).unwrap();

    let result_str = result.to_string(scope).unwrap();
    println!("Result: {}", result_str.to_rust_string_lossy(scope));
    // => Result: 42

    // シャットダウン
    unsafe {
        V8::dispose();
    }
    V8::dispose_platform();
}

Promise を返す関数を実行する

index.js
async function add(a, b) {
    return new Promise((resolve, reject) => {
        if (a > b) {
            resolve(a + b);
        } else {
            reject(new Error(`Condition not met: ${a} must be greater than ${b}`));
        }
    });
}
setTimeout を使ったコード

実行時に setTimeout is not defined と返ってきます。

index.js
function add(a, b) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            if (a > b) {
                resolve(a + b);
            } else {
                reject(new Error(`Condition not met: ${a} must be greater than ${b}`));
            }
        }, 5000);
    });
}

JavaScript ファイルを読み込んで関数を実行 のコードのまま実行すると、Result: [object Promise] と出力されます。

Promise を扱えるように修正したコードが以下です。

Cargo.toml
[dependencies]
futures = "0.3.30"
v8 = "129.0.0"
main.rs
use std::fs;
use std::path::Path;
use futures::executor::block_on;
use v8::{V8};

fn main() {
    // V8を初期化
    let platform = v8::new_default_platform(0, false).make_shared();
    V8::initialize_platform(platform);
    V8::initialize();

    // Isolate を作成
    let isolate = &mut v8::Isolate::new(v8::CreateParams::default());

    // HandleScope を作成
    let handle_scope = &mut v8::HandleScope::new(isolate);

    // コンテキストを作成
    let context = v8::Context::new(handle_scope, v8::ContextOptions::default());
    let scope = &mut v8::ContextScope::new(handle_scope, context);

    // JavaScriptファイルを読み込む
    let file_path = Path::new("index.js");
    let code = fs::read_to_string(file_path).expect("Unable to read JavaScript file.");

    // JavaScript コードをコンパイルして実行
    let source = v8::String::new(scope, &code).unwrap();
    let script = v8::Script::compile(scope, source, None).unwrap();
    let _ = script.run(scope).unwrap();

    // add 関数を取得
    let global = context.global(scope);
    let add_key = v8::String::new(scope, "add").unwrap();
    let add_val = global.get(scope, add_key.into()).unwrap();
    let add_func = v8::Local::<v8::Function>::try_from(add_val).expect("Function not found.");

    // 引数を作成
    let arg_a = v8::Number::new(scope, 40.0);
    let arg_b = v8::Number::new(scope, 200.0);
    let args = &[arg_a.into(), arg_b.into()];

    // add関数を呼び出して結果を得る
    let call_func = add_func.call(scope, global.into(), args).unwrap();

    // プロミスの結果を待機
    let result = block_on(async {
        let promise = v8::Local::<v8::Promise>::try_from(call_func).unwrap();
        while promise.state() == v8::PromiseState::Pending {
            scope.perform_microtask_checkpoint();
        }
        promise
    });

    let result_str = result.result(scope).to_string(scope).unwrap();
    println!("Result: {}", result_str.to_rust_string_lossy(scope));
    // => Result: 42

    // シャットダウン
    unsafe {
        V8::dispose();
    }
    V8::dispose_platform();
}

また、arg_a, arg_b をそれぞれ 40, 200 に変更して実行すると Result: Error: Condition not met: 40 must be greater than 200 と出力されます。

まとめ

Rust のコードから JavaScript を実行することができました!!!!

異なる言語のコードを実行する...
独自の JavaScript ランタイムの実装... とても夢のある話ですね🥳✨

参考

Discussion