🦖

Rustとdeno_coreを使って、独自のJavaScriptランタイムを作ってみる

2022/08/02に公開

Node.js、Denoに続いて、Bunという新たなJavaScriptランタイムが登場していますが、Denoの7月29日のブログにて、"Roll your own JavaScript runtime"という記事が投稿されていたので紹介します。
https://deno.com/blog/roll-your-own-javascript-runtime
タイトルの通り、自分のJavaScriptランタイムを作ってみよう!という記事です。
作り方は元記事に載っているのでここでは省略して、掻い摘んだ内容をまとめていきます。

JavaScriptランタイムの材料

  • Rust
    • deno_core
    • tokio

Rustのdeno_coretokioというクレートを使って作っていきます。

deno_core

https://crates.io/crates/deno_core
deno_coreって、もうそれdenoじゃんと思いましたが、こちらのクレートはJavaScriptエンジンであるV8との橋渡し的な役割を持つようです。

deno_coreは、複雑で大量のAPIを持つV8をシンプルに利用できるように、V8インスタンスをカプセル化したJsRuntimeという構造体を提供してくれます。

JsRuntimeは、イベントループを抽象化して実装していますが、ループを駆動させるのはユーザの責任となります。
ちなみに、TypeScriptのサポートなどはありません。

そもそもV8とは何ぞやという方は、こちらの記事が非常に分かりやすかったのでおすすめです。
https://zenn.dev/estra/books/js-async-promise-chain-event-loop/viewer/e-epasync-v8-engine

tokio

https://crates.io/crates/tokio
JsRuntimeのイベントループを駆動させるために入れます。
非同期アプリケーションを書くための、イベント駆動型ノンブロッキングI/Oプラットフォーム。
特徴としては3つ挙げられていました。

  • 速い: ゼロコスト抽象化機能により、ベアメタル・パフォーマンスを実現する
  • 信頼性: Rustの特徴を活用し、バグを少なく、スレッドセーフを確保する。
  • 拡張性: フットプリントを最小限に抑え、バックプレッシャーやキャンセルを自然に処理できる。

Console API

V8では、Console APIが提供されています。
そのため、特にコードを書かなくてもconsole.log()などは最初から使うことができるのかと思いましたが、そうでもなかったです。

console.log("Hello, runjs!");                                      
console.error("Boom!");   

エラーを出さずに実行できましたが、コンソールに何も表示されませんでした。
ちなみに、Console APIにないものを呼び出すとエラーを吐いたので、ちゃんと用意はされているようです。

console.test("TEST");   
error: TypeError: console.test is not a function

runtime.jsに次のように書いて、Rustのメインファイルで読み込ませると実行できます。
(詳細は元記事のAdding the console APIをご覧ください。)

// runtime.js
((globalThis) => {
  const core = Deno.core;

  function argsToMessage(...args) {
    return args.map((arg) => JSON.stringify(arg)).join(" ");
  }

  globalThis.console = {
    log: (...args) => {
      core.print(`[out]: ${argsToMessage(...args)}\n`, false);
    },
    error: (...args) => {
      core.print(`[err]: ${argsToMessage(...args)}\n`, true);
    },
  };
})(globalThis);
[out]: "Hello runjs!"
[err]: "Boom!"

もちろん、Consoleに独自のものを追加することだってできます。

  globalThis.console = {
    log: (...args) => {
      core.print(`[out]: ${argsToMessage(...args)}\n`, false);
    },
    error: (...args) => {
      core.print(`[err]: ${argsToMessage(...args)}\n`, true);
    },
    test: (...args) => {
      core.print(`[test]: ${argsToMessage(...args)}\n`, true);
    },
  };

console.test("TEST");   
[test]: "TEST"

Dino API

新しくDinoというグローバルオブジェクトを追加して、非常に単純なAPI(?)を作ってみました。
(元記事では、filesystem APIを追加しながら説明しています。)
core.opAsynccore.opSyncは、JavaScriptとRustの関数を結合するためのものです。ここでは、同期的な処理しかしないのでopSyncしか使っていません。
deno_coreはこれらの引数の文字列にマッチする、#[op]属性を持つRust関数を見つけ出してくれます。

// runtime.js
  globalThis.dino = {
      carn: () => {
          core.opSync("op_carn");
      },
      herb: () => {
          core.opSync("op_herb");
      },
  };
// main.rs
#[op]
fn op_carn() {
    println!("🦖");
}

#[op]
fn op_herb() {
    println!("🦕");
}

また、これらのopをJavaScriptのコードから利用するには、main.rsで "extension" を 登録してdeno_coreに知らせる必要があります。
この "extension" を利用することで、Rustのさまざまな関数をJavaScriptに読み込ませることができるようになります。

// main.rs
async fn run_js(file_path: &str) -> Result<(), AnyError> {
    let main_module = deno_core::resolve_path(file_path)?;
    // 大事なのはここ↓
    let runjs_extension = Extension::builder()
        .ops(vec![
             op_carn::decl(),
             op_herb::decl(),
        ])
        .build();
    // 大事なのはここ↑
    let mut js_runtime = deno_core::JsRuntime::new(deno_core::RuntimeOptions {
        module_loader: Some(Rc::new(deno_core::FsModuleLoader)),
        extensions: vec![runjs_extension], // ←ここも大事
        ..Default::default()
    });
    js_runtime.execute_script("[runjs:runtime.js", include_str!("./runtime.js")).unwrap();

    let mod_id = js_runtime.load_main_module(&main_module, None).await?;
    let result = js_runtime.mod_evaluate(mod_id);
    js_runtime.run_event_loop(false).await?;
    result.await?
}

Dino APIのcarn関数とherb関数をそれぞれ実行した様子です。

// example.js
dino.carn();
dino.herb();
🦖
🦕

Rustの内部にある関数を簡単に呼び出すことができました。

感想

JavaScriptのランタイムなんて、とてもじゃないけど作る気にはなれないし、そもそも作れませんが(bunの作者はすごい...)、簡単なものを少し作って遊んでみる分には面白かったです。
ぜひみなさんも作って遊んでみてください!
ここまで読んでいただきありがとうございました。

成果物

https://github.com/k41531/runjs

Discussion