プレアルファSpinel 2026.09.12MIT

Rails 流の Web アプリを、ひとつのネイティブバイナリに。

CyberTrain は、matz による Ruby の AOT コンパイラである Spinel のためにネイティブに書かれた、Rails 流の Web アプリケーションフレームワークです。語彙(resources、before_action、validates、form_with、スキャフォールド)はそのままに、cybertrain build がコントローラー、モデル、ビュー、そしてフレームワーク自体をひとつのネイティブバイナリにコンパイルします。成果物には Rack も、実行時の gem も、Ruby インタプリタも含まれません。

クイックスタート

ターミナル — 任意のディレクトリ、その後 blog/shell
gem install cybertrain
cybertrain new blog          # 初回は Spinel 2026.09.12 をインストール(数分)
cd blog
cybertrain g scaffold article title:string body:text
cybertrain db migrate
cybertrain server            # http://127.0.0.1:3000
cybertrain build             # 本番用: dist/blog, dist/public/, dist/storage/, dist/tmp/

Ruby 3.2+ が必要なのは cybertrain コマンドだけです(アプリケーションが CRuby 上で動くことはありません)。ほかに git、make、curl、C コンパイラ、SQLite 3 のヘッダーが必要です:始める前に。ビルドしたものは、cd dist && ./blog migrate && ./blog で、CYBERTRAIN_SECRET_KEY_BASE を設定して実行します。

  • サーバーHTTP/1.1、Rack なし
  • ランタイムgem なし、インタプリタなし
  • データベースSpinel FFI 経由の SQLite
  • CILinux と macOS

誰のためのものか、何ではないか

ひとつのネイティブバイナリを求める Rails 開発者のうち、若いプロジェクトと Ruby のサブセットを受け入れられる人のためのものです。

  • Roundhouse ではない

    Roundhouse は既存の Rails アプリを Spinel 向けにトランスパイルします。CyberTrain は、それに対して直接コードを書くネイティブなフレームワークであり、互換レイヤーではありません。

  • Rails・Rack 互換ではない

    Rack 互換性はなく、実行時に gem も使えません。使えるのは Spinel 独自の spin-index のパッケージのうち、その Ruby サブセットでコンパイルできるものだけです。

  • 本番運用には未対応

    CyberTrain はプレアルファです。サンプルのブログは Linux と macOS の CI で検証されています。

書くコード

Rails の語彙のまま、Spinel のルールは明文化。

コントローラー、モデル、ルーティング、ビューは Rails アプリと同じように読めます。AOT コンパイラのために違いが生じる箇所は、小さく、明示的で、ドキュメント化されています。

  • ルーティング

    ネストした resources ブロックはマッパーを明示的に受け取ります:articles.resources :comments。

  • コントローラー

    new は new_action と書きます。new という名前のメソッドは Klass.new(ctx) を覆い隠してしまうからです。

  • ビュー

    開発環境ではテンプレートをディスクから読み込むので、ビューの編集に再ビルドは不要です。cybertrain build はテンプレートをバイナリに埋め込みます。テンプレートは閉じた文法であり、任意の Ruby ではありません。

  • モデル

    class Article ではバリデーションを宣言します。カラム、ファインダー、関連付けは db/schema.rb から生成されます。

config/routes.rbruby
Cybertrain::Routes.draw do
  root "articles#index"

  # A nested block takes the mapper explicitly (Spinel's instance_eval
  # trampoline only rewires `self` for the outermost block): `articles.resources`.
  resources :articles do |articles|
    articles.resources :comments, only: [:create, :destroy]
  end
end
app/controllers/articles_controller.rbruby
# app/controllers/articles_controller.rb (index/show/edit are as plain as
# Rails' own scaffold; update mirrors create)
class ArticlesController < ApplicationController
  before_action :set_article, only: [:show, :edit, :update, :destroy]

  def new_action   # a `new` method would shadow ArticlesController.new
    @article = Article.new
  end

  def create
    @article = Article.new(article_params)
    if @article.save
      flash[:notice] = "Article was successfully created."
      redirect_to article_path(@article), status: :see_other
    else
      render :new, status: :unprocessable_entity
    end
  end

  private

  def set_article
    @article = Article.find(params[:id])
  end

  def article_params
    params.require(:article).permit(:title, :body)
  end
end
app/views/comments/_comment.html.erberb
<%# locals: (comment:) %>
<div class="comment">
  <p>
    <strong>Commenter:</strong>
    <%= comment.commenter %>
  </p>

  <p>
    <strong>Comment:</strong>
    <%= comment.body %>
  </p>

  <%= button_to "Destroy comment", [@article, comment], method: :delete %>
</div>
app/models/article.rbruby
# app/models/article.rb -- only the first string field gets a default
# presence validation; add anything past that yourself.
class Article
  validates :title, presence: true
  validates :body, presence: true, length: { minimum: 10 }
end

現在できること

最初に手に取る Rails の機能を、Spinel 向けにネイティブで。

  • ネイティブ HTTP サーバー

    素の Ruby で書かれた HTTP/1.1 サーバーで、接続ごとにグリーンスレッドを 1 本使い、リクエストとレスポンスは型付きです。Rack 層はありません。TLS には前段に nginx か Caddy を置きます。

    TCPServer#accept

  • ルーティングとコントローラー

    resources、ネストしたルーティング、before_action、after_action、rescue_from は、ビルド時に素の Ruby として生成されます。Strong Parameters や、フラッシュを設定してからのリダイレクトも Rails と同じように動きます。

    config/routes.rb

  • セッション、フラッシュ、CSRF

    HMAC 署名付き cookie によるセッション、フラッシュメッセージ、CSRF 保護がデフォルトで有効です。フォームには自動でトークンが含まれ、偽造された POST には 403 が返ります。

    _cybertrain_session

  • ERB 風のテンプレート

    ビューは Rails の ERB と同じ見た目で、レイアウト、パーシャル、form_with、link_to が使えます。開発環境ではディスクから読み込むので、編集しても再ビルドは不要です。cybertrain build はビューを埋め込みます。

    app/views/**/*.html.erb

  • SQLite のモデル

    バリデーション、コールバック、外部キーから導出される関連付け。Spinel の FFI 経由で、コネクションプールを使って SQLite とやり取りします。クエリはすべてパラメータをバインドします。

    ffi_func · ffi_lib

  • マイグレーションとスキャフォールド

    cybertrain generate scaffold はマイグレーション、モデル、コントローラー、5 つのビューを書き出します。cybertrain db migrate は生成を行い、マイグレーションを適用して db/schema.rb を書き換え、もう一度生成します。

    db/migrate/*.rb

  • ライブ開発ループ

    cybertrain server はコードを監視してバックグラウンドで再ビルドし、新しいバイナリを同じポート・同じ PID で再 exec します。ビルドに失敗した場合は古いサーバーが動き続けます。

    rebuild → stop → execv

  • 組み込みのテスト

    Cybertrain::Test でテストを書き、cookie jar 付きのクライアントでアプリ全体をプロセス内で動かします。cybertrain spin test は各テストをコンパイルし、その出力の差分を取ります。

    test/<name>.rb.expected

フレームワークと CLI

ひとつの名前、ふたつの部品。

gem install cybertrain でインストールされるのはコマンドであり、フレームワークではありません。フレームワークは Spinel パッケージで、アプリがこれに依存し、アプリに組み込んでコンパイルします。gem が提供するのは cybertrain コマンドで、アプリの作成、Spinel のインストール、Spinel のツールの実行を代わりに行います。

Spinel パッケージ

フレームワーク

アプリの土台となるライブラリ。

中身

HTTP サーバー、ルーティングとコントローラー、セッションと CSRF 保護、テンプレートインタプリタ、SQLite のモデルとマイグレーション、そして spin run gen の裏で動くコードジェネレーター。

入手方法

インストールは不要です。アプリの spin.toml がこのリポジトリのリリースタグ v0.2.1 を指定し、spin lock がそれを spin のキャッシュに取得して、コミットを spin.lock に固定します。アプリには何もコピーされません。

動く場所

アプリの中です。Spinel があなたのコードと一緒に事前(AOT)コンパイルします。CRuby 上で動くことはありません。

dist/blog には

組み込み済みCyberTrain のうち、出荷されるのはこの部分です。

RubyGem

cybertrain コマンド

アプリを作成し、ビルドし、実行するツール。

中身

cybertrain コマンドだけです:new、generate scaffold、db migrate、server、build、spin、setup、doctor。フレームワークも Spinel も gem には含まれません。

入手方法

gem install cybertrain です。初回使用時に、固定された Spinel のリリース 2026.09.12 を ~/.cybertrain にビルドします(そのリリースがすでに PATH 上にある場合を除く)。

動く場所

手元のマシン上で、CRuby 3.2 以降で動きます。Spinel の spin ツールを代わりに呼び出すので、自分で呼ぶ必要はありません。

dist/blog には

含まれないバイナリは Ruby、gem、Spinel なしで動きます。

両者は同じタグからリリースされるため、バージョンは一致します:0.2.1 の gem は、フレームワーク v0.2.1 に依存するアプリを書き出します。アプリを新しいリリースに移行するには、gem を更新し、spin.toml を新しいタグに向けてから、cybertrain spin lock と cybertrain spin run gen を実行します。

仕組み

メタプログラミングは一度だけ、ビルド時に。

Spinel は事前(AOT)コンパイルを行い、eval を持ちません。そのため、Rails が実行時に method_missing や define_method を使う場面では、spin run gen が読んでコミットできる素の Ruby を書き出し、インタプリタで解釈するのはビューだけです。

CyberTrain のパイプラインdb/schema.rb、config/routes.rb、app/、app/views/ がビルドの入力になります。spin run gen は、コミットされる素の Ruby を gen/ に書き出します。cybertrain build は app/views/ を埋め込み、gen/ とフレームワークをひとつのネイティブバイナリ dist/blog にコンパイルします。このバイナリは HTTP/1.1 で応答し、Spinel の FFI を通じて SQLite とやり取りします。開発環境では、バイナリは代わりに app/views/ をディスクから読み込み、戻りの経路がソースを監視します:監視、再ビルド、停止、execv。書くものビルド時 · spin実行時db/schema.rbtablesconfig/routes.rbルートapp/controllers, modelsspin run gen1gen/素の Ruby・コミット済モデルルート · URLヘルパーview_assignsrun_callbackmigrations · viewsmanifest2cybertrain builddist/blogone native binaryframeworkcontrollers · modelstemplate interpreter埋め込みビューHTTP/1.1 サーバーSQLite via Spinel FFI4app/views/開発: ディスク読込dist/blog に埋め込み35開発:監視 → 再ビルド → 停止 → execv開発時のみ · ビルドに失敗しても旧サーバーは動き続ける CyberTrain のパイプラインdb/schema.rb、config/routes.rb、app/、app/views/ がビルドの入力になります。spin run gen は、コミットされる素の Ruby を gen/ に書き出します。cybertrain build は app/views/ を埋め込み、gen/ とフレームワークをひとつのネイティブバイナリ dist/blog にコンパイルします。このバイナリは HTTP/1.1 で応答し、Spinel の FFI を通じて SQLite とやり取りします。開発環境では、バイナリは代わりに app/views/ をディスクから読み込み、戻りの経路がソースを監視します:監視、再ビルド、停止、execv。書くものdb/schema.rbtablesconfig/routes.rbルートapp/controllers, modelsビルド時spin run gen1gen/素の Ruby・コミット済モデルルート · URLヘルパーview_assignsrun_callbackmigrations · viewsmanifest2cybertrain build実行時dist/blogone native binaryframeworkcontrollers · modelstemplate interpreter埋め込みビューHTTP/1.1 サーバーSQLite via Spinel FFI4app/views/開発: ディスク読込dist/blog に埋め込み3開発:監視 → 再ビルド → 停止 → execv5開発時のみ · ビルドに失敗しても旧サーバーは動き続ける
ビルドの手順開発時のみ:ディスクから読み込み/開発ループバイナリ番号は下の手順に対応しています。
  1. ビルド時に生成する

    spin run gen は db/schema.rb と config/routes.rb を実行してテーブルとルートのデータを取得し、app/controllers と app/models を字句的にスキャン(パーサーではなく正規表現)して、@ivar = の代入、コールバック名、引数なしの def を見つけます。そして読みやすい素の Ruby を gen/ に書き出します。これには埋め込みビューのテーブルである gen/views.rb も含まれます(デプロイ用にビルドするとき以外は空です)。cybertrain db migrate、server、build がこれを代わりに実行します。

  2. gen/ をコミットする

    spin build と spin test には先に生成を行うフックがないため、gitignore せずにリポジトリに含めます。CI は spin run gen を再実行し、差分があれば失敗するので、古い生成コードがマージされることはありません。cybertrain spin run gen -- --check は古くなったファイルを報告します。

  3. ビューを解釈する

    app/views/**/*.html.erb はリクエスト時にパースされ、構文木をたどるインタプリタ(tree-walking interpreter)で評価されます。開発環境ではエンジンが app/views/ をディスクから読み込み、テンプレートが変更されると再パースするので、ビューを編集しても再ビルドは不要です。cybertrain build はすべてのテンプレートをバイナリに埋め込みます(spin run gen -- --embed-views)。本番バイナリはそのテーブルからのみレンダリングし、各テンプレートのパースは一度だけです。Spinel には eval がないため、これは閉じた文法であり、本物の Ruby ではありません。

  4. グリーンスレッドで配信し、SQLite に保存する

    素の Ruby で書かれた HTTP/1.1 サーバーが、接続ごとに Spinel のグリーンスレッドを 1 本実行します。モデルは ffi_func/ffi_lib を通じ、4 接続のプール(WAL、約 5 秒の busy ハンドラ、外部キー有効)で SQLite とやり取りします。SPINEL_WORKERS のデフォルトは 1 です。

  5. 開発環境で再ビルドする

    cybertrain server は spin run gen を実行し、続いて spin run blog(blog はアプリ名)を実行します。開発環境ではバイナリが app/**/*.rb、config/**/*.rb、db/schema.rb、gen/**/*.rb を 0.5 秒ごとにポーリングします(ビューと gen/views.rb は除外)。変更があると spin run gen && spin build blog をバックグラウンドで実行し、成功すると再起動フラグ(外部からの kill -HUP が立てるのと同じフラグ)を立てます。するとサーバーは待ち受けをやめ、処理中のリクエストを捌き切ってから、新しいバイナリを同じポート・同じ PID で execv します。ビルドに失敗した場合は古いバイナリで配信を続け、コンパイラの出力をバナーとして表示します。cybertrain build が tmp/cybertrain-build.lock を保持している間は再ビルドをスキップします。これらはいずれも本番環境では読み込まれません。

ターミナル — blog/shell
cybertrain spin run gen   # db/schema.rb と config/routes.rb を読み、app/ をスキャンして gen/ を書き出す
cybertrain build          # app/views を埋め込んだ dist/blog と、dist/public/, dist/storage/, dist/tmp/

Rails と CyberTrain

語彙は同じ。違うルールは明文化。

AOT コンパイラのために、いくつかの習慣が変わります。ここでは最初に出会うものを挙げます。

Rails と CyberTrain の違い(抜粋)
項目RailsCyberTrain
01コンソールrails consoleコンソールなし — Spinel には eval がない
02リロードコードを編集すると、実行中のアプリに反映されるRuby の変更には再ビルドが必要。開発環境では cybertrain server が代わりに行う(再ビルド、停止、execv)
03new アクションdef newdef new_action (new だと Klass.new(ctx) を覆い隠してしまう)
04コールバックbefore_action { do_thing }(暗黙の self)before_action { |c| c.do_thing } — 保存したブロックに対する instance_exec がないため、コールバックはコントローラー/レコードを明示的に受け取る
05ネストしたルーティングresources :posts do resources :comments endresources :posts do |posts| posts.resources :comments end — ネストしたブロックはマッパーを明示的に受け取る
06セッションセッションにはマーシャリングした任意のオブジェクトを保持できるセッションの値は String のみ、HMAC 署名付き cookie
07データベース多数のデータベースアダプターSQLite のみ、FFI 経由
08テストminitest / RSpecCybertrain::Test — リフレクションに基づくテストランナーは AOT では動かない

すべてを網羅してはいません。

README の完全な表

パフォーマンス

CPU 1 コアなら、Rails の 2.6〜12.2 倍のリクエストをさばく。

examples/blog と、同じブログを Rails 8.1 で書いたものの比較です。ビュー、ルーティング、モデル、SQLite のスキーマは同じ。どちらも本番モードで、1 台のマシンの同じ CPU コアに固定し、wrk で同じ負荷をかけました。2 コア目がよく効くのは Rails のほうです。Puma はプロセスを丸ごと 1 つ増やしますが、CyberTrain の 2 つ目のワーカースレッドで増えるのは 9〜58% なので、2 コアでの差は 2.0〜5.5 倍です。

16 接続での 1 秒あたりのリクエスト数CyberTrainRails 8.1
  • GET /articles一覧、記事 30 件

    CPU 1 コア

    CyberTrain: 2,641

    Rails: 468

    5.6×対 Rails

    CPU 2 コア

    CyberTrain: 3,289

    Rails: 1,036

    3.2×対 Rails

  • GET /articles/1コメント 10 件とフォーム 12 個のある記事

    CPU 1 コア

    CyberTrain: 2,450

    Rails: 201

    12.2×対 Rails

    CPU 2 コア

    CyberTrain: 2,662

    Rails: 486

    5.5×対 Rails

  • POST /articles/2/commentsバリデーション付きの INSERT とリダイレクト

    CPU 1 コア

    CyberTrain: 1,507

    Rails: 390

    3.9×対 Rails

    CPU 2 コア

    CyberTrain: 2,379

    Rails: 870

    2.7×対 Rails

  • GET /style.csspublic/ の静的ファイル

    CPU 1 コア

    CyberTrain: 13,300

    Rails: 5,127

    2.6×対 Rails

    CPU 2 コア

    CyberTrain: 20,589

    Rails: 10,311

    2.0×対 Rails

負荷をかけた後のメモリ(少ないほど良い)CyberTrainRails 8.1
  • システムの malloc既定のアロケーター

    CPU 1 コア

    CyberTrain: 10.6 MB

    Rails: 125.9 MB

    11.9×少ない

    CPU 2 コア

    CyberTrain: 12.0 MB

    Rails: 331.4 MB

    27.6×少ない

  • jemalloc両方に入れた場合

    CPU 1 コア

    CyberTrain: 13.3 MB

    Rails: 119.9 MB

    9.0×少ない

    CPU 2 コア

    CyberTrain: 15.1 MB

    Rails: 313.2 MB

    20.7×少ない

サーバーの全プロセスの常駐メモリ(/proc から読んだ RSS)を合計し、すべての負荷計測のあとに読んだ値です。Rails は 1 コアでは 3 スレッドの Puma プロセス 1 つ、2 コアではワーカー 2 つです。RSS は Puma のワーカー間で共有されるページをワーカーごとに数えます。共有ページをプロセスで按分する PSS で数えると、2 コアの Rails は 249.1 MB(jemalloc では 222.5 MB)で、CyberTrain の 10.2 MB(12.3 MB)に対してなお大きな差があります。

  • 負荷をかけた後のメモリ

    10.6 MB

    Rails は 126 MB(Puma 1 プロセス、3 スレッド)。

  • 起動から最初の応答まで

    0.02 秒

    Rails は 1.3 秒。

  • サーバーに置くもの

    2.5 MB

    バイナリ 1 つ。Rails は Ruby、69 個の gem、アプリで 109 MB。

  • 代わりに要るもの:ビルド

    3 分

    cybertrain build でバイナリをコンパイルする時間。Rails はソースのまま動きます。

ウォームアップの後、10 秒の計測 3 回の中央値です。4 vCPU のクラウド VM で、wrk はサーバーが使わない 2 コアで動かしました。CyberTrain 0.2.1(Spinel 2026.09.12 でビルド)と、Ruby 3.4.6(YJIT 有効)と Puma 8.0 で動く Rails 8.1.4(1 コアでは 3 スレッドのプロセス 1 つ、2 コアでは 2 つ)を比べています。各行の棒は 1 つの目盛りを共有しています。リクエストを 1 つずつ送ると、記事ページの応答は 0.4 ms 対 4.7 ms。jemalloc を両方に入れると(Rails の本番イメージは標準で読み込みます。CyberTrain は spin.toml の 1 行です)、1 コアで 2.8〜14.6 倍です。差の一部は、Rails が標準で行い CyberTrain は行わない処理(セッション Cookie の暗号化と、フォームごとの CSRF トークンの署名)の分です。手法、すべての数値、再実行の方法は docs/benchmark.md にあります。

始める前に

手元のマシンに必要なもの。

Ruby
3.2+ と RubyGems。cybertrain コマンドのためだけに必要です。アプリケーションが CRuby 上で動くことはありません。
Spinel
2026.09.12。他のバージョンでは未テストです。Spinel を必要とする最初のコマンドが ~/.cybertrain にビルドします(数分、初回のみ)。そのリリースの spinel がすでに PATH 上にあれば、それをそのまま使います。
ツールチェーン
cc と make(Spinel はすべてのプログラムを C にコンパイルします)、さらに Spinel のビルド用に git と curl。SQLite 3 のヘッダーとライブラリ:Debian/Ubuntu では libsqlite3-dev、macOS では Xcode コマンドラインツールに含まれています。OpenSSL のヘッダーは任意です。
ステータス
プレアルファ、バージョン 0.2.1、MIT ライセンス。サンプルのブログは Linux と macOS の CI で検証されています。

cybertrain doctor はこれらのツールとヘッダー、Spinel のインストールを確認し、足りないものがあればプラットフォームに応じたインストールコマンドを表示します。cybertrain setup は求めに応じて Spinel をインストールし、spin を自分で実行するための PATH の設定行を表示します。cybertrain spin ARGS... を使えば、その設定なしで実行できます。