Ruby製の自作RDBMSをruby.wasmにてWasm化し、Wasmtimeで動かしてみた

前回、ruby.wasmを使ってRubyスクリプトをWasm化しました。
ruby.wasmにてRubyスクリプトをWasm化し、Wasmtimeで動かしてみた - メモ的な思考的な

 
次に、以前作成したRuby製の自作RDBMSをWasm化してみたことから、メモを残します。

 
目次

 

環境

  • WSL2
  • Ruby 3.3.6
  • ruby.wasm 2.7.0
  • Wasmtime 28.0.0

なお、Ruby製の自作RDBMSはこちらです。
https://github.com/thinkAmi-sandbox/simple_ruby_db

 

Wasm化したら、何の環境で動かすかを検討

自作RDBMS

という状況です。

 
自作RDBMSTCPサーバとして使えるようにした経緯もあり、まずはブラウザではなく、OS上の環境で動かしてみたいと考えました。

OS上の環境で動かすとなると気になるのがWASI (WebAssembly System Interface) です。WASIのPreview2には wasi-sockets が含まれることから、WASI Preview2 な環境で動かせると面白そうでした。
WebAssemblyを進化させる「WASI Preview 2」が安定版に到達。OSや言語に依存しないコンポーネントモデルを実現 - Publickey

 
ただ、RubyKaigi 2024の文字起こしレポートによると

kateinoigakukun:Wasmtimeのserveコマンドについては、WASI Preview 2っていう新しいブリーディングエッジなABIに依存した話で、現状はCRubyのWASIの対応としては基本的にはPreview 1に依存していて、Preview 2のものは直接は呼び出せない。 Preview 2にするっていうことはどういうことかというと、コンポーネントモデルに対応するっていうのとほぼ同義なんですね。なのでPreview 2にまず対応するにはCRubyをコンポーネントモデルに対応させることが前提になっていて、それがもうちょっとでgem対応と一緒にいい感じになる。その先でWASI Preview 2に固有のHTTPのリクエストをRubyAPIに対応させていくことで、serveコマンドとかも自然に使えるようになるはずです。

とのことでした。

また、kateinoigakukunさんの2024/9/12のスライドを見ても、Preview2にはチェックが入ってませんでした。
What you can do with Ruby on WebAssembly - Speaker Deck

そのため、自作RDBMSをWASI Preview2な環境で動かすのはまだ早そうと考えました。

 
次に、ブラウザで動かせるのか考えてみました。

ただ、自作RDBMSはファイルの open や write を行っているため、ブラウザのファイルシステムAPIへの書き直しが必要かもしれず、これはこれで大変そうでした。

 
そのため、今回は自作RDBMSをgemとして扱い、Rubyスクリプトに組み込んで使ってみることにしました。

これより、今回作成するRubyスクリプトでは

  • 実行環境は Wasmtime
  • require 'simple_ruby_db' して、自作RDBMSを使えるようにする
  • Rubyスクリプトの引数として実行したいSQL一式を受け取り、SQLを実行する
    • 自作RDBMS実装の都合上、SQLは一式で受け取らないとうまく動かない...
  • SQL実行のSELECTについてのみ、結果を puts する

を実装することにしました。

 

実装

simple_ruby_db を使うRubyスクリプトを作成

前回の記事同様 src ディレクトリを用意し、その中に database.rb に実装します。

 
なお、実装についてはPostgreSQLプロトコルを実装したときのソースコードを流用して手を加えていることもあり、解説は省略します。

詳しくは以下の記事を参照してください。
Ruby製の自作RDBMSにて、PostgreSQLプロトコルに対応して psql でメッセージを交換できるようにしてみた - メモ的な思考的な

require '/bundle/setup'
require 'simple_ruby_db'


class Database
  private attr_reader :db, :planner

  def initialize
    data_file_path = "#{Dir.pwd}/datafile"
    meta_file_path = "#{Dir.pwd}/metafile"

    File.open(data_file_path, 'wb') { |f| } unless File.exist?(data_file_path)
    File.open(meta_file_path, 'wb') { |f| } unless File.exist?(meta_file_path)

    @db = SimpleRubyDb::SimpleDb.new(data_file_path, meta_file_path)
    @planner = @db.planner
  end

  def run(sql)
    puts "SQL: #{sql}"
    case sql
    in String if sql.start_with?('create table')
      create_table(sql)
    in String if sql.start_with?('insert')
      insert(sql)
    in String if sql.start_with?('update')
      update(sql)
    in String if sql.start_with?('select')
      result = select(sql)
      puts "Result: #{result}"
    else
      return
    end
  end

  private

  def create_table(sql)
    planner.execute_update(sql, db.metadata_buffer_pool_manager)
  end

  def insert(sql)
    planner.execute_update(sql, db.buffer_pool_manager)
  end

  def update(sql)
    planner.execute_update(sql, db.buffer_pool_manager)
  end

  def select(sql)
    scan = db.planner.create_query_plan(sql, db.buffer_pool_manager).open

    schema = db.metadata_manager.layout(table_name(sql)).schema
    request_field_list = field_list(sql)
    col_defs = request_field_list.map { |field_name| {name: field_name, type: schema.field_type(field_name)} }

    [].tap do |result|
      while scan.next
        result.push(col_values(col_defs, scan))
      end
    end
  end

  def table_name(sql)
    # 今回、テーブル名は1つしか指定できない仕様なので、 first で取得して問題ない
    SimpleRubyDb::Parse::Parser.new(sql).query.table_list.first
  end

  def field_list(sql)
    SimpleRubyDb::Parse::Parser.new(sql).query.field_list
  end

  def col_values(col_defs, scan)
    col_defs.map do |col_def|
      col_def[:type] == 'integer' ? scan.get_int(col_def[:name]) : scan.get_string(col_def[:name]).delete_prefix("'").delete_suffix("'")
    end
  end
end

database = Database.new
ARGV.each do |sql|
  database.run(sql)
  puts '=' * 20
end

 

rbwasmによるビルドとパッケージング

まずは Gemfileに ruby_wasm と自作RDBMSのgem simple_ruby_db を指定します。

# frozen_string_literal: true

source "https://rubygems.org"

gem "ruby_wasm"
gem "simple_ruby_db", git: 'https://github.com/thinkAmi-sandbox/simple_ruby_db', branch: 'main'

 
続いて、rbwasmを使ってビルドします。

$ bundle exec rbwasm build -o ruby_with_db.wasm
...
INFO: Packaging gem: simple_ruby_db-0.1.0
INFO: Packaging setup.rb: bundle/setup.rb
INFO: Size: 51.27 MB

 
その後、rbwasmを使ってWasmに database.rb をパッケージングします。

$ bundle exec rbwasm pack ruby_with_db.wasm --dir ./src::/src -o database.wasm

 
最後に、Wasmtimeを使って、gemやRubyスクリプトのパッケージングを確認します。良さそうです。

$ wasmtime run database.wasm -e 'puts Dir.glob(["/usr/local/*", "/bundle/*/*", "/src/*"])'
/usr/local/bin
/usr/local/lib
/usr/local/share
/bundle/gems/simple_ruby_db-0.1.0
/src/database.rb

 

動作確認

今回は

  • CREATE TABLE でテーブルを作成
  • INSERT でデータを登録
  • SELECT でデータを確認
  • UPDATE でデータを更新
  • SELECT で再度データを確認

の流れを確認するため、上記SQL一式をWasmに渡してみます。

なお、実行コマンドが長いことから記事では折り返しています。実際には \ を削除し1行にして実行しています。

wasmtime run --dir .::/ database.wasm /src/database.rb \
  "create table apples (id int, name varchar(255));" \
  "insert into apples(id, name) values (1, 'shinano_gold');" \
  "insert into apples(id, name) values (2, 'fuji');" \
  "select id, name from apples;" \
  "select id, name from apples where id = 1;" \
  "update apples set name = 'akibae' where id = 2;" \
  "select id, name from apples;"

SQL: create table apples (id int, name varchar(255));
====================
SQL: insert into apples(id, name) values (1, 'shinano_gold');
====================
SQL: insert into apples(id, name) values (2, 'fuji');
====================
SQL: select id, name from apples;
Result: [[1, "shinano_gold"], [2, "fuji"]]
====================
SQL: select id, name from apples where id = 1;
Result: [[1, "shinano_gold"]]
====================
SQL: update apples set name = 'akibae' where id = 2;
====================
SQL: select id, name from apples;
Result: [[1, "shinano_gold"], [2, "akibae"]]
====================

 
出力された結果より、動作は良さそうです。

 

ソースコード

GitHubに上げました。なお、各wasmファイルはリポジトリに追加していません。
https://github.com/thinkAmi-sandbox/simple_ruby_db_wasm

今回のプルリクはこちら。
https://github.com/thinkAmi-sandbox/simple_ruby_db_wasm/pull/1