前回、ruby.wasmを使ってRubyスクリプトをWasm化しました。
ruby.wasmにてRubyスクリプトをWasm化し、Wasmtimeで動かしてみた - メモ的な思考的な
次に、以前作成したRuby製の自作RDBMSをWasm化してみたことから、メモを残します。
目次
環境
なお、Ruby製の自作RDBMSはこちらです。
https://github.com/thinkAmi-sandbox/simple_ruby_db
Wasm化したら、何の環境で動かすかを検討
自作RDBMSは
- gemとして作成してあるため、Rubyスクリプトに組み込んで使える
- PostgreSQLプロトコルに対応しているため、PostgreSQL風TCPサーバとしても使える
という状況です。
自作RDBMSをTCPサーバとして使えるようにした経緯もあり、まずはブラウザではなく、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のリクエストをRubyのAPIに対応させていくことで、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スクリプトに組み込んで使ってみることにしました。
- 実行環境は Wasmtime
require 'simple_ruby_db'して、自作RDBMSを使えるようにする- Rubyスクリプトの引数として実行したいSQL一式を受け取り、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