Pré-alphaSpinel 2026.09.12MIT

Des applications web à la Rails, compilées en un seul binaire natif.

CyberTrain est un framework d’applications web à la Rails écrit nativement pour Spinel, le compilateur Ruby AOT de matz. Vous gardez le vocabulaire (resources, before_action, validates, form_with, scaffolds), tandis que cybertrain build compile les contrôleurs, les modèles, les vues et le framework lui-même en un seul binaire natif, sans Rack, sans gems à l’exécution et sans interpréteur Ruby dans l’artefact.

Démarrage rapide

terminal — n’importe quel répertoire, puis blog/shell
gem install cybertrain
cybertrain new blog          # installe Spinel 2026.09.12 la première fois (quelques minutes)
cd blog
cybertrain g scaffold article title:string body:text
cybertrain db migrate
cybertrain server            # http://127.0.0.1:3000
cybertrain build             # production: dist/blog, dist/public/, dist/storage/, dist/tmp/

Nécessite Ruby 3.2+ pour la commande cybertrain uniquement (les applications ne tournent jamais sur CRuby), ainsi que git, make, curl, un compilateur C et les en-têtes SQLite 3 : avant de commencer. Lancez le build avec cd dist && ./blog migrate && ./blog, en définissant CYBERTRAIN_SECRET_KEY_BASE.

  • ServeurHTTP/1.1, sans Rack
  • ExécutionNi gems, ni interpréteur
  • Base de donnéesSQLite via la FFI de Spinel
  • CILinux et macOS

Pour qui, et ce que ce n’est pas

Pour les développeurs Rails qui veulent un seul binaire natif et qui s’accommodent d’un projet jeune et d’un sous-ensemble de Ruby.

  • Pas Roundhouse

    Roundhouse transpile des applications Rails existantes vers Spinel. CyberTrain est un framework natif pour lequel vous écrivez directement votre code, pas une couche de compatibilité.

  • Pas compatible Rails ni Rack

    Aucune compatibilité Rack et aucune gem à l’exécution : uniquement les paquets spin-index propres à Spinel qui compilent avec son sous-ensemble de Ruby.

  • Pas prêt pour la production

    CyberTrain est en pré-alpha. Le blog d’exemple est testé par la CI sous Linux et macOS.

Votre code

Le vocabulaire de Rails, avec les règles de Spinel écrites noir sur blanc.

Contrôleurs, modèles, routes et vues se lisent comme une application Rails. Là où un compilateur AOT impose une différence, elle est minime, explicite et documentée.

  • Routes

    Les blocs resources imbriqués reçoivent le mapper explicitement : articles.resources :comments.

  • Contrôleurs

    new s’écrit new_action : une méthode nommée new masquerait Klass.new(ctx).

  • Vues

    En développement, les templates sont lus depuis le disque, donc modifier une vue ne demande pas de recompilation ; cybertrain build les intègre au binaire. Ils forment une grammaire fermée, pas du Ruby arbitraire.

  • Modèles

    class Article déclare les validations ; les colonnes, les finders et les associations sont générés à partir de 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

Ce qui fonctionne

La partie de Rails dont vous vous servez en premier, écrite nativement pour Spinel.

  • Serveur HTTP natif

    Serveur HTTP/1.1 en Ruby pur, avec un green thread par connexion et des requêtes et réponses typées. Pas de couche Rack ; placez nginx ou Caddy devant pour le TLS.

    TCPServer#accept

  • Routage et contrôleurs

    resources, routes imbriquées, before_action, after_action et rescue_from, générés en Ruby pur à la compilation. Les strong params et le flash suivi d’une redirection fonctionnent comme dans Rails.

    config/routes.rb

  • Sessions, flash, CSRF

    Les sessions par cookie signé HMAC, les messages flash et la protection CSRF sont activés par défaut. Les formulaires portent le jeton automatiquement ; les POST falsifiés reçoivent une 403.

    _cybertrain_session

  • Templates façon ERB

    Les vues ressemblent à de l’ERB Rails, avec layouts, partiels, form_with et link_to. En développement, elles sont lues depuis le disque, donc les modifications ne demandent pas de recompilation. cybertrain build les intègre.

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

  • Modèles SQLite

    Validations, callbacks et associations déduites des clés étrangères. Dialogue avec SQLite via la FFI de Spinel, avec un pool de connexions ; chaque requête utilise des paramètres liés.

    ffi_func · ffi_lib

  • Migrations et scaffolds

    cybertrain generate scaffold écrit la migration, le modèle, le contrôleur et cinq vues. cybertrain db migrate génère, applique les migrations, réécrit db/schema.rb et génère à nouveau.

    db/migrate/*.rb

  • Boucle de dev à chaud

    cybertrain server surveille votre code, recompile en arrière-plan, puis ré-exécute (exec) le nouveau binaire sur le même port et avec le même PID. Un build échoué laisse tourner l’ancien serveur.

    rebuild → stop → execv

  • Tests intégrés

    Écrivez des tests avec Cybertrain::Test et pilotez toute l’application dans le même processus avec un client doté d’une cookie jar. cybertrain spin test compile chaque test et compare (diff) sa sortie.

    test/<name>.rb.expected

Framework et CLI

Un nom, deux parties.

gem install cybertrain installe une commande, pas le framework. Le framework est un paquet Spinel dont votre application dépend et qu’elle compile avec elle. La gem vous fournit la commande cybertrain, qui crée l’application, installe Spinel et lance les outils de Spinel pour vous.

Paquet Spinel

Le framework

La bibliothèque sur laquelle repose votre application.

Contenu

Le serveur HTTP, le routage et les contrôleurs, les sessions et la protection CSRF, l’interpréteur de templates, les modèles SQLite et les migrations, ainsi que le générateur de code derrière spin run gen.

Comment l’obtenir

Vous ne l’installez pas. Le spin.toml de votre application désigne le tag de version v0.2.1 de ce dépôt, et spin lock le récupère dans le cache de spin et épingle le commit dans spin.lock. Rien n’est copié dans votre application.

Où il s’exécute

Dans votre application. Spinel le compile à l’avance avec votre code ; il ne tourne jamais sur CRuby.

Dans dist/blog

Compilé dedansC’est la partie de CyberTrain qui est livrée.

RubyGem

La commande cybertrain

L’outil qui crée, compile et lance votre application.

Contenu

Uniquement la commande cybertrain : new, generate scaffold, db migrate, server, build, spin, setup et doctor. Ni le framework ni Spinel ne sont dans la gem.

Comment l’obtenir

gem install cybertrain. À la première utilisation, elle compile la version épinglée de Spinel, 2026.09.12, dans ~/.cybertrain, sauf si cette version est déjà dans votre PATH.

Où elle s’exécute

Sur votre machine, sous CRuby 3.2 ou plus récent. Elle appelle l’outil spin de Spinel pour vous, pour que vous n’ayez pas à le faire.

Dans dist/blog

Non livréeLe binaire tourne sans Ruby, sans la gem et sans Spinel.

Les deux sont publiés à partir du même tag, leurs versions correspondent donc : la gem 0.2.1 crée des applications qui dépendent du framework v0.2.1. Pour passer une application à une nouvelle version, mettez à jour la gem, faites pointer spin.toml vers le nouveau tag, puis lancez cybertrain spin lock et cybertrain spin run gen.

Fonctionnement

La métaprogrammation a lieu une seule fois, à la compilation.

Spinel compile à l’avance et n’a pas d’eval. Là où Rails recourt à method_missing et à define_method à l’exécution, spin run gen écrit donc du Ruby pur que vous pouvez lire et commiter, et seules les vues sont interprétées.

Le pipeline CyberTraindb/schema.rb, config/routes.rb, app/ et app/views/ alimentent le build. spin run gen écrit du Ruby pur, versionné, dans gen/. cybertrain build intègre app/views/ et compile gen/ et le framework en un seul binaire natif, dist/blog, qui sert du HTTP/1.1 et dialogue avec SQLite via la FFI de Spinel. En développement, le binaire lit plutôt app/views/ depuis le disque, et un chemin de retour surveille les sources : surveillance, recompilation, arrêt, execv.YOU WRITEBUILD TIME · spinRUN TIMEdb/schema.rbtablesconfig/routes.rbroutesapp/controllers, modelsspin run gen1gen/Ruby pur, versionnémodelsroutes · helpers URLview_assignsrun_callbackmigrations · vuesmanifest2cybertrain builddist/blogun binaire natifframeworkcontrollers · modelstemplate interpretervues intégréesHTTP/1.1 serverSQLite via Spinel FFI4app/views/dev : lu sur disqueintégré à dist/blog35dev: watch → rebuild → stop → execven dev uniquement · build échoué garde l’ancien serveur actif Le pipeline CyberTraindb/schema.rb, config/routes.rb, app/ et app/views/ alimentent le build. spin run gen écrit du Ruby pur, versionné, dans gen/. cybertrain build intègre app/views/ et compile gen/ et le framework en un seul binaire natif, dist/blog, qui sert du HTTP/1.1 et dialogue avec SQLite via la FFI de Spinel. En développement, le binaire lit plutôt app/views/ depuis le disque, et un chemin de retour surveille les sources : surveillance, recompilation, arrêt, execv.YOU WRITEdb/schema.rbtablesconfig/routes.rbroutesapp/controllers, modelsBUILD TIMEspin run gen1gen/Ruby pur, versionnémodelsroutes · helpers URLview_assignsrun_callbackmigrations · vuesmanifest2cybertrain buildRUN TIMEdist/blogun binaire natifframeworkcontrollers · modelstemplate interpretervues intégréesHTTP/1.1 serverSQLite via Spinel FFI4app/views/dev : lu sur disqueintégré à dist/blog3dev: watch → rebuild → stop → execv5en dev uniquement · build échouégarde l’ancien serveur actif
étape de builddev uniquement : lecture depuis le disque / boucle de devle binaireLes numéros correspondent aux étapes ci-dessous.
  1. Générer à la compilation

    spin run gen exécute db/schema.rb et config/routes.rb pour obtenir les données des tables et des routes, et effectue un balayage lexical (des regex, pas un parseur) de app/controllers et app/models à la recherche d’affectations @ivar =, de noms de callbacks et de def sans argument. Il écrit du Ruby pur et lisible dans gen/, y compris gen/views.rb, la table des vues intégrées (vide, sauf lors d’un build pour le déploiement). cybertrain db migrate, server et build le lancent pour vous.

  2. Commiter gen/

    Il est versionné, et non ignoré par git, car spin build et spin test n’ont pas de hook pour générer d’abord. La CI relance spin run gen et échoue en cas de diff : du code généré périmé ne peut donc pas être fusionné. cybertrain spin run gen -- --check signale les fichiers périmés.

  3. Interpréter les vues

    Les app/views/**/*.html.erb sont analysés au moment de la requête et parcourus par un interpréteur d’arbre (tree-walking). En développement, le moteur lit app/views/ depuis le disque et réanalyse un template quand il change, donc modifier une vue ne demande pas de recompilation. cybertrain build intègre tous les templates au binaire (spin run gen -- --embed-views) ; un binaire de production ne fait le rendu qu’à partir de cette table et n’analyse chaque template qu’une fois. Spinel n’a pas d’eval, il s’agit donc d’une grammaire fermée, pas de vrai Ruby.

  4. Servir avec des green threads, stocker dans SQLite

    Un serveur HTTP/1.1 en Ruby pur exécute un green thread Spinel par connexion. Les modèles dialoguent avec SQLite via ffi_func/ffi_lib, avec un pool de 4 connexions (WAL, un gestionnaire busy de 5 secondes, clés étrangères activées). SPINEL_WORKERS vaut 1 par défaut.

  5. Recompiler en développement

    cybertrain server lance spin run gen, puis spin run blog (le nom de votre application). En développement, le binaire inspecte app/**/*.rb, config/**/*.rb, db/schema.rb et gen/**/*.rb toutes les demi-secondes (hors vues et gen/views.rb). Un changement lance spin run gen && spin build blog en arrière-plan ; en cas de succès, un indicateur de redémarrage est levé (celui que lève un kill -HUP externe), puis le serveur cesse d’écouter, termine les requêtes en cours et fait un execv du nouveau binaire sur le même port et avec le même PID. Un build échoué continue de servir l’ancien binaire et affiche la sortie du compilateur dans un bandeau. Les recompilations sont ignorées tant que cybertrain build détient tmp/cybertrain-build.lock. Rien de tout cela n’est chargé en production.

terminal — blog/shell
cybertrain spin run gen   # lit db/schema.rb + config/routes.rb, analyse app/, écrit gen/
cybertrain build          # dist/blog avec app/views intégré, plus dist/public/, dist/storage/, dist/tmp/

Rails vs CyberTrain

Même vocabulaire. Des règles différentes, écrites noir sur blanc.

Un compilateur AOT change quelques habitudes. Voici celles que vous rencontrerez en premier.

Différences entre Rails et CyberTrain (version condensée)
SujetRailsCyberTrain
01Consolerails consolePas de console — Spinel n’a pas d’eval
02RechargementModifiez le code, l’application en cours d’exécution le prend en compteLe Ruby demande une recompilation ; en développement, cybertrain server s’en charge pour vous (recompilation, arrêt, execv)
03L’action newdef newdef new_action (new masquerait Klass.new(ctx))
04Callbacksbefore_action { do_thing } (self implicite)before_action { |c| c.do_thing } — pas d’instance_exec sur un bloc stocké, donc les callbacks reçoivent explicitement le contrôleur/l’enregistrement
05Routes imbriquéesresources :posts do resources :comments endresources :posts do |posts| posts.resources :comments end — les blocs imbriqués reçoivent le mapper explicitement
06SessionsLa session contient n’importe quel objet sérialisé (marshal)Valeurs de session de type String uniquement, cookie signé HMAC
07Base de donnéesDe nombreux adaptateurs de base de donnéesSQLite uniquement, via FFI
08Testsminitest / RSpecCybertrain::Test — les runners fondés sur la réflexion ne peuvent pas fonctionner avec une compilation AOT

Liste non exhaustive.

Tableau complet dans le README

Performances

Sur un cœur, 2,6 à 12,2 fois plus de requêtes que Rails.

examples/blog face au même blog écrit en Rails 8.1 : mêmes vues, routes, modèles et schéma SQLite. Les deux tournent en mode production, épinglés sur les mêmes cœurs d’une même machine, sous la même charge envoyée par wrk. Un second cœur profite surtout à Rails : Puma ajoute tout un processus, alors que le second thread de travail de CyberTrain n’ajoute que 9 à 58 %, si bien que sur deux cœurs l’écart est de 2,0 à 5,5 fois.

Requêtes par seconde avec 16 connexionsCyberTrainRails 8.1
  • GET /articlesla liste, 30 articles

    Un cœur

    CyberTrain: 2 641

    Rails: 468

    5,6×face à Rails

    Deux cœurs

    CyberTrain: 3 289

    Rails: 1 036

    3,2×face à Rails

  • GET /articles/1un article avec 10 commentaires et 12 formulaires

    Un cœur

    CyberTrain: 2 450

    Rails: 201

    12,2×face à Rails

    Deux cœurs

    CyberTrain: 2 662

    Rails: 486

    5,5×face à Rails

  • POST /articles/2/commentsune insertion validée et une redirection

    Un cœur

    CyberTrain: 1 507

    Rails: 390

    3,9×face à Rails

    Deux cœurs

    CyberTrain: 2 379

    Rails: 870

    2,7×face à Rails

  • GET /style.cssun fichier statique de public/

    Un cœur

    CyberTrain: 13 300

    Rails: 5 127

    2,6×face à Rails

    Deux cœurs

    CyberTrain: 20 589

    Rails: 10 311

    2,0×face à Rails

Mémoire après les passes de charge (moins, c’est mieux)CyberTrainRails 8.1
  • malloc du systèmel’allocateur par défaut

    Un cœur

    CyberTrain: 10,6 Mo

    Rails: 125,9 Mo

    11,9×de moins

    Deux cœurs

    CyberTrain: 12,0 Mo

    Rails: 331,4 Mo

    27,6×de moins

  • jemallocpréchargé dans les deux

    Un cœur

    CyberTrain: 13,3 Mo

    Rails: 119,9 Mo

    9,0×de moins

    Deux cœurs

    CyberTrain: 15,1 Mo

    Rails: 313,2 Mo

    20,7×de moins

Mémoire résidente (RSS, lue dans /proc) additionnée sur tous les processus du serveur, relevée après toutes les passes de charge. Rails tourne en un processus Puma à 3 threads sur un cœur et en deux workers sur deux. Le RSS compte une page partagée entre les workers de Puma une fois par worker ; en PSS, qui répartit les pages partagées entre les processus, Rails sur deux cœurs occupe 249,1 Mo (222,5 Mo avec jemalloc) contre 10,2 Mo (12,3 Mo) pour CyberTrain.

  • Mémoire sous charge

    10,6 Mo

    Rails : 126 Mo, un processus Puma à 3 threads.

  • Du lancement à la première réponse

    0,02 s

    Rails : 1,3 s.

  • Ce qu’il faut sur le serveur

    2,5 Mo

    Un seul binaire. Rails : 109 Mo de Ruby, de 69 gems et de l’application.

  • Le prix : une compilation

    3 min

    pour compiler le binaire avec cybertrain build. Rails exécute ses sources telles quelles.

Médianes de trois mesures de 10 secondes après un échauffement, sur une VM cloud à 4 vCPU, wrk tournant sur les deux cœurs que le serveur n’utilise pas : CyberTrain 0.2.1 compilé par Spinel 2026.09.12, face à Rails 8.1.4 sur Ruby 3.4.6 avec YJIT et Puma 8.0 (un processus à 3 threads sur un cœur, deux sur deux cœurs). Les barres d’une même ligne partagent une échelle. Une requête à la fois, la page d’un article répond en 0,4 ms contre 4,7 ms. Avec jemalloc des deux côtés (l’image de production de Rails le précharge ; pour CyberTrain, c’est une ligne dans spin.toml), un cœur donne 2,8 à 14,6 fois. Une partie de l’écart vient de ce que Rails fait par défaut et CyberTrain non : il chiffre le cookie de session et signe un jeton CSRF par formulaire. La méthode, tous les chiffres et comment relancer la mesure : docs/benchmark.md.

Avant de commencer

Ce qu’il vous faut sur votre machine.

Ruby
3.2+ avec RubyGems, uniquement pour la commande cybertrain. Les applications ne tournent jamais sur CRuby.
Spinel
2026.09.12, non testé avec d’autres versions. La première commande qui en a besoin le compile dans ~/.cybertrain (quelques minutes, une seule fois) ; un spinel de cette version déjà présent dans le PATH est utilisé tel quel.
Outils
cc et make (Spinel compile chaque programme en C), plus git et curl pour compiler Spinel. En-têtes et bibliothèque SQLite 3 : libsqlite3-dev sous Debian/Ubuntu, inclus avec les Command Line Tools de Xcode sous macOS. Les en-têtes OpenSSL sont facultatifs.
Statut
Pré-alpha, version 0.2.1, sous licence MIT. Le blog d’exemple est testé par la CI sous Linux et macOS.

cybertrain doctor vérifie ces outils et en-têtes ainsi que l’installation de Spinel, et affiche la commande d’installation pour votre plateforme quand l’un d’eux manque. cybertrain setup installe Spinel à la demande et affiche la ligne PATH pour lancer spin vous-même ; cybertrain spin ARGS... le lance sans cette ligne.