🕌

LLRTからロードできるネイティブなESMモジュールを追加する

2024/02/16に公開

LLRT only support a fraction of the Node.js APIsなので既存のNode.jsのモジュールを動かすためには自分でランタイムにサポートを入れる必要がある

これには動作時にshimを指し込むとかビルド時にコードを書き換えるなどの方法があるが、LLRTはrquickjsのAPIに沿って設計されているので自分でRustで書いて拡張するのも結構敷居が低い

ビルド環境はBuilding from sourceのとおり行えば当方aarch64-apple-darwinだが問題なく構築できた

注意点としてはLLRTは内部バイナリ表現としてfacebook/zstdを使っていて、それのビルドにzigコンパイラが必要になったりする

あとrustcの新しめのnightlyを使った方が問題が起きなさそう。我が家ではrustc 1.76.0 (07dca489a 2024-02-04)が動いていた

makeが済んだら以下コマンドが実行できれば環境は整っている

❯ ./target/debug/llrt --version
LLRT (darwin arm64) 0.1.6-beta

全体の流れとしては以下

  1. src/以下のmodでrquickjsの独自ModuleDefを実装
  2. src/main.rsに上記を追加
  3. src/vm.rsにJS(ESM)とのマッピングを定義

src/hello/mod.rsを追加して以下のように書く

src/hello/mod.rs
use rquickjs::{module::{Declarations, Exports, ModuleDef}, prelude::{Func}, Ctx, Result};
use crate::module::export_default;

pub struct HelloModule;

impl HelloModule {
    /*
    呼び出される関数本体コード
    */
    pub fn greet(_ctx: Ctx, name: String) -> String {
        format!("Hello, {}!", name)
    }
}

impl ModuleDef for HelloModule {
    fn declare(declare: &mut Declarations) -> Result<()> {
        declare.declare("greet")?;
        declare.declare("default")?;
        Ok(())
    }

    fn evaluate<'js>(ctx: &Ctx<'js>, exports: &mut Exports<'js>) -> Result<()> {
        /*
         export_default: LLRTが用意するヘルパー関数 Default import空間で設定する
         */
        export_default(ctx, exports, |default| {
            default.set("greet", Func::from(Self::greet))?;
            Ok(())
        })?;

        Ok(())
    }
}

main.rsに下位モジュールとして定義する

src/main.rs
@@ -34,6 +34,8 @@ mod uuid;
 mod vm;
 mod xml;
+mod hello;
+
 use minimal_tracer::MinimalTracer;
 use rquickjs::{AsyncContext, Module};
 use std::{

vm.rsは重要ファイルでLLRTのJavaScruipt VMの実装

ESMの"hello"とHelloModuleを対応付けする

src/vm.rs
@@ -59,6 +59,7 @@ use crate::{
     },
     uuid::UuidModule,
     xml::XmlModule,
+    hello::HelloModule,
 };
 
 macro_rules! create_modules {
@@ -99,7 +100,8 @@ create_modules!(
     "buffer" => BufferModule,
     "child_process" => ChildProcessModule,
     "util" => UtilModule,
     "uuid" => UuidModule,
+    "hello" => HelloModule
 );

これをテストするためのJSコードも用意

test.mjs
import {greet} from "hello";

console.log(greet("LLRT"));

Hello Worldに必要なのはこれだけだが、Rustコードを編集しながらデバッグしたくなる

以下のように呼び出す

cargo run -- test.mjs
   Compiling llrt v0.1.6-beta (/src/llrt)
    Finished dev [unoptimized + debuginfo] target(s) in 4.33s
     Running `target/debug/llrt test.mjs`
Hello, LLRT!

Hello, LLRT!が表示されたのでhelloモジュールが動作していることが分かる

Discussion