1 points par GN⁺ 2024-09-14 | 1 commentaires | Partager sur WhatsApp
  • Pour faire tourner le moteur de jeu Trial sur Nintendo Switch, le runtime Common Lisp SBCL a été porté pendant deux ans ; il permet désormais de compiler et d’exécuter du code Lisp sur Switch, ainsi que de s’interfacer avec des bibliothèques partagées
  • La Switch offre un environnement ARM64 Cortex-A57 à 4 cœurs, 4 Go de RAM, mais elle ne dispose ni de shell, ni de ligne de commande, ni de compilateur, et empêche aussi la création de pages exécutables, ce qui ne correspond pas à la méthode habituelle de bootstrap de SBCL
  • Le build combine un hôte Linux ARM64 et le Nintendo SDK ; avec fasteval, immobile-code et elfination, le core est séparé en code et données avant d’être regroupé dans le paquet final
  • L’exemple de REPL de Trial crée un contexte OpenGL, gère les entrées et alloue des shaders sur le devkit Switch, mais il crashe lors de l’exécution du garbage collector, et l’audio reste impossible à cause de problèmes de callbacks C
  • Les principaux travaux restants sont la stabilisation des safepoints pour le GC multithread, la restauration des callbacks C, l’évitement de la compilation à l’exécution de CLOS et l’optimisation des performances ; une partie du code ne peut pas être publiée à cause du NDA de Nintendo

Jusqu’où SBCL fonctionne-t-il sur Switch ?

  • Avec Charles Zhang, le portage de SBCL a été mené pendant les deux dernières années afin d’exécuter le moteur de jeu Trial sur Nintendo Switch
  • La difficulté centrale consistait à adapter le runtime Common Lisp utilisé sous Trial à la plateforme Switch
  • Ce qui est possible aujourd’hui :
    • Le runtime et le compilateur ont été portés, ce qui permet de compiler et d’exécuter directement du code Lisp arbitraire sur Switch
    • Il est possible de s’interfacer avec des bibliothèques partagées
    • Plusieurs bibliothèques de portabilité système nécessaires à Trial sur Switch ont également été portées
  • L’exemple de REPL de Trial s’exécute sur le devkit Switch
    • Comme la Switch elle-même n’a pas de terminal, Trial crée un contexte OpenGL, gère les entrées et alloue des shaders pour afficher du texte à l’écran
  • Points encore bloquants :
    • SBCL crashe rapidement lorsqu’il tente de faire fonctionner le garbage collector
    • Certaines contraintes propres à la Switch n’ont pas encore été contournées
    • Le mécanisme de callbacks C est cassé, ce qui rend la sortie audio impossible
    • D’autres problèmes de performance non encore identifiés pourraient subsister

Coûts et viabilité

  • Le travail de portage a coûté jusqu’à présent environ 17 000 dollars, avec des paiements mensuels à Charles Zhang
  • Les revenus proviennent des ventes de Kandria et du soutien via Patreon, GitHub Sponsors et Ko-Fi
    • En bon mois, environ 1 200 dollars
    • En mauvais mois, environ 600 dollars
  • À Zürich, en Suisse, il est difficile de vivre uniquement avec ces revenus ; la situation actuelle consiste à vivre chez ses parents et à réduire les dépenses personnelles pour tenir
  • Comme il est douteux que les ventes de Kandria sur Switch permettent de récupérer le coût du portage, il devient difficile de décider où affecter des ressources limitées
  • Les revenus de sponsoring serviront pour le moment au portage de SBCL sur Switch et au projet de jeu non annoncé actuellement en cours

Environnement Switch et conditions de portage

  • Environnement Switch publiquement connu :
    • Le code utilisateur s’exécute sur une puce ARM64 Cortex-A57
    • Il utilise 4 cœurs et 4 Go de RAM
    • Il tourne au-dessus d’un système d’exploitation propriétaire à microkernel, initialement développé pour la Nintendo 3DS
  • SBCL dispose déjà d’un port ARM64 Linux, donc la génération de code est résolue
  • Kandria tenant facilement dans 4 Go de RAM, la capacité mémoire elle-même n’est pas un problème
  • La vraie difficulté est l’interface avec le système d’exploitation propriétaire de la Switch
    • Il impose des contraintes absentes des systèmes d’exploitation PC classiques
    • C’est particulièrement problématique pour des systèmes comme Lisp
  • La Switch est la seule console à prendre en charge le rendu OpenGL sur lequel Trial est basé
    • La Xbox ne prend en charge que DirectX, mais il existerait une couche OpenGL → DirectX développée par Microsoft, ce qui pourrait ouvrir une possibilité
    • La Playstation est connue pour disposer d’une API graphique entièrement propriétaire, ce qui en fait une plateforme dont le portage reste peu souhaitable
  • Pour le développement, un accès a été obtenu auprès de Nintendo of Europe, et un devkit d’environ 400 dollars a été acheté
  • Le devkit et le SDK ne fonctionnent que sous Windows, ce qui a ensuite alourdi le processus de build

Build SBCL classique et incompatibilités avec la Switch

  • SBCL est principalement écrit en Lisp lui-même, avec aussi un petit runtime C
  • Le runtime C se construit avec un compilateur C classique, mais doit connaître des informations sur l’environnement du système d’exploitation cible
  • Comme le runtime lui-même ne contient pas de compilateur Lisp, bootstrapper SBCL nécessite une autre implémentation Lisp, idéalement une autre version de SBCL
  • Build en 5 étapes sur PC

    • build-config
      • Rassemble les options de configuration de build pour la cible et les émet dans un format lisible par les étapes suivantes
    • make-host-1
      • Construit un compilateur croisé avec le compilateur Lisp hôte
      • Génère aussi des fichiers d’en-tête décrivant sous forme de structures C la disposition mémoire des objets Lisp
    • make-target-1
      • Génère le runtime C avec le compilateur C cible
      • Le runtime C inclut le garbage collector et le code de liaison avec l’environnement du système d’exploitation
      • Génère aussi, depuis les en-têtes du système d’exploitation, les constantes que le compilateur Lisp cible et le runtime doivent connaître
    • make-host-2
      • Construit le système Lisp cible — compilateur et bibliothèque standard — avec le compilateur croisé Lisp créé par make-host-1
      • Génère un cold core dans lequel le runtime peut entrer
    • make-target-2
      • Charge le cold core dans le runtime cible et termine le bootstrap
      • Une fois le système Lisp chargé en mémoire, il est dumpé en warm core
      • Ensuite, il devient possible de charger du nouveau code et de dumper librement de nouvelles images
  • Ce qui pose problème sur Switch

    • Le build SBCL nécessite une étape d’exécution de code Lisp sur la machine cible
    • Le code Lisp utilisateur ne peut pas être traité par simple compilation batch comme du C, et suppose se trouver dans l’environnement cible lorsqu’il s’exécute
    • Le déploiement d’applications ressemble aussi à make-target-2 : on compile progressivement du code Lisp, puis on dumpe un core avec le runtime attaché
    • Au démarrage, le runtime SBCL mappe le blob de core en mémoire, marque les pages de code comme exécutables, puis saute vers une fonction d’entrée personnalisée
    • Sur Switch, toute cette approche pose problème

Stratégie de build pour la Switch

  • La Switch n’est pas un environnement PC et ne dispose ni de shell, ni de ligne de commande, ni de suite de compilateurs
  • Comme le système d’exploitation n’autorise pas la création de pages exécutables, la compilation incrémentale Lisp habituelle serait impossible même si les étapes de build pouvaient être exécutées sur la Switch
  • La majeure partie du code étant indépendante de la plateforme, elle peut être compilée pour ARM64
  • Seules les parties touchant à l’environnement du système d’exploitation environnant doivent savoir que la cible est la Switch, tandis que l’implémentation peut être produite sur un autre environnement ARM64, comme Linux
  • Étapes de build pour la Switch

    • build-config
      • S’exécute sur le système hôte avec des flags spéciaux indiquant un build Switch
      • Active le contrib fasteval
      • Comme la compilation à l’exécution n’est pas possible sur Switch, fasteval remplace les points qui nécessiteraient d’appeler le compilateur
    • make-host-1
      • Peu de changements, génère les en-têtes de préparation pour la plateforme Switch
    • make-target-1
      • Compile en cross-compilation le runtime C pour Switch avec le compilateur C fourni par le Nintendo SDK
      • Comme Switch OS n’est pas compatible POSIX, une cible de runtime personnalisée est créée dans SBCL, et les différences système comme le linking dynamique ou le mapping de pages sont stubbées ou encapsulées
    • Deuxième build-config, make-host-1, make-target-1
      • Crée un système ARM64 Linux ordinaire possédant le même ensemble de fonctionnalités que la Switch
      • Certains processus Lisp utilisent des flags spéciaux indiquant que la cible finale est la Switch
    • make-host-2, make-target-2
      • Produit un build SBCL légèrement spécial pour Linux ARM64, qui sert ensuite à compiler le code utilisateur
    • Compilation du code utilisateur
      • Modifie *features* pour faire croire que l’exécution se fait sur Switch, et non sous Linux
      • Inclut :nx et exclut :linux, :unix, :posix
      • Désactive ASDF, puis compile un programme comme Trial d’une manière proche du processus habituel et dumpe un nouveau core

Packager le core avec immobile-code et elfination

  • Sur Switch, la stratégie habituelle de mapping du core ne peut pas être utilisée, donc attacher un nouveau core au runtime Switch ne fonctionne pas
  • La solution consiste à utiliser deux fonctionnalités relativement peu connues de SBCL : immobile-code et elfination
  • Un SBCL classique place le code compilé à l’exécution dans certaines pages, puis les marque comme exécutables
    • Plus tard, si le code n’est plus nécessaire, il peut devenir éligible au garbage collection
    • L’espace peut être récupéré et le code restant compacté
  • immobile-code utilise une autre stratégie
    • Il place le code dans des pages de code spécialement réservées et l’y maintient
    • Le code ne peut pas être collecté par le garbage collector
    • En contrepartie, on peut s’appuyer sur la prise en charge traditionnelle des exécutables par le système d’exploitation
    • L’exécutable contient des sections préalablement marquées que le système d’exploitation reconnaît comme du code, et celui-ci gère le mapping au lancement du programme
  • L’étape d’elfination transforme le core en sections séparées de code et de données nécessaires à un exécutable classique
    • L’elfinator analyse le core
    • Il réécrit l’assembleur pour produire du code indépendant de la position, nécessaire à la randomisation de l’agencement de l’espace d’adressage
    • Il sépare le tout en un fichier assembleur de code pur et un fichier de payload de données pures
  • Étape finale :
    • Génération du fichier assembleur avec l’elfinator
    • Linkage du binaire final
    • Regroupement par les outils d’authoring du Nintendo SDK des métadonnées, bibliothèques partagées, assets et binaire applicatif dans un paquet final unique

Infrastructure de build et périmètre publiable

  • Le build nécessite au minimum une machine Linux ARM64 pour exécuter la majeure partie du build
  • Pour lancer le compilateur et les outils d’authoring du Nintendo SDK, il faut une machine Windows AMD64 ou une machine Linux AMD64 utilisant Wine
  • La configuration réelle se rapproche d’un total de trois machines
    • Un « driver » AMD64
    • Un hôte de build ARM64
    • Une VM Windows qui communique avec le devkit
  • Un système de build spécial, avec cache et logique de synchronisation entre machines, a été écrit pour automatiser le tout
    • Il devait aussi fonctionner dans un environnement MSYS2/Windows, ce qui a entraîné des problèmes de conversion de chemins
  • L’elfinator et immobile-code ont été rendus fonctionnels sur ARM64, et des bibliothèques de support comme pathname-utils, libmixed et cl-gamepad ont aussi été portées
  • De nombreux détails ne peuvent pas être publiés à cause du NDA Nintendo
  • Le travail publiable a été upstreamé, et les bibliothèques Lisp n’ont pas de forks privés
  • Afin d’éviter une liaison directe avec le Nintendo SDK, une bibliothèque C séparée a été créée, et les bibliothèques Lisp accèdent aux fonctions système via cette interface personnalisée
    • Cette structure permet de publier la partie Lisp tout en gardant privée uniquement la petite bibliothèque C

Pointeurs absolus et relocalisation au chargement

  • L’elfination n’avait pas été conçue à l’origine pour produire du code Lisp exécutable indépendant de la position
  • Le code Lisp contient généralement beaucoup de pointeurs absolus
  • Il a fallu travailler dans le compilateur et le runtime SBCL pour prendre en charge la relocalisation des pointeurs absolus au chargement
  • Les objets de code contiennent généralement des constantes de code, et il fallait faire en sorte qu’ils ne contiennent plus de pointeurs absolus
    • Le GC ne peut pas modifier la section exécutable
    • Le loader de l’OS ne peut pas non plus modifier la section exécutable pour relocaliser des pointeurs absolus
  • Solution retenue :
    • Déplacer les pointeurs absolus, comme les constantes de code, hors de la zone text vers une zone lisible/inscriptible
    • Réécrire les références aux constantes dans le code pour les charger depuis cette zone lisible/inscriptible
    • Le loader et le GC déplaçant peuvent alors modifier les pointeurs de cette zone

Garbage collector et problème des safepoints

  • Le GC standard de SBCL est gencgc, un garbage collector générationnel
  • gencgc sépare les générations d’objets, les scanne à des fréquences différentes et compacte l’espace en copiant les objets vers d’autres emplacements générationnels
  • Cette structure n’est pas problématique en soi sur Switch, mais le multithreading crée une difficulté
  • En présence de plusieurs threads, un thread peut être en train d’accéder à un objet, donc il n’est pas possible de le déplacer arbitrairement
  • La solution la plus simple est d’arrêter tous les threads avant le démarrage du GC
  • Différences entre Unix et Switch

    • Sur les systèmes Unix, le mécanisme des signaux permet d’envoyer un signal aux autres threads pour les faire se parquer
    • La Switch n’a pas de mécanisme de signaux, et il n’est pas non plus possible d’interrompre un thread
    • Il faut faire en sorte que chaque thread remarque de lui-même qu’il doit se parquer ; la stratégie classique est le safepoint
    • Un safepoint consiste pour le compilateur à insérer du code supplémentaire qui vérifie si le thread doit se parquer
    • Comme l’ajout de checks a un coût, il faut en mettre aussi peu que possible
    • Si les checks sont trop rares, le GC ne peut pas commencer avant que tous les threads soient parqués, ce qui bloque les autres threads
    • Si les checks nécessitent beaucoup d’instructions, ils perturbent les lignes de cache CPU et les optimisations de pipelining
  • Limites des safepoints existants de SBCL

    • Le système actuel de safepoints de SBCL a été écrit pour Windows
    • Windows n’a pas non plus de gestionnaire de signaux interprocessus, mais contrairement à la Switch, il dispose d’une gestion des signaux pour le thread courant
    • Approche existante :
      • Chaque thread maintient une page sur laquelle le safepoint écrit un mot
      • Au démarrage du GC, cette page est marquée en lecture seule
      • Lorsqu’un autre thread atteint un safepoint et tente d’écrire sur la page, une segmentation fault se produit et le thread se parque
      • Une seule instruction d’écriture suffit, ce qui est efficace
    • Sur Switch, cette technique ne peut pas non plus être utilisée, il faut donc insérer un check plus complexe
    • Les safepoints n’étaient pas nécessaires sur les plateformes autres que Windows, ils n’y ont donc pas été testés et restent instables indépendamment des modifications pour Switch
    • Cette partie de la base de code pourrait nécessiter un important nettoyage, en espérant ne pas devoir aller jusqu’à une réécriture complète

Éviter la compilation à l’exécution de CLOS

  • CLOS reporte généralement la compilation de la discriminating function nécessaire au dispatch des méthodes jusqu’au premier appel de la fonction générique
  • CLOS est très dynamique, et des méthodes peuvent être ajoutées ou supprimées presque à tout moment, ce qui rend difficile de savoir quand le système est complet
  • Sur Switch, il n’est pas possible d’appeler le compilateur, donc cette compilation différée ne peut pas être conservée telle quelle
  • La stratégie actuelle consiste à s’appuyer sur le fast evaluator
    • La fonction compile est remplacée par un stub
    • Au lieu de compiler, une lambda est créée pour exécuter le code via l’evaluator
    • Cela fonctionne aussi pour le code utilisateur qui dépend de compile
    • L’exécution est beaucoup plus lente qu’avec une vraie compilation

Travaux restants et risques de performance

  • L’approche fasteval sert surtout de fallback
  • Il est envisagé de figer autant que possible l’état CLOS juste avant de dumper l’image finale, et de précompiler autant de code que possible
  • Le mode block compilation que Charles avait restauré il y a quelques années sera aussi examiné plus en détail
  • Le processeur relativement faible de la Switch pourrait nécessiter des optimisations supplémentaires
    • Côté moteur Trial
    • Côté code de Kandria
  • Jusqu’à présent, même des ordinateurs vieux de 10 ans satisfaisaient largement aux exigences du jeu, ce qui a permis de limiter les optimisations
  • Travaux prioritaires :
    • Faire fonctionner complètement le garbage collector
      • Actuellement, il est possible de booter et d’entrer dans la boucle principale de Trial
      • L’échec survient au moment d’atteindre la compaction multi-générationnelle
    • Rétablir les callbacks venant de C
      • La partie concernée de la base de code SBCL contient de nombreuses routines assembleur écrites à la main
      • Des ajustements pourraient être nécessaires pour immobile-code et l’elfination
      • Trial n’a besoin de callbacks que pour la lecture audio via libmixed
    • Réaliser le travail supplémentaire sur les fonctions de portabilité nécessaires avant que la selftest suite de Trial passe entièrement sur Switch

Périmètre de publication limité par le NDA

  • L’ensemble du travail de portage aurait vocation à être publié, mais le NDA Nintendo l’empêche
  • Les parties publiables sont upstreamées ou rendues publiques
  • Une partie du code liée directement au Nintendo SDK ne peut pas être partagée avec des personnes n’ayant pas signé le NDA
  • Toute personne souhaitant publier un jeu Common Lisp sur Nintendo Switch peut obtenir un accès au travail de portage après signature du NDA
  • Des mises à jour mensuelles destinées aux Patrons partageront l’avancement plus en détail

1 commentaires

 
GN⁺ 2024-09-14
Avis de Hacker News
  • Ces dernières semaines, j’ai essayé Trial(https://github.com/Shirakumo/trial) pour tester le développement de jeux en Common Lisp, et c’était très agréable.
    Le fait de pouvoir modifier presque tous les éléments pendant que le jeu tourne est un énorme avantage, donc j’espère que ce portage réussira.

    • Les langages de la famille Lisp semblent bien adaptés à la création de jeux. Le fait de pouvoir évaluer du code de façon interactive sans recompilation change vraiment la donne pour développer des fonctionnalités, avancer par itérations et corriger des bugs.
      Pouvoir conserver l’état de l’application entre deux modifications de code paraît aussi très utile, et Common Lisp semble bien plus rapide que je ne l’imaginais vaguement.
      Personnellement, au-delà du développement de jeux, je trouve que le plus gros inconvénient est surtout la façon assez rugueuse d’utiliser les structures de données, en particulier les maps, mais le compromis semble acceptable.
  • C’est vraiment génial. SBCL est une excellente implémentation du langage, et j’avais envie d’essayer le développement en Common Lisp sur une « vraie » console de jeu.
    Le fait que Shinmera fasse ce travail est aussi une bonne surprise. Je l’avais déjà vue quelques fois sur #lispgames et le Discord Lisp, mais je ne savais pas qu’elle s’intéressait aussi à ce type de développement bas niveau.
    Ayant déjà jeté un coup d’œil aux entrailles de SBCL avant de reculer, intimidé, je trouve ça d’autant plus impressionnant. Je me demande aussi si la combinaison SBCL avec threading et SDL2 fonctionne aujourd’hui sur Raspberry Pi.

    • Le travail côté SBCL n’est pas de moi ; il est entièrement fait par Charles, que j’ai mandaté. Ce dont je m’occupe, c’est la partie portabilité pour que Trial fonctionne dans plusieurs environnements, la structure de build globale et les premiers stubs de runtime.
      Et comme dit plus haut, c’est *her :)
    • Ce ne devrait pas être « her » ?
  • Merci à l’auteur pour cet article intéressant et détaillé. On a l’impression que ce niveau de détail sur les portages console officiels n’apparaît généralement que plusieurs années après la fin de vie d’une console.
    Lire un travail aussi approfondi me rend parfois envieux, en pensant à mon propre boulot où j’utilise des logiciels répétitifs toute la journée.

    • D’après mon expérience à l’époque, même ces outils officiels étaient souvent des assemblages assez bricolés et provisoires ; hormis le support du débogage, les toolchains homebrew modernes pour les anciennes plateformes étaient souvent meilleures.
      Les kits de développement avaient généralement de meilleurs hooks, mais ils avaient aussi l’avantage d’éviter un flux de travail centré sur GDB. Pour avoir connu les deux, je ne suis pas sûr que ce monde mérite tant d’envie.
    • J’étais sur le point de commencer une nouvelle journée avec Ruby on Rails, et je me demandais quel hobby ou projet open source pourrait plus tard mériter que j’y consacre mon attention.
      Peut-être que ce n’est pas un projet qui a besoin de mon attention, mais un projet dont mon attention aura besoin plus tard :D
  • En voyant le passage disant « j’aimerais tout rendre public, mais je ne peux pas à cause du NDA », je me demande pourquoi ils ont utilisé le SDK officiel plutôt qu’un SDK homebrew sans contraintes (https://switchbrew.org/wiki/Setting_up_Development_Environment).
    Pure spéculation, mais c’est peut-être parce que Nintendo n’autorise pas la sortie officielle de jeux compilés avec des SDK tiers.

    • On ne peut pas publier un jeu fait en homebrew ; il faut impérativement utiliser le SDK officiel. En plus, très peu de gens possèdent une Switch jailbreakée, ce qui rendrait le jeu très difficile à lancer en dehors d’un émulateur.
  • Dans le même registre, il y a https://opengoal.dev.
    Pour le contexte, Naughty Dog a utilisé GOAL, son propre langage proche de Lisp, pour créer la série Jak & Daxter sur PS2. Comme ils avaient laissé suffisamment d’informations de débogage, la rétro-ingénierie a été possible, et le projet OpenGOAL l’a menée à bien.
    Aujourd’hui, ces jeux peuvent tourner sur toutes les plateformes vers lesquelles le compilateur GOAL a été porté. À ma connaissance, c’est surtout x86 pour l’instant, et un portage sur Switch serait chouette.

  • Je viens d’acheter Kandria. Je ne joue pas énormément, donc je n’y jouerai sans doute pas beaucoup, mais Shinmera repousse clairement les frontières du monde Lisp, et je pense que ça mérite d’être soutenu.

  • Son travail est vraiment formidable. Comme j’utilise parfois Common Lisp, ce genre de travail me réjouit beaucoup.

  • J’aimerais que des entreprises comme Nintendo ou Sony soutiennent directement ce genre d’efforts. Après tout, c’est une autre manière de créer des jeux pour console, donc de l’IP ; je ne vois pas vraiment quel serait l’inconvénient à lancer au niveau de la plateforme quelque chose comme un Github Accelerator.

    • Il est déjà bien établi que les développeurs de jeux passent, à leurs frais, par toutes les procédures exigées par les détenteurs de plateformes. Ils n’ont pas beaucoup de pouvoir de négociation pour chipoter sur les détails techniques au moment de choisir sur quelle plateforme sortir.
      Nintendo n’a pas besoin de créer de nouvelles incitations pour encourager les sorties sur Switch. Le plus grand incitatif existe déjà : plus de 140 millions d’unités vendues et un taux élevé d’achat de jeux.
      Cela dit, il y a moins de procédures qu’avant. Les systèmes actuels ont pour la plupart convergé vers des architectures CPU et GPU généralistes, et même lorsqu’il y a des différences, elles relèvent souvent du détail cosmétique.
    • Ce genre de soutien existait autrefois, mais la plupart de ce que les gens en ont fait a consisté à porter MAME et d’autres émulateurs, ou à cloner des jeux de l’époque 8 bits et 16 bits.
      C’est comme ça qu’on finit par ne plus pouvoir profiter de bonnes choses.
  • C’est pour lire des articles comme celui-ci que je viens sur HN. Bravo à l’autrice originale et à son collègue. Je sais que c’est impossible, mais ce serait une vraie bénédiction si Nintendo traitait ses systèmes de façon un peu plus ouverte.

  • Comme ce n’est expliqué nulle part, j’ajoute que SBCL signifie « Steel Bank Common Lisp ».
    « Steel Bank Common Lisp(SBCL) est un compilateur Common Lisp haute performance. C’est un logiciel open source/libre sous licence permissive, qui fournit un environnement interactif comprenant, en plus d’un compilateur et d’un système d’exécution pour ANSI Common Lisp, un débogueur, un profileur statistique, un outil de couverture de code et plusieurs extensions. »
    https://www.sbcl.org/