Rusty V8 で JavaScript のコードを実行する
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 の標準化された言語機能の仕様である ECMAScript と WebAssembly を実装しています。
Rusty V8 とは
Rusty V8 は V8 の C++ API に対する Rust のバインディングライブラリです。
このライブラリを使用することで組み込みデバイスやサーバーレス環境などで高性能で安全な独自の JavaScript ランタイムを構築することができます。
Rusty V8 は、Rust の所有権モデルを活用して、メモリの安全性を確保しています。
また、WebAssembly モジュールの実行もサポートされており、V8 のデバッガやプロファイラも利用可能とのことです。
なお、バージョンは V8 との同期を保つため Chrome のバージョン管理に準拠し「Chrome 129」に対応した 129.0.0 となっています。
cargo add v8
[dependencies]
v8 = "129.0.0"
Rusty V8 で JavaScript を実行する
Math
を実行してみる
JavaScript で数学的な定数と関数を提供するビルトインオブジェクトである Math
を実行してみます。
Math.PI
プロパティは円周率πを表す定数であり、Math.sin
メソッドはラジアン値から正弦を計算する関数です。
次のサンプルコードでは、90度における正弦を計算しています。
90 度の正弦は 1 なので、JavaScript のコードの実行結果は 1 を返します。
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
関数を実行します。
function add(a, b) {
return a + b;
}
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 を返す関数を実行する
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
と返ってきます。
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 を扱えるように修正したコードが以下です。
[dependencies]
futures = "0.3.30"
v8 = "129.0.0"
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