quickjs.rbでRubyの環境からJavaScriptのロジック(I/Oのないコード)の動作結果を得る

JavaScriptのコードの動作結果をRubyから得たい

実は、ながーいことこういうことを思ってはポシャり思ってはポシャりしていたのです。

お前、Ruby がプライマリじゃないのかよと思うかもしれないけど、まぁいろいろあって JS を書くことはかなり多いです。仕方ないよね、いろんな Runtime で JS らしき何かが動くからね。でまぁそれなりの量の JS のコードがあるわけですよ。

ということでこんなことを考えてはいるんだけど、直近で言うと

HerokuのCloud Native Buildpacksを使ってCloud RunでSinatraアプリを動かしてみた (2023-12-16) | あーありがち

辺りで調べて諦めている。この時は

  • コンテナで Ruby を動かしたい
  • でも一部 JS のコード、JS の何かを得たい

という要求で、この時は

を試していたんだけど、ES5 が基本になっていてさすがに厳しかった。これが2023 年。

quickjs.rbがよさげ

上記 2023 年の時点では存在しなかった新しいプロジェクトが 2024 年に生まれていた。それが

hmsk/quickjs.rb: A CRuby wrapper to run QuickJS in Ruby

※ 当時は自分で調べていたんだけど、今回は Perplexity Pro Research にお願いした結果たどり着いた

QuickJS は 2019 年に始まったプロジェクトで、いわゆる組み込み型の JavaScript エンジンてやつ。当時は ES2020 対応で始まっていたらしいんだけど、2025 年現在は ES2023 互換を謳ってるっぽい。

quickjs.rb はこの QuickJS を Ruby から使えるようにしたもので、Duktape.rb や MiniRacer と同じ系統。ちょっと macOS ( Apple Sillicon ) で build してサイズ比較してみたところ、

  • MiniRacer 8.5 MB ほど
  • quickjs.rb 2.3MB ほど
  • Duktape.rb 650KB ほど

という感じになった。

ちょうど中間。Duktape はめちゃくちゃ小さいけど動作するコードが古すぎて無理だったが、V8 エンジンを組み込める MiniRacer よりはずいぶん小さく済んでる。これはなかなかよさそう。

使ってみる

昨日の KintoneTabulax のコードを Ruby から動かしてみる。

require "quickjs"
require "csv"
require "json"

table = CSV.read(<csvfile>, headers: true, nil_value: "")
csv = table.map { |row| row.to_h }

vm = Quickjs::VM.new(timeout_msec: 5000)
vm.import(
  { KintoneTabulax: "KintoneTabulax" },
  from: File.read("node_modules/kintone-tabulax/dist/kintone-tabulax.js")
)
vm.define_function("csv") { csv }

unpivot = vm.eval_code(<<EOD)
const t = new KintoneTabulax({
  // configuration
})
t.normanpivot(csv())
EOD

unpivot.each_pair { |table, records|
  File.write(
    "#{table}.csv",
    (
      [records.first.keys.to_csv] +
      records.map { |record| record.values.to_csv }
    ).join
  )
}

全体の流れ

上に書いたコードは

  1. Ruby から一つの CSV ファイルを読み込んで
  2. quickjs.rb
    • import 宣言のような記述で JS のライブラリを与える
    • 読み込んだ CSV データを返す function を定義
    • JS 側の KintoneTabulax を呼んで処理
  3. 処理した結果を Ruby のオブジェクトとして取得し、Ruby で新しい CSV ファイルを作成

している。I/O は Ruby で、ロジックだけ JS で処理できている。いやーやはりこういうコードは Ruby だととても短く書けて見通しがいい。

特徴、気を付けること

  1. 日本語を含む文字列の log を p で出力しようとするとバイト列になっちゃう
  2. console.log は VM の中の logs に溜まる。そのまま terminal などで確認することはできないんだけど、忘れがち
  3. Ruby 側で動的に生成されるデータについては、上記のようにそれを返す function を define_function() で定義して block にデータを与えると、いい具合に変換して JS 側に渡してくれる
    • 戻り値もいい具合に変換して返ってくる
  4. VM 初期化時のオプションのドキュメントがないのでコードを読め
    • 上のコードでは timeout を設定している。データ量やスペックに応じてここは見直しが必要

memory や stack のサイズも決められるので、これを大きくしておけばそこそこのことができそう。

感想

ドキュメントは足りないと思うが、かなりいいんじゃないかと思う。

  • ES2023 てどの範囲?
  • Ruby 側で動的に作るデータをどう渡すか?

という問題はあるものの、後者の問題が解決すれば、少なくとも依存のない自作のコードについてはかなりすんなり使えた。実践的な事例がたぶんまだかなり少ないと思うので、上記のコードを参考に自分で簡単な JS を用意してアレコレ試してみるといいと思う。

More