@ledsun blog

無味の味は佳境に入らざればすなわち知れず

Kindle 蔵書検索PWAアプリケーション

github.com

「電子書籍ストアはKindle以外使わないの?」ってたまに聞かれるんですけど、書籍数が多すぎて移行とかできないんですよ。 「多いってどれくらい?」と思ったのでちょっと調べてみたら、7,046冊でした。 人力で本を探し出すのは不可能ですよね。

検索アプリをつくりました。

どうせならスマートフォンから検索したいなとChatGPT と相談したら、PWAで小さくつくるのが良さそうでした。

アプリ自体にはデータを埋め込んでおらず、 JSONの蔵書リストを取りこんでIndexedDBに保存します。 検索用の UI だけ提供するアプリケーションです。

蔵書の取り込み自体は PC のブラウザから行います。 10年前からある Kindle Cloud Reader のページから蔵書をダウンロードしてくるスクリプトを流用しました。 ただ7000冊ダウンロードはコールスタックがあふれたので改良してあります。

RbValueのProxy化によるRubyメソッド呼び出し構文の簡略化

これは検討中の仕様を文章としてまとめたものです。

目的

JavaScriptから RbValue が表すRubyオブジェクトのメソッドを呼び出す際に、RbValue#call を明示する代わりに、通常のJavaScriptメソッド呼び出しに近い構文を利用できるようにする。

// 現在
user.call("name");
user.call("greeting");

// Proxy化後
user.name();
user.greeting();

RubyVM#eval やRubyメソッド呼び出しからJavaScriptへ返される RbValue をProxy化し、Rubyメソッドの戻り値に対しても呼び出しを連鎖できるようにする。

user.profile().name().upcase();

初期実装の目的は、Rubyメソッドの「直接呼び出し構文を簡潔にする」こと。 Proxyを生成する具体的な内部実装については、この文書では扱わない。ここでは外部的なAPI仕様のみを定義する。

基本方針

JavaScriptに公開されるすべての RbValue をProxy化する

利用者が asProxy() などを明示的に呼び出す方式にはしない。 JavaScriptに以下の経路で公開される RbValue は、Proxy化された状態で提供する。

  • RubyVM#eval の戻り値
  • RubyVM#evalAsync の解決値
  • RbValue#call の戻り値
  • RbValue#callAsync の解決値
  • Proxy経由のメソッド呼び出し結果
  • RubyからJavaScriptのコールバック関数へ渡される RbValue

RubyVM#eval と RubyVM#evalAsync で取得した RbValue

const user = vm.eval("User.new");
user.name();

RbValue#call や RbValue#callAsync で呼び出したメソッドの戻り値

const profile = user.call("profile");
profile.name();

メソッド呼び出し形式の戻り値

user.profile().name().upcase();

JavaScriptのコールバック関数へ渡す RbValue

const callback = (user) => {
   console.log(user.name().toString());
};

Proxy化すると別オブジェクトになる

JavaScriptのProxyは元のtargetとは別のオブジェクトである。 Proxy化された RbValue と内部の元 RbValue のJavaScript上のオブジェクト同一性は一致しない。

ruby.wasmは、一度Proxy化した RbValue を二重にProxy化しない。 利用者自身が明示的にProxyを生成するなど、ruby.wasmの管理外で発生する多重Proxyについては保証しない。

Proxyによってメソッド呼び出しを有効にするとブラケット記法も動作する

JavaScriptのProxyから見ると、次の2つは区別できません。

value.foo
value["foo"]

どちらも get(target, "foo", receiver) になります。そのため、

value.foo();

をサポートすると、必然的に次も動作します。

value["foo"]();

やらないこと

非同期メソッド呼び出しはメソッド呼び出しにしない

JS::Object#await を使うメソッドは、従来どおりcallAsyncを使う。

await value.callAsync("foo", ...args);

引数にRubyのブロックを渡す方法はサポートしない

現在の RbValue#call は引数としてブロックを受け取れない。 この挙動はそのままとする。

RubyメソッドをJavaScriptの関数オブジェクトとして扱わない

RubyメソッドをJavaScriptの関数オブジェクトとして取得、保存、比較、受け渡しする操作は初期サポートの対象外とする。

// 初期サポート対象外
const foo = value.foo;

foo();

value.foo === value.foo;

emitter.on("event", value.foo);
emitter.off("event", value.foo);

これらが実装上偶然動作する可能性はある。 仕様としては保証しない。

JavaScript引数の自動変換は行わない

Rubyメソッドへ渡すJavaScript引数の変換はしない。 従来どおり RbValue を受け取る。

例えば

array.push(vm.eval("123"));

JavaScriptのコールバック関数への引数の自動変換は行わない

ruby.wasmでは、RubyからJavaScriptのコールバック関数へRubyオブジェクトを渡す際、 to_jsメソッドがあるオブジェクトは自動変換されます。

例えば

JS.global.receive(123)

to_jsメソッドを持たない任意のRubyオブジェクトを渡す際はJS::Object.wrapを使い、RbValueへの変換を委譲します。

JS.global.receive(
  JS::Object.wrap(User.new("Alice"))
)

この動作は変更しません。

JavaScriptのプロパティ代入をRubyのsetterへ変換しない

初期実装では、

user.name = value;

をRubyの name= メソッド呼び出しへ変換しない。 通常のJavaScriptのプロパティ代入とする。

Rubyのsetterを呼び出したい場合は、RbValue#callメソッドを使用する。

value.call("name=", newName)

newNameはJavaScriptの値ではなく RbValue である。

JavaScriptのプロパティ探索APIを使ったRubyメソッド探索を提供しない

以下の、JavaScriptのプロパティからRubyのオブジェクトのメソッド応答性を検査することはできません。

typeof value.foo;

if (value.foo) {
}

"foo" in value;
Object.keys(value);
Object.getOwnPropertyNames(value);
Object.hasOwn(value, "foo");
Object.getOwnPropertyDescriptor(value, "foo");

Rubyのメソッドの存在や応答性を検査したいときは、RbValue#call を使ってRuby APIを呼び出します。。

value.call("respond_to?", vm.eval('"foo"'));
value.call("methods");

Rubyメソッドの探索ルール

RbValueは以下の優先順位でメソッドを探索する。

  1. レシーバ自身に定義されたJavaScriptプロパティ
  2. RbValue のJavaScript APIとプロトタイプチェーン
  3. Rubyメソッドへの転送を禁止する予約名
  4. Rubyメソッド

4に該当する場合に、Rubyのメソッド呼び出しと判断します。

レシーバ自身に定義されたJavaScriptプロパティ

JavaScriptからRbValueオブジェクトに追加したプロパティです。 利用者がJavaScript用として明示的に定義できる想定です。

value.toJSON = () => ({ type: "RubyValue" });
value.inspect = () => "custom inspection";

これによって利用者がRbValueオブジェクトをJavaScriptのオブジェクトとして拡張できるようにします。

RbValue のJavaScript API

以下の RbValue のJavaScript APIを優先します。

  • call
  • callAsync
  • toString
  • toJS
  • Symbol.toPrimitive

例えば、

value.call("foo");

はRubyの call メソッドではなく、既存の RbValue#call を呼び出す。

Ruby側の同名メソッドを呼び出したい場合は、RbValue#call を使う。

value.call("call", argument);
value.call("toString");
value.call("toJS");

これによって、既存の RbValue APIの動作を維持します。

たとえば toString および Symbol.toPrimitive が提供するJavaScriptでの文字列操作の動作を保証します。

value.toString();

String(value);
`${value}`;
"" + value;

Number(value);
+value;

RbValue のJavaScriptプロトタイプチェーン上のプロパティをRubyメソッドより優先する

例えば、

  • value.constructor
  • value.valueOf
  • value.hasOwnProperty
  • value.toLocaleString
  • value.isPrototypeOf
  • value.__proto__

JavaScriptランタイムやツールによる暗黙に呼ばれる名前を隠蔽する

以下のようなJavaScriptランタイム、開発ツール、ライブラリは、利用者が明示的に指定していないプロパティを内部的に参照することがある。

  • console.log
  • Node.jsのinspect
  • ブラウザのDevTools
  • JSON.stringify
  • テストフレームワーク
  • スナップショット機能
  • structuredClone
  • エラー表示

次の名前は暗黙に参照される可能性がある。

  • then
  • toJSON
  • inspect
  • Node.jsのinspect用Symbol

これらのメソッドへの参照はundefinedを返す。 呼び出すときはRbValue#callをつかう。

この動作はJavaScriptプロパティを定義することで上書き可能。

JavaScriptの識別子として表現できない呼び出したい場合は、RbValue#call またはブラケット記法を使う

RubyではJavaScriptの識別子として表現できないメソッド名も存在する。 その場合はRbValue#callを使用する。

value.call("empty?")
value.call("foo!")
value.call("name=", newName)
value.call("[]", key)
value.call("[]=", key, newValue)
value.call("<=>", other)
value.call("+", other)

ブラケット記法も利用可能。

value["empty?"]();
value["foo!"]();
value["[]"](key);
value["+"](other);

newName、key、other はJavaScriptの値ではなく RbValue である。

非機能的仕様

Proxy経由の呼び出しでも例外情報が減らない

value.foo();

と

value.call("foo");

で、Ruby側で発生した例外に差が生じないことを保証する。 少なくとも以下を維持する。

  • Rubyの NoMethodError が正しくJavaScriptへ伝播する
  • Ruby例外のメッセージを維持する
  • Ruby例外のバックトレースを維持する
  • Proxy層によって、本来のRuby例外が別の例外へ置き換わらない

Proxy化による性能への影響を検証する

JavaScriptへ公開されるすべての RbValue をProxy化するため、Proxy化前と比較して以下の操作に追加コストが発生する。

  • JavaScriptプロパティアクセス
  • RbValue の既存API呼び出し
  • Rubyメソッド呼び出し
  • Rubyメソッドの戻り値の受け渡し

初期実装ではAPIの一貫性を優先し、原則としてすべての RbValue をProxy化する。 実用上問題になる性能劣化が確認された場合は、Proxyを経由しない利用方法の提供を検討する。

ヤバい日本の住所

3部構成です。 タイトルから期待する珍住所紹介は2章にあります。

1章には日本の住所システムの成り立ちが、法律上の成り立ちと市町村での運用の具体例を交えて説明されています。 日本の住所システムは長期運用されてきた業務システムそのものです。法律にあわせ、現場の運用ルールを加味し、かつ時間経過によってルールが変わったあとも、変更前ルール準拠のデータが生き残っている。いわば、ひとつの巨大レガシーシステムです。

2章では、その巨大レガシーシステムの持つデータ(珍住所)を扱うシステム屋さんからみた苦悩が列挙されています。 とにかく日本の住所のヤバさをもっと知るべきだと思います|inuro に書いてある、珍住所がさらにたくさん紹介されています。

3章は、システム屋さんからみた理想の住所システムの模索です。どれも一長一短です。日本の住所システムを置き換えられるようなものではありません。

ブログの記事との大きな違いは、日本の住所システムの成り立ちの説明が補強されていて、「日本の住所システムは(システム屋さんから見たら)、作る側からみても、使う側からみても、再構築する側からみても、どっからどうみても地獄です」というお話になっている点です。

住所に限らず、システムの大小に限らず、システム屋さんは現実データを一般ユーザーが扱えるようにルールを整備して大量の例外処理を組んで、通れる道を作るのがお仕事です。大変ですよね。

ITシステム屋とはなんなのかが新書一冊にぎゅっと詰まっていてお得な本です。一本のブログ記事から執筆依頼した編集さんのホームランだと思います。

RequireLocalを作ったら、WASI Preopenだけでよかった

Node.jsでruby.wasmを動かす時にRubyスクリプトをどうやって読み込むか? - @ledsun blogで、考えていたやつを実現して、PR #658でマージされるまでの物語です。

背景

JavaScriptからRubyを使う機能を開発していくにあたり、Node.jsを検証環境として使いたいと考えました。ブラウザより起動やデバッグが手軽で、自動テストにも組み込みやすいからです。

ruby.wasmでは、JavaScriptからWebAssembly版のRubyを起動し、Rubyコードを評価できます。 短いコードなら、JavaScriptから直接渡せば十分です。

vm.eval(`
  puts "Hello from Ruby"
`);

実際のRubyアプリケーションは一つのファイルだけでは完結しません。 エントリーポイントから別のファイルをrequire_relativeし、そのファイルがさらに別のコードを読み込みます。

Node.js上のruby.wasmからローカルのRubyコードを読み込む方法が必要になりました。

RequireRemoteをローカルファイルへ応用する

ruby.wasmのjs gemには、URLからRubyコードを取得して評価するJS::RequireRemoteがあります。

ブラウザではホストのファイルシステムを直接読むことはできません。 JavaScriptのfetchを使ってURLからソースコードを取得できます。 RequireRemoteは、JavaScriptのこの機能をRubyから利用するための仕組みです。

Node.jsでも、似た構成が考えられます。

  1. Rubyから読み込みたいパスをJavaScriptへ渡す
  2. JavaScriptがreadFileSyncでファイルを読む
  3. 読み込んだRubyコードをruby.wasmで評価する

このローカル版をRequireLocalとして実装し始めました。

require "js/require_local"

JS::RequireLocal.instance.base_dir = "/path/to/app"
JS::RequireLocal.instance.load("main")

さらにrequire_relativeのshimを用意すれば、アプリケーション側では通常のRubyコードに近い形で使えます。

良さそうです。実装を進めましょう。

RequireLocalはRubyのrequireを再実装しない

Rubyのrequireには、ファイルを評価する以上の機能があります。

  • ロードパスの探索
  • 拡張子の補完
  • $LOADED_FEATURESによるロード済みファイルの管理
  • 二重requireの防止
  • 循環requireの処理
  • LoadErrorの生成
  • __FILE__の設定

RequireLocalに、これらすべてを再実装はしません。 既存のRequireRemoteと同程度の機能をローカルファイル向けに提供することを目標にします。

RequireLocalがカバーする範囲

RequireLocalは次の範囲を担当します。

  • ファイルを見つける

    • 基準ディレクトリから相対パスを解決する
    • 省略された.rbを補う
    • Node.jsのreadFileSyncでソースコードを読む
  • Rubyコードとして読み込む

    • ファイル名を指定してトップレベルで評価する
    • __FILE__やcaller_locationsからファイル名を参照できるようにする
    • 読み込み失敗をLoadErrorとして扱う
  • 最低限のロード状態を管理する

    • 成功したファイルを$LOADED_FEATURESへ登録する
    • 読み込み済みなら再評価せずfalseを返す
    • RequireLocalをつかったrequire_relativeのshimを提供する

$LOAD_PATHやRubyGemsを使った探索、ネイティブ拡張の読み込み、循環requireへの対応などは対象外としました。

現実的なスコープに落とし込めてそうです。実装を進めましょう。

RequireRemoteとRequireLocalの共通化

RequireRemoteとRequireLocalはどちらも、大まかには次の処理をしています。

  1. 読み込み対象の場所を解決する
  2. Rubyコードを取得する
  3. ファイル名を指定して評価する
  4. 読み込んだファイルを管理する

将来、ロード処理を修正する場所が二つに増えるのは避けたいところです。

そこで、Rubyコードを評価する部分をRubyLoaderとして共通化しました。require_relativeのshimや、ロード済みファイル名を求める処理についても共通化を検討しました。

一方、すべてを一つにまとめられるわけではありません。

RequireRemoteのURL解決はJavaScriptのURLオブジェクトに依存します。対して、RequireLocalのパス解決はRubyだけで動作します。アルゴリズムは似ていても、依存する環境が大きく異なります。

設計の筋も悪くなさそうです。

課題: readFileSyncをRubyへ公開してよいのか?

require_relativeを使うために、アプリケーションのディレクトリを読みたいです。 そこでRequireLocalでは、Node.jsのreadFileSyncをRubyから呼び出せるようにしした。

機能はこれで実現できます。 が、問題はRubyコードからNode.jsプロセスが読めるすべてのファイルへアクセスできることです。 つまり、実行ユーザーの権限で読めるすべてのファイルです。

RequireLocalをopt-in機能にして、ユーザーの責任とすることもできます。

const { vm } = await DefaultRubyVM(module, {
  enableRequireLocal: true,
});

しかし、WASMに対してサンドボックスを期待するユーザーは「ファイルすべて」という権限制御の単位は「大雑把過ぎる」と感じそうです。 代わりに「ディレクトリ単位」で許可・不許可を制御したいでしょう?

ここで、WASIにファイルシステムの公開範囲を制御する仕組みがあることに気づきました。 WASI Preopenです。

WASI Preopenを使う

WASIでは、ホスト側のディレクトリをゲスト側のパスへ明示的に割り当てられます。

たとえば、ホスト側の./appを、ruby.wasmからは/appとして見せられます。 DefaultRubyVMに初期化オプションで渡すならこんなイメージです。

const { vm } = await DefaultRubyVM(module, {
  preopens: {
    "/app": "./app",
  },
});

Ruby側からは、通常のファイルシステムとして見えます。 requireも動きますし、

require "/app/main"

読み込んだ/app/main.rbからrequire_relativeも使えます。

# /app/main.rb
require_relative "lib/helper"

つまり、新しいローダーを作る必要はありませんでした。 必要なのは「Node.js向けのDefaultRubyVMに、WASIのpreopensオプションを指定できるようにする」でした。

RequireLocalとPreopenの比較

二つの方式を比較するとこんな感じです。

観点 RequireLocal WASI Preopen
ファイルの読み込み JavaScriptのreadFileSync WASIファイルシステム
ロード処理 RequireRemote相当の独自実装 CRuby組込
require_relative shimで提供 CRuby組込
__FILE__ 独自管理 CRuby組込
$LOADED_FEATURES 独自管理 CRuby組込
二重require 独自管理 CRuby組込
エラー処理 独自管理 CRuby組込
公開範囲 Node.js側の権限 Preopen単位
保守対象 RubyとJavaScriptのローダー オプションを中継するブリッジ

あらためて整理すると、WASI Preopenの圧勝です。

気がつくまでにRequireLocalも結構作り込んでいました。 RequireLocalブランチとmainの比較をご覧ください。 悲しいですね。

でも、RequireLocalを作ったことも無意味ではありません。 一度実装したから、Preopen方式が互換性とセキュリティの両面で優れていることを確認できました。

設計方針の変遷

実は二段階ではなく三段階の変遷です。

  1. evalFile
  2. RequireLocal
  3. WASI Preopen + Ruby標準のrequire

徐々に「ruby.wasm側が責任を持つ範囲」が小さくなりました。

第1段階:evalFile

最初の案は、JavaScript APIへ「Rubyファイルを評価する」機能の追加でした。

vm.evalFile("/path/to/main.rb");

利用者にとっては簡単です。 用途は限定されています。 読み込むファイルの数だけAPIを呼び出す必要があります。

第2段階:RequireLocal

次に、RequireRemoteを模倣したRequrieLocalを思いつきました。 RequireRemoteでrequire_relativeを実装できることが分かっていました。 一連のアプリケーションファイルを一度に読み込めます。

JS::RequireLocal.instance.load("main")

第3段階:WASI Preopen

最後がWASI Preopenです。

const { vm } = await DefaultRubyVM(module, {
  preopens: {
    "/app": "./app",
  },
});

Rubyからアプリケーションのディレクトリが見えれば、Rubyの標準機能が使えます。

require "/app/main"

main.rbからの読み込みも通常どおりです。

require_relative "lib/helper"

二段階で取り除かれたもの

段階 ruby.wasmが提供するもの ruby.wasmが引き受ける責務
evalFile ファイルを評価する高水準API 読み込みとRuby評価
RequireLocal ローカルRubyコード用ローダー 読み込み、パス解決、評価、ロード管理
Preopen WASI設定の受け渡し ディレクトリをWASIへ渡す

後になるほど、APIは低水準になります。 利用できる範囲は広がります。

Pure Ruby Gemも読み込める

先ほどちょっと書きましたが、Preopenはアプリケーションコードだけでなく、Pure Ruby Gemにも利用できます。 たとえば、Gemのlibディレクトリを渡し

const { vm } = await DefaultRubyVM(module, {
  preopens: {
    "/gems/example/lib": "./vendor/gems/example/lib",
  },
});

Ruby側でパスを$LOAD_PATHへ追加すれば、requireで読み込めます。

$LOAD_PATH.unshift("/gems/example/lib")
require "example"

Rubyだけで実装されたGemであれば、この方法で読み込めます。 説明がややこしいので、このことはプルリクエストやドキュメントには書きませんでした。

data-envとの類似性

data-envの記事では

  1. Ruby::Boxをブラウザから有効にする
  2. HTMLからRuby VMへ任意の起動時環境変数を渡す

と解決策が変化しました。今回も次のように変化しています。

  1. Node.js上のruby.wasmでRubyスクリプトを読み込む
  2. DefaultRubyVMからWASIの任意のPreopenを設定できるようにする

共通点は「目的の機能を実装する」から「下のレイヤーにすでに存在する仕組みを、利用者が設定できるようにする」への変化です。

学び

似た判断を繰り返したということは、何か共通の性質がありそうです。 試しに、一言でまとめてみます。

専用機能を実装する前に、その要求を実現できる下位レイヤーの能力が既に存在していないか確認する

別の問題に再適用しやすいようにチェックリストにすると

1. 必要以上に複雑な実装をしそうになったら問題設定を見直す

  • data-envでは、独自フラグから環境変数への変換
  • RequireLocalでは、ロード関連機能を独自実装

Preopenは、ファイルを見せるだけにしてRuby標準のrequire・require_relativeへ任せました。

2. 要望の制約を探す

実現したいこと 妨げていた制約
Ruby::Boxを動かしたい 起動時環境変数を渡せない
require_relativeを動かしたい Rubyからローカルディレクトリが見えない

3. 既存の仕組み接続する

下位レイヤーの機能を公開する薄いブリッジを実装します。

  • Ruby VMの環境変数を受け取る機能
  • WASIのPreopen

Ruby::Boxをきっかけに、ruby.wasmへ汎用的なdata-envを追加しました

Pull Request: Enable easy passing of environment variables in browser.script.iife.js(ruby/ruby.wasm#649)

まとめ

ruby.wasmのブラウザ向けスクリプトに、Ruby VMの起動時環境変数を渡すためのdata-env属性を追加しました。

この変更のきっかけは、Brandon ZylstraさんのIssue 「Ruby::Box support—is it possible in Ruby WASM?」(ruby/ruby.wasm#645) です。

Issueで尋ねられていたのは「Ruby 4.0で導入された実験的機能Ruby::Boxをruby.wasmでも有効にできるか」でした。 調べてみると、Ruby::Box自体はruby.wasmでも動作しました。ただし、ブラウザから手軽に使うためのbrowser.script.iife.jsには、Ruby VMの起動時環境変数を渡す方法がありませんでした。 そこで、Ruby::Box専用の対応ではなく、任意の起動時環境変数をHTMLから渡せる汎用的なdata-env属性を追加しました。

今回の変更により、次のようなHTMLでRuby::Boxを有効にできるようになりました。

<script
  src="https://cdn.jsdelivr.net/npm/@ruby/head-wasm-wasi@latest/dist/browser.script.iife.js"
  data-env='{"RUBY_BOX":"1"}'
></script>

<script type="text/ruby">
  box = Ruby::Box.new

  box.eval <<~RUBY
    X = 123
  RUBY

  puts Ruby::Box.enabled? #=> true
  puts box::X             #=> 123
</script>

PRで追加したサンプルコードは、ruby-box.htmlで確認できます。

Ruby::Box専用の機能ではありません。任意の環境変数をRuby VMの起動時に渡せる、汎用的な仕組みです。

機能への要望や具体的なユースケースをIssueとして表明してもらえることは、OSSプロジェクトにとって非常にありがたいことです。今回も「Ruby::Boxをruby.wasmで使いたい」というニーズが示されたことで、Ruby::Boxだけでなく、さまざまな用途に利用できる仕組みを追加できました。

技術的な背景

Ruby::Boxには起動時の環境変数が必要

Ruby::Boxを有効にするには、Ruby VMの起動時にRUBY_BOX=1という環境変数を渡す必要があります。 重要なのは、「起動時に」という点です。

次のようにRubyコードの中から環境変数を設定しても、Ruby::Boxは有効になりません。

ENV["RUBY_BOX"] = "1"

Rubyコードが実行される時点では、Ruby VMの初期化はすでに完了しています。そのため、Ruby::Boxを有効にするには、Ruby VMを起動する側から環境変数を渡さなければなりません。

JavaScript APIでは環境変数を渡せた

Node.jsや、JavaScriptからRuby VMを直接初期化する場合は、以前からenvオプションを利用できました。

const { vm } = await DefaultRubyVM(module, {
  env: {
    RUBY_BOX: "1",
  },
});

ブラウザでruby.wasmを最も簡単に使うためのbrowser.script.iife.jsには、このenvをHTMLから指定する方法がありませんでした。

ruby.wasmそのものはRuby::Boxを実行できるのに、最も手軽なブラウザ向けインターフェースからは有効にできない状態でした。

今回の設計判断

Ruby::Box専用の設定にしなかった

Ruby::Boxを動かすだけなら、例えば次のような専用属性を追加する方法も考えられます。

<script
  src="..."
  data-ruby-box="true"
></script>

しかし、この方法ではRuby::Boxしか解決できません。 Ruby::Box専用の属性にしてしまうと、同じ種類の要望が出るたびに新しい属性を追加することになります。

この設計判断には、過去のIssueでの経験も影響しています。

Issue #340では、スタックサイズに起因する問題への対処として、RUBY_FIBER_MACHINE_STACK_SIZEをRuby VMの外から指定する必要がありました。このような「Ruby VMの起動時環境変数を変更したい」ニーズが以前にもあったことを覚えていました。

そのため、今回RUBY_BOX=1を渡す仕組みを追加するなら、Ruby::Boxだけを特別扱いするのではなく、RUBY_FIBER_MACHINE_STACK_SIZEのような別の環境変数にも使える形にするのがよいと考えました。

今回見つかった本当の問題は、「Ruby::Boxを有効にできないこと」ではなく、「Ruby VMの起動時環境変数をHTMLから渡せないこと」でした。 そこで、Ruby::Box専用のスイッチではなく、任意の環境変数を渡せるdata-env属性を追加しました。

<script
  src="..."
  data-env='{
    "RUBY_BOX": "1",
    "RUBY_FIBER_MACHINE_STACK_SIZE": "1048576"
  }'
></script>

Ruby::Boxは今回の変更を始めるきっかけですが、「起動時環境変数を外部から変更したい」ニーズはRuby::Boxに限りません。 追加した仕組みもRuby::Boxには依存していません。 目の前のユースケースだけを動かすのではなく、同じ種類の要求にも再利用できるインターフェースにできたことが、今回の変更で気に入っている点です。

data-envにはJSONを採用しました

data-envの形式として、当初は次のようなKEY=valueの列を考えていました。

<script
  src="..."
  data-env="RUBY_BOX=1 MSG=hello"
></script>

単純な値だけであれば、これは分かりやすい形式です。しかし、空白を含む値を自然に表現できません。

MSG=hello world

また、環境変数の値には=が含まれる場合もあります。

FORMULA=a=b

これらを正しく扱おうとすると、引用符やエスケープに関する独自ルールが必要になります。単純に見えた記法が、次第に小さなシェル構文のようになってしまいます。

この点についてコードレビューで提案をいただき、最終的にはJSONオブジェクトを採用しました。

<script
  src="..."
  data-env='{
    "RUBY_BOX": "1",
    "MSG": "hello world",
    "FORMULA": "a=b"
  }'
></script>

JSONを利用することで、空白や=を含む値を独自のエスケープ規則なしで表現できます。また、「オブジェクトの値は文字列だけ」というAPIの仕様も明確になります。

実装では、不正なJSONや配列、null、文字列以外の値などを検証し、利用できない項目については警告を出して無視するようにしています。

明示的なJavaScriptオプションを優先しました

WASI Preview 1では、DefaultRubyVMに渡す既存のenvオプションへdata-envの内容を追加します。

const { vm } = await DefaultRubyVM(module, {
  ...options,
  env: {
    ...scriptEnv,
    ...options?.env,
  },
});

JavaScriptから明示的に渡されたoptions.envを後から展開しているため、同じ名前の環境変数があった場合は、JavaScript側の指定が優先されます。

Pull Requestがカバーしている範囲

data-envはWASI Preview 1とPreview 2に対応

ruby.wasmには、WASI Preview 1を使う構成とWASI Preview 2を使う構成があります。 両者では、Ruby VMへ環境変数を渡す経路が異なります。 WASI Preview 1ではDefaultRubyVMのenvオプションを使用します。 WASI Preview 2では、wasi:cli/environmentの実装へ環境変数を合成します。

Ruby::Boxのサンプルを動かすことが発端ではありますが、data-env自体はRuby::Boxのためだけの機能ではありません。そのため、data-envによる環境変数の受け渡しはPreview 1とPreview 2の両方に実装し、E2Eテストでも確認しています。

Ruby::BoxのサンプルはPreview 2に対応していない

今回追加したRuby::Boxのブラウザサンプルは、現在のところWASI Preview 1でのみ動作します。

Ruby::Boxのrequireフックは、拡張ライブラリをロードする前にファイルを/tmpへコピーします。しかし、現在のWASI Preview 2のブラウザ環境には、書き込み可能な/tmpが用意されていません。

これは環境変数を渡せるかどうかとは別の問題です。今回のPull Requestでは、次のように対象範囲を分けました。

  • data-envによる環境変数の受け渡しはPreview 1とPreview 2の両方に対応します
  • Ruby::BoxのブラウザサンプルはPreview 1でのみテストします
  • Preview 2に書き込み可能な一時領域を用意する対応は、今後の課題とします

サンプル、テスト、ドキュメントを追加しました

今回のPull Requestでは、data-envの実装に加えて、次のものも追加しました。

  • ブラウザ上でRuby::Boxを実行するサンプル
  • data-envで渡した値をRubyのENVから参照するE2Eテスト
  • 空白や=を含む値のE2Eテスト
  • Ruby::Boxが実際に有効になることを確認するE2Eテスト
  • data-envのチートシートへの記載

APIの追加にくわえて、実際の利用例、ブラウザでの動作確認、ドキュメントまで含めて一つの変更としてまとめています。

わたしたちが光の速さで進めないなら

ko.rbでkazzixさんに教えてもらったSF小説短編集。韓国産なんですって、韓国産SF初めてです。

「感情の物性」が一番好きです。 この話は星新一風SFです。 星新一のショートショート的なアイロニーがあって、それでいて人間の感情の矛盾をウェットに描いています。星新一って、あんまりウェットじゃなくて、さらっとし過ぎと感じる時もあります。

全部が全部星新一風ではないです。例えば「巡礼者たちはなぜ帰らない」は二瓶勉風味でした。

bootstrap(セルフホスト)時のspinel_codegen.rb実行時のGC回数増

bootstrap(セルフホスト)時のspinel_codegen.rb実行時間の変わり方 - @ledsun blog で、Nodeテーブルを初期化するクラスを抽出すると、実効時間が160秒 => 344秒と伸びることが分かりました。 この原因を探りたいです。

まず、stackprof の CPU profile で計測したところGCに掛かる時間が増えていることが分かりました。 そこでGCに注目して計測してみました。

GC.stat による GC 回数の測定

GC.stat で GC 回数と allocation の差を確認します。

目的

ruby spinel_codegen.rb ast output.c の実行前後で GC.stat を取り、次の値の差分を比較します。

  • total_allocated_objects
  • total_freed_objects
  • heap_live_slots
  • heap_free_slots
  • minor_gc_count
  • major_gc_count
  • old_objects

測定コマンド

抽出前の base と抽出後の extract-node-init を同じ入力 AST で測ります。 こんな感じです。

ruby -e '
ast, out, codegen = ARGV
ARGV.replace([ast, out])
before = GC.stat
t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
load codegen
t1 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
after = GC.stat

keys = [
  :total_allocated_objects,
  :total_freed_objects,
  :heap_live_slots,
  :heap_free_slots,
  :minor_gc_count,
  :major_gc_count,
  :old_objects,
]

puts "elapsed=#{(t1 - t0).round(3)}"
keys.each do |key|
  puts "#{key}=#{after[key]} delta=#{after[key].to_i - before[key].to_i}"
end
' AST_PATH OUT_C_PATH SPINEL_CODEGEN_RB_PATH

測定対象

対象 コミット 内容
base 0d10645 extract-node-init の親
extract d94f5d6 refactor(codegen): extract node table loader

結果

指標 base extract 差
elapsed 189.826s 356.201s +166.375s
total_allocated_objects delta 2,149,566,411 2,064,184,970 -85,381,441
total_freed_objects delta 2,144,512,398 2,063,651,005 -80,861,393
heap_live_slots delta 5,054,013 533,965 -4,520,048
heap_free_slots delta 2,416,646 2,339,788 -76,858
minor_gc_count delta 2,082 19,438 +17,356
major_gc_count delta 19 33 +14
old_objects delta 3,393,053 526,672 -2,866,381

読み取り

extract-node-init では総 allocation 数は少し減っています。 minor_gc_count が 2,082 から 19,438 に増えています。約 9.3x の増加です。 推論ロジックよりGCに原因がありそうです。

さて、なぜGC回数が増えたのでしょうか? またGCを切ったらこの差は縮まるのでしょうか?

bootstrap(セルフホスト)時のspinel_codegen.rb実行時間の変わり方

spinel_codegen.rbのbootstrap(セルフホスト)時の性能評価手順を整理 - @ledsun blog をつかってどのくらい性能が変わるかを試しに測ってみます。

refactor(codegen): extract node table loader · ledsun/spinel@d94f5d6 · GitHub のようにNodeテーブルを初期化するクラスを抽出します。すると?

変更前 。 コミットで言うと https://github.com/matz/spinel/commit/f6b593711e8100d0ff0fbd77243b8e1678e39d7e です。

{
  "results": [
    {
      "command": "ruby spinel_codegen.rb /tmp/spinel_master_codegen.ast /tmp/spinel_master_bench_gen1.c",
      "mean": 160.00851559026,
      "stddev": 11.138275974757432,
      "median": 154.14327744726,
      "user": 158.99329034,
      "system": 1.0015925266666665,
      "min": 153.02849486026,
      "max": 172.85377446326,
      "times": [
        153.02849486026,
        154.14327744726,
        172.85377446326
      ],
      "exit_codes": [
        0,
        0,
        0
      ]
    }
  ]
}

変更後

{
  "results": [
    {
      "command": "ruby spinel_codegen.rb build/codegen.ast /tmp/spinel_codegen_bench_gen1.c",
      "mean": 344.58640011078,
      "stddev": 9.74270517215269,
      "median": 347.07773677278,
      "user": 343.53394030666664,
      "system": 1.0031182066666666,
      "min": 333.83992995578,
      "max": 352.84153360378,
      "times": [
        333.83992995578,
        352.84153360378,
        347.07773677278
      ],
      "exit_codes": [
        0,
        0,
        0
      ]
    }
  ]
}

雑に中央値を見ると 160秒 => 344秒。 軽く倍になっています。 「こんなに変わるんだ」と、驚きがあります。

ここから、性能に大きく影響を与える変更を絞り込んで行きたいです。

spinel_codegen.rbのbootstrap(セルフホスト)時の性能評価手順を整理

ビルド済みのspinelコマンドの実効時間の話ではありません。 spinelコマンドを作成するためにspinel_codegen.rbを実行する時間を計測します。

背景

[codex] Extract NodeStore from codegen by ledsun · Pull Request #284 · matz/spinel · GitHub にてリファクタリングしてみたところ、僕の予想以上に実効時間が伸びたことが始まりです。 いくらか憶測で修正してみましたが、どういう修正が実効時間に影響を与えるのか特定できていません。

基本に立ち返ってまずは計測方法を用意します。

目的

spinel_codegen.rb の分割や require 追加が、ruby spinel_codegen.rb の実行速度に与える影響を切り分けるための計測手順です。

計測対象

計測対象はコード生成フェーズだけに固定します。

ruby spinel_codegen.rb build/codegen.ast /tmp/spinel_codegen_bench_gen1.c

build/codegen.ast は計測前に一度だけ作り直します。

./spinel_parse spinel_codegen.rb build/codegen.ast

理由

make spinel_codegen は次の処理を含みます。

  1. spinel_parse による AST 生成
  2. ruby spinel_codegen.rb ... による C コード生成
  3. C コンパイラによる spinel_codegen バイナリ生成

今回見たいのは Ruby スクリプトの分割による影響なので、AST 生成と C コンパイルは計測から外します。これにより、Ruby のファイルロード、初期化、AST 読み込み、コード生成処理にかかる時間を比較しやすくします。

出力先は /tmp/spinel_codegen_bench_gen1.c に固定します。リポジトリ内の build/gen1.c を更新しないため、計測で作業ツリーを汚しません。

方法

hyperfine を使い、ウォームアップ 1 回、計測 5 回を標準条件にします。

#!/usr/bin/env bash
set -euo pipefail

RUNS="${RUNS:-5}"
WARMUP="${WARMUP:-1}"
AST="${AST:-build/codegen.ast}"
OUT="${OUT:-/tmp/spinel_codegen_bench_gen1.c}"
JSON_OUT="${JSON_OUT:-/tmp/spinel_codegen_hyperfine.json}"

if ! command -v hyperfine >/dev/null 2>&1; then
  echo "error: hyperfine is required" >&2
  exit 1
fi

if ! command -v ruby >/dev/null 2>&1; then
  echo "error: ruby is required" >&2
  exit 1
fi

if [ ! -x ./spinel_parse ]; then
  echo "error: ./spinel_parse is required; build it before measuring" >&2
  exit 1
fi

mkdir -p "$(dirname "$AST")"

echo "ruby: $(ruby -v)"
echo "hyperfine: $(hyperfine --version)"
echo "commit: $(git rev-parse --short HEAD 2>/dev/null || echo unknown)"
echo "ast: $AST"
echo "out: $OUT"
echo "runs: $RUNS"
echo "warmup: $WARMUP"

./spinel_parse spinel_codegen.rb "$AST"

hyperfine \
  --warmup "$WARMUP" \
  --runs "$RUNS" \
  --export-json "$JSON_OUT" \
  "ruby spinel_codegen.rb $AST $OUT"

wc -l "$AST" "$OUT"
echo "json: $JSON_OUT"
benchmark/measure_codegen.sh

実行回数は環境変数で変更できます。

RUNS=10 WARMUP=2 benchmark/measure_codegen.sh

結果の JSON は標準では /tmp/spinel_codegen_hyperfine.json に出力します。

JSON_OUT=/tmp/codegen-master.json benchmark/measure_codegen.sh

比較時は同じ Ruby、同じマシン、同じ RUNS / WARMUP で測ります。ブランチを切り替えた後は毎回このスクリプトを実行し、各ブランチの平均値、中央値、標準偏差を比較します。

Ubuntu + llama.cppでローカルLLM環境構築メモ

llama.cpp のビルド

依存ツールインストール

sudo apt update
sudo apt install -y build-essential cmake git

ソース取得

git clone https://github.com/ggml-org/llama.cpp
cd llama.cpp

ビルド(CMake)

cmake -B build
cmake --build build --config Release -j

動作確認

モデル実行

./build/bin/llama-cli -hf Qwen/Qwen2.5-Coder-3B-Instruct-GGUF

今のところ効果がありそうな設定

-t 8
-c 4096

下のパラメーターを指定したら遅くなりました。

-t 12
--cache-type-k q8_0
--cache-type-v q8_0

性能目安

  • Prompt: ~60 tok/s
  • Generation: ~10 tok/s

ノートPCのCPUで動かしているので、これくらい

WSLのrbenv経由のRubyの起動を速くする

結論

mkdir -p ~/.local/bin
ln -sf /usr/bin/readlink ~/.local/bin/greadlink
fish_add_path -p ~/.local/bin

問題

https://github.com/matz/spinel のテストをローカルで実行すると、229回 Ruby が実行されます。 Ruby の起動に時間が掛かると、それだけでテスト実行に時間が掛かります。

原因

rbenv はまず /usr/bin/rbenv がシンボリックリンクなので、それをたどって /usr/lib/rbenv/libexec/rbenv を見つけます。 このときに readlink コマンドを使います。

ただし macOS の標準 readlink は BSD 系で、GNU readlink とはオプションや挙動が違います。 macOS で GNU coreutils を入れると、GNU 版 readlink は greadlink という名前で提供されることがあります。

この差を吸収するため、rbenv は type -p greadlink readlink を実行し、まず greadlink コマンドを探します。

WSL で PATH に /mnt/c/... の Windows 側パスが多く含まれていると、Linux 側に存在しない greadlink を探すために Windows 側パスまで探索します。 この探索に時間が掛かります。

対策

greadlink コマンドを PATH の上流に置くと探索が即座に終わり、rbenv shim 経由の Ruby 起動が速くなります。

RubyKaigi 2026

0日目

移動トラブル

今回のRubyKaigiは、かなり印象的なスタートになりました。

もともとは羽田空港から函館空港へ飛行機で移動する予定でした。 管制機器のトラブルによって、飛行機の出発が危ぶまれる状況になったのです。 飛行機は諦めて新幹線で移動することにしました。

移動には4時間ほどかかり、函館駅に到着したのは10時過ぎでした。

ただ、このトラブルのおかげで良い体験もありました。 同じように移動トラブルに巻き込まれていた rhiroe さんが、Slackで「ご飯行く人いませんか?」と募集していたのを見かけて、それに乗っかる形で一緒にジンギスカンを食べに行きました。 Slack上ではIDを見かけていて認識はしていたのですが、実際にお会いするのは初めてでした。

ジンギスカン

移動トラブルが良い思い出になりました。

1日目

キーノート

The Journey of Box Building - RubyKaigi 2026

Tagomorisさん

1日目で一番印象に残ったのは、Tagomorisさんのキーノートでした。

3年間、Ruby::Boxを作り続けてきたのもすごいのですが、2023年のshioyama先生の発表がきっかけでRubyにコミットするモチベーションに気がついた話がエモくて良かったです。

自分の過去のことを思い出しました。 そういえば自分も2022年のkateinoigakukunさんの発表をみて、自分の「ブラウザでRubyを動かしたい」というモチベーションに思い出したのでした。

こういうRubyKaigiの発表を見ていた人が刺激を受けて次の発表者になるのが、とてもRubyKaigiで、エモいです。

PicoRuby.WASM

Funicular: A Browser App Framework Powered by PicoRuby.WASM - RubyKaigi 2026

はすみさんの PicoRuby.WASM の発表も印象に残っています。

スリープ処理を自前で実装しているのがかっこよかったです。 ruby.wasm は、WASIに依存する作戦をとったため、スリープの実装は諦めています。 必要であればアドホックにブラウザのsetTimeoutを使う方針です。 ここを諦めずに、自前でタスクスケジューラから実装しているのがかっこよかったです。

さらにブラウザでデバッガも動いていてかっこいいです。

はすみさんは継続的にアウトプットを出し続けている点もすごいです。

お昼ご飯とちょっとした雑談

1日目のお昼は、ANDPADさんが配られていたラッキーピエロのチャイニーズチキンバーガーを食べました。

杉野さんと大塚さんと一緒に食べていました。 RubyKaigiで毎年恒例になってきました。 美容院の話なんかしました。

杉野さんが血糖値の計測装置をつけていて、その話で盛り上がっていました。 ご飯を食べると本当に数値が上がるので面白かったです。

レッドブルを飲むと一気に上がって下がるという話を聞いて、へーって思いました。

はるかさんの発表

Building a Standalone Ruby Programming Environment - RubyKaigi 2026

はるかさん

はるかさんの発表は、また違った意味で印象に残っています。 内容自体も面白かったのですが、それ以上に説明をはっしょった苦労ポイントがありそうでした。 DVI出力の速度が足りないときにどうやってボトルネックを特定したのか不思議でした。

詳しいことは、やんちゃバーで話を聞くことができました。 しかし残念なことに、聞いた内容は、僕の脳みその中のアルコールで蒸留されました。

後日、Boothで販売されていたマイコンも購入しました。

ライトニングトーク

今年は銅鑼が一杯聞けて良かったです。 去年の懸田さんのプチ炎上したメッセージは伝わっていたみたいです。

LT発表者の方々

懇親会

移動は最初徒歩で進んでいたのですが、途中から市電が空いてきたため、市電に乗って駅前まで移動し、そこから歩いてホテルに向かいました。

Bashさん、高橋さん、nagachikaさんと話した記憶はあります。 翌日に発表があるので、ここで宿に帰りました。

発表資料修正

オチで悩んでいました。 そもそもこの時点で、デモの案はありましたが動いていませんでした。 代わりにひねったオチを用意してありました。 ただしRubyKaigiでコードやデモ見せないで口先だけでなんかいうのかっこ悪すぎるんですよね。

デモをつくってみました。 意外とちゃんと動いたので、オチは「乞うご期待」とシンプルに終わらせることにしました。

完成してみると、自分は思っていたよりピリピリしてたなと気がつきました。

やんちゃハウスとやんちゃバーという拠点

今回の滞在でかなり重要だったのが、やんちゃハウスの存在でした。

函館アリーナのすぐ近くにあり、徒歩ですぐに行き来できる距離にあったため、イベントの合間に戻ることができました。 これが想像以上に便利で、セッションの合間に休憩したり、お風呂に入ったり、ちょっと横になったりすることができました。

また、やんちゃバーが毎日20時頃から深夜2時頃まで開かれていて、「常に戻れる場所がある」感がありました。

2日目

2日目の朝は、近くの日帰り温泉に入りに行きました。 このあとあわせて、3日で5回入ります。

Astroの話が気になった

The AST Galaxy to the Virtual Machine Blues - RubyKaigi 2026

笹田さん

笹田さんのAstroの発表でした。

事前コンパイルすることでツリーウォークインタプリタ方式で実装を簡単にしつつ、YJIT並の実行速度を出すアイデアでした。 AIの力で2週間で新しいアイデアを試せるのすごい時代ですよね。

なんで「事前コンパイルすることでツリーウォークインタプリタ方式で実装を簡単にしつつ、YJIT並の実行速度が出る」のかはちんぷんかんぷんでした。

Matzの発表で出てきたSpinelの話とも対応してて少し面白いです。 AstroはフルのRubyをどう簡単(?)に実装するかという話で、Spinelは割り切ったRubyサブセットの高速化を狙うもので、同じようなアプローチでも目指すゴールは違いそうです。

関さん

Programming with a DJ Controller - not vibe coding - RubyKaigi 2026

関さん

見てるときは気がつかなかったけど、エディタが同時に二つ動くの異常なんだよなあ。 「エディタがRuby製でOSの機能を通さずに直接動かしているから」なんだけど、その場では気がつかなかった。 咳マニアとして不覚です。

「ログを音楽にするのは、直感に反して多分実用的なんじゃないかなあ?」と、思いました。 ログを目grepするときって文字じゃなくて、色の濃さとか波とか見ているので、慣れれば音楽からも同じことができる気がします。

発表前の準備と初めての通し練習

五島軒のお弁当をゲットして早めに食べて発表練習をしていました。 このタイミングで、初めて30分通しのリハーサルを行いました。

デモ込みでも、大体時間丁度だったので良かったです。 本番では、早めに進んで、30秒くらい時間が余りました。

nagachikaさん

Pure Intonation on Browser: Building a Sequencer with Ruby - RubyKaigi 2026

nagachikaさん

nagachikaさんの発表も、結構印象に残っています。 音楽理論の話で、純正律と平均律の違いについて説明されていました。

普段自分たちが聞いている音楽は平均律で作られていて、これはオクターブを均等に分割したものです。均等に割ると無理数になるのでプログラマー的にはちょっと気持ち悪い数値になっています。 一方で純正律は、きれいな比率で音を作るので、理屈としてとても美しいです。

デモで ruby.wasm を使って、ブラウザのAPI経由で音を鳴らしていたのも印象的でした。 実際に音を聞いてみると、単音では逆に違和感がありました。普段から平均律に慣れているので、「あれ、なんか変だな」と感じるみたいです。 和音で聞くと綺麗に聞こえました。

ruby.wasm 経由でブラウザのWeb Audio APIが使えることで、Rubyから音楽プログラミングができるようになりました。 実際に発表で使っているのを見ると、胸が熱くなります。

しかも、あのnagachika新聞のnagachikaさんが使っているのです。

発表本番

ruby.wasm also enables JavaScript to call Ruby libraries. - RubyKaigi 2026

自分の発表を行いました。

メインテーマは「JavaScriptからRubyを呼び出す方向をがんばるぞい!」です。 裏テーマは「これまでのruby.wasm総復習」です。 裏番組が両方英語だったので、ruby.wasmに初めて触れる人も迷い込んできそうと読みました。

函館アリーナのGLAY関連展示

発表後の完全オフモード

発表が終わった後は、アフタヌーンブレイク以降のセッションには参加せず、やんちゃハウスに戻りました。 お風呂に入り横になったら、そのまま寝てしまったようです。

目覚めたら、少し時間があったので函館山周辺を散歩しました。 アリーナ周辺は平らなのに、函館山周辺だけめちゃ坂でした。

函館山の坂観光

転職ドラフトの懇親会に行った

2日目の夜は、転職ドラフト主催の懇親会に参加しました。 ビールは4杯飲みました。APA、IPA、もものヴァイツェンと黒いのを飲みました。

「RubyKaigiで発表している人として見られている」感覚が新鮮でした。 過去3年RubyKaigiで発表しているので、別に不思議なことは何もないのですが。

このあと流れでタクシーで函館山に登りました。

函館山展望台からの夜景

めちゃくちゃ寒かったです。

函館山から五稜郭へ

函館山で夜景を見たあと、そのままタクシーで五稜郭の方へ移動しました。

五稜郭のあたりで、会社の人たちと合流したら、なぜかないさるろーさんとも再会しました。 前に会ったのは沖縄でのRubyKaigiのときで、公式懇親会のあとに、麦汁さんたちと急に飲みに行きました。

それ以来だったので、久しぶりに会えて嬉しかったです。 こういうふうに、「前のRubyKaigiで会った人と、また次のRubyKaigiで会う」というのが続いていくのも、RubyKaigiの面白いところです。

今は横須賀.rb方面で活動されているらしいです。 横須賀.rbに行く理由が増えました。

このあと初やんちゃバーです。 さすがに一杯飲んで寝ました。

3日目

3日目の朝は、近くの日帰り温泉に入りに行きました。 3回目です。

Ruby Committers and the World

Ruby Committers and the World - RubyKaigi 2026

Rubyコミッターのみなさん

RubyKaigiの目玉イベントですね。 After Matz体制はみんな気になりますよね。

お昼

早めに会場を抜けて昼食はラーメンを食べました。

塩ラーメン

偶然、横須賀.rbの記念撮影現場に出くわして、ないさるろーさんに代わって撮影係をやりました。

国分さん

Lightning-Fast Method Calls with Ruby 4.1 ZJIT - RubyKaigi 2026

いまやJIT第一人者ですね。 言ってることは分かったつもりなのですが、なぜJITフレームに情報を逃がすと速くなるかはよくわかりませんでした。 保持する情報量は変わっていないように思えます。

最後の「AIで誰でもZJIT書けるようになったので、みんなもJIT書きましょう」って言ってたのが面白かったです。 RJITの時も「RUBYで誰でもRJIT書けるようになったので、みんなもJIT書きましょう」と言ってたのを思い出しました。

Matzのキーノートが良かった

Matzさん

最終日のMatzのキーノートも、今回かなり印象に残っています。

正直なところ、これまでのRubyKaigiのキーノートは、どちらかというとRubyの大枠の方針の話で技術の詳細に踏み込まない「締めの儀式」みたいな感覚で聞いていることが多かったです。 もちろん毎回面白いのですが、いわゆるテックトークとは少し違うものとして聞いていました。 今回はかなり印象が違いました。

Spinelの話が出てきて、「新しいことをやっているな」と感じました。 RubyをAOTでCにトランスパイルして、コンパイルしてから実行します。 Rubyのサブセットとして割り切って設計されているのが、mrubyの経験とかが生きているのかな?と思いました。つまり「RubyというのはサブセットであってもRubyである」という経験知がありそうです。 上手く発展すると、GoやRustが主戦場にしているCLIツール系にRubyの利用領域が広がるので夢があります。

Matzから新しいプレゼントがもらえて、自分は盛り上がりました。 あとで気がついたのですが、笹田さんのAstroとも少し繋がっていますね。

それと同時に、AIの使い方の話も印象に残っています。

Emacsで直接編集するのをやめて、Claude Codeだけでmrubyを実装している、という話をしていて、「そこまで振り切るのか」と思いました。自分だと「手で直した方が早い」と思ってしまうのですが、あえてそうしないのが、“縛りプレイ”的で面白かったです。

夜の懇親会

3日目の夜はTwoGateさんの懇親会に参加しました。 懇親会の前にお風呂に入りました。 4回目ともなると慣れた物で、RTAっぽくなってきました。

会場はモデルルームみたいなとてもおしゃれな一戸建ての家で、出張シェフの方がその場で肉を焼いて提供してくれるスタイルでした。 豚肉や鹿肉をいただきながら、ワインを飲みつつ、AOTコンパイラの話などで盛り上がりました。

やんちゃバー

その後、やんちゃバーに戻り、katsuyoshiさんとSpinelのデバッグなどをしていました。

Open class definitions emit duplicate C definitions · Issue #5 · matz/spinel · GitHub

記憶にないのですが、記録によるとやんちゃバーでバグ報告までやったみたいです。 バグ報告としては最速だったみたいです。

翌日にはMatzに直してもらえました。 SpinelにIssueを出すと、Matzが(ClaudeCodeをつかって)直してくれます。まるでMatzと会話しているような感じがあって楽しいです。

4日目

最終日は、少し観光をしてから帰ることにしました。

やんちゃハウスチェックアウトまえが最後のお風呂でした。 RTA記録はおおよそドア・ツー・ドアで20分くらいです。

そのあと、丸井今井に寄ってお土産を一通り買いました。 観光も兼ねて、五稜郭タワーに上ろうかなと思っていたのですが、これがかなり混んでいて驚きました。 金曜日までとは明らかに人の多さが違っていて、「これが花見&GW初日の威力か・・・」とおののきました。 タワーに登るのは諦めて、写真を撮りました。

五稜郭タワー

周りのお店もどこも混んでいて少し困りましたが、バス乗り場の前にあるスープカレー屋さんは空いていたので、そこでお昼を食べることにしました。

スープカレー

バスで函館空港へ向かいました。 荷物が結構増えていたので預け入れをしたのですが、そのときに航空会社の方から「座席をエコノミーに変更してもらえませんか」という相談を受けました。 もともと少し良い席を取っていたのですが、特に問題もなかったので、そのままオファーを受けることにしました。 搭乗口の前で呼び出されてチケットを変更しました。 たまに見る呼び出されている人はこういう感じなのかと、実績解除できました。

帰りは無事に羽田空港に到着して、そのままスムーズに帰宅しました。

帰宅後Spinelで遊んでバグ報告しました。

Struct definition order can produce incomplete type errors for value fields · Issue #14 · matz/spinel · GitHub

WSL2のディスクサイズを縮小する手順

WSL2のディスク(.vhdx)は動的拡張です。使っていると段々大きくなっていきます。 WSL内のファイル削除しても、Windowsからは空き領域が検知できずvhdxファイルの物理サイズは縮まりません。 exportで実データをtarに書き出す。importで再作成すると、未使用領域を除いた状態でvhdxを再作成できます。

事前準備

WSL内を掃除

sudo apt clean
sudo apt autoremove
docker system prune -a

TRIM

sudo fstrim -av

縮小手順

WSLを停止

wsl --shutdown

export

wsl --export Ubuntu D:\backup\ubuntu.tar

補足

  • pax format cannot archive sockets は 無視してOK(正常)
  • 実データ量だけtarになる

既存WSL削除

wsl --unregister Ubuntu

新しい保存先を作成

Microsoft StoreからインストールしたUbuntuはC:\Users\<ユーザー>\AppData\Local\Packages\...を使っています。 importするときは、使用ディレクトリを指定します。

mkdir C:\wsl\ubuntu

import

wsl --import Ubuntu C:\wsl\ubuntu D:\backup\ubuntu.tar --version 2

新しい ext4.vhdx が作られる

デフォルトユーザー復元

import後はrootになるので修正

sudo nano /etc/wsl.conf

内容:

[user]
default=ledsun
wsl --shutdown

Gem.ruby_api_versionについて

ruby.wasmの怪現象 - @ledsun blog についてrubygemsにissueつくるかー、と思って調べていたら・・・迷宮入りしました。 現状をメモしておきます。

背景

ruby.wasm では、Ruby VM 初期化直後に次のような状態で返ることがあります。

  • Gem は定義されている
  • rubygems.rb は未ロード
  • Gem.ruby_api_version / Gem.extension_api_version は未定義

WASI向けにbundle install --standalone で生成される bundler/setup.rb を使うときに問題になります。k

ruby.wasm 側ではワークアラウンドを PR #622 で入れてあります。

Bundler に立てようと思っているIssueの概要

Bundler の standalone generator は generated setup.rb の中で:

  • require "rbconfig" は行う
  • unless defined?(Gem) のときだけ Gem.ruby_api_version / Gem.extension_api_version の簡易実装を定義する

そのため、今回のように

  • Gem は defined
  • しかし RubyGems API surface は未完成

という状態だと、フォールバック定義が入らず、 Gem.ruby_api_version を前提にしているソースコードが壊れます。

関連コード: https://github.com/ruby/rubygems/blob/4758fb59f8f83695aac41e7dd1034c9042424a66/bundler/lib/bundler/installer/standalone.rb#L76

次のように個別にチェックすればいいのかな?とイメージしています。

def self.ruby_api_version
   RbConfig::CONFIG["ruby_version"]
 end unless respond_to?(:ruby_api_version)

def self.extension_api_version
  if "no" == RbConfig::CONFIG["ENABLE_SHARED"]
    "\#{ruby_api_version}-static"
  else
    ruby_api_version
  end
end unless respond_to?(:extension_api_version)

Bundlerの既存 Issue

Standalone bundle's setup.rb should implement Gem.try_activate to avoid breaking binding.irb · Issue #7545 · ruby/rubygems · GitHub が近いです。 standalone の setup.rb がpartial な Gem surface が問題を起こしています。

また、コメントで次のような設計案が出ています。

  • Gem.ruby_api_version を RbConfig.ruby_api_version に移せないか
  • Gem.extension_api_version も RbConfig 側に移せないか
  • Gem.* は将来的に deprecated にできないか

もし、これが実現すればアドホックにruby_api_versionを定義する必要が無くなります。 今回の問題の根本的な解決になりそうです。

bugs.ruby-lang.orgの既存議論

RbConfig.ruby_api_version の提案はありませんでした。

関連しそうな議論:

特に #6648 はとても長くて、議論に追いつけていません。 RbConfig.ruby_api_version を提案するなら、それなりに準備が必要そうです。

Gem.ruby_api_versionと RbConfig::CONFIG["ruby_version"] の違い

Gem.ruby_api_version は Gem.target_rbconfig["ruby_version"]を見ているようです。 これはクロスコンパイルと関係があるようですが、僕にはよくわかりません。

今のところ考えていること

とりあえず rubygems/rubygems にIssueを作るのは良さそうです。 あとは、せっかくRubyKaigiが近いので、RbConfigについての理解を深めておいて、誰かに直接相談したいところです。