2 points par GN⁺ 2025-02-05 | 1 commentaires | Partager sur WhatsApp
  • Un développeur a consacré ses soirées et ses week-ends pendant 6 mois à créer Semblance, une VM WebAssembly en C, en en faisant un projet d’apprentissage au long cours axé sur l’étude de la spécification plutôt qu’un side project vite abandonné
  • WebAssembly est un bytecode standard destiné à exécuter dans le navigateur du code non fiable à une vitesse proche du natif, et son usage hors navigateur progresse aussi, comme chez Fastly, Shopify ou Zed
  • L’objectif de Semblance est d’apprendre la WebAssembly Core Specification et de se préparer à contribuer à des runtimes industriels comme Wasmtime ; l’implémentation de tous les opcodes ou la réussite de la core test suite sont donc hors périmètre
  • L’interpréteur vérifie les magic bytes et la version 1 d’un fichier .wasm, puis fonctionne dans l’ordre suivant : décodage des sections, résolution des imports, instanciation du module, recherche de la fonction exportée, initialisation de la pile et de l’activation frame, puis exécution de l’opcode switch loop
  • Ce n’est pas un runtime complet : il est lent, présente des fuites mémoire et peut rester vulnérable à des modules malveillants, mais l’exécution de Hello, World! a permis d’atteindre l’objectif d’apprentissage de la spécification

Pourquoi choisir WebAssembly

  • WebAssembly est un ensemble d’instructions standard et un format de bytecode pour une machine virtuelle à pile
  • Son point de départ a été le compilateur C/C++ vers JavaScript d’Emscripten
    • Emscripten transformait l’LLVM IR en JavaScript pour permettre d’exécuter du code C et C++ sur le Web
    • Les développeurs de navigateurs et le projet Emscripten ont limité le code généré à un sous-ensemble simple de JavaScript afin d’optimiser les performances, et ce sous-ensemble a été standardisé sous le nom asm.js
    • Par la suite, Wasm a été conçu comme un format de bytecode indépendant afin d’éviter le surcoût de parsing de JavaScript
  • Ces derniers temps, l’usage de WebAssembly hors navigateur augmente également
    • Edge Compute de Fastly et Functions de Shopify sont construits sur un moteur WebAssembly
    • Le système d’extensions de Zed repose aussi sur Wasm
  • La Bytecode Alliance mène un effort intersectoriel pour soutenir l’utilisation de WebAssembly hors navigateur, avec le runtime Wasmtime et la standardisation de WASI
    • WASI permet aux modules WebAssembly de communiquer avec le système d’exploitation hôte via des host calls similaires à POSIX
    • Il fournit une manière standard d’interagir avec des interfaces externes comme l’entrée/sortie standard, le système de fichiers ou le réseau

Du side project au projet d’apprentissage

  • Il existait un schéma récurrent consistant à démarrer de nouveaux side projects, puis à perdre l’intérêt au bout de quelques semaines et à passer à autre chose
  • Il fallait un grand projet permettant de rester concentré plus longtemps et de travailler à un niveau plus bas de la pile informatique que dans le travail quotidien
  • Comme de nombreux side projects avaient été abandonnés faute de direction, l’auteur cherchait un projet avec un objectif clair et un résultat visible
  • Après avoir eu le sentiment qu’un profil d’ingénieur généraliste influençait la recherche d’un poste stable, il cherchait un domaine de spécialisation permettant de construire un jeu de compétences en T
  • La possibilité de concevoir des environnements de calcul personnalisés avec des system calls sur mesure dans WebAssembly semblait intéressante comme outil de platform engineering

Objectif et périmètre de Semblance

  • Le projet s’appelle Semblance ; c’est un interpréteur WebAssembly écrit en C
  • Son objectif principal était de se familiariser avec la WebAssembly Core Specification
  • À plus long terme, l’objectif était aussi d’acquérir assez de connaissances sur WebAssembly pour pouvoir contribuer à des runtimes industriels comme Wasmtime
  • Comme il s’agit d’un projet d’apprentissage, il n’était pas prévu d’implémenter tous les opcodes ni de passer la core test suite
  • Pouvoir exécuter “Hello, World!” constituait un critère de réussite suffisant
  • L’auteur reconnaît ne pas avoir d’expérience professionnelle en développement C, que la qualité du code C n’est pas idéale, et accueille volontiers les code reviews

Flux d’exécution de Hello, World!

  • Le code C d’exemple déclare la fonction externe puts et affiche "Hello, World!\n" dans hello()
  • WebAssembly est généré avec Clang
    • --target=wasm32
    • -nostdlib
    • -Wl,--no-entry
    • -Wl,--export-all
    • -Wl,--allow-undefined
    • -O3
  • Le fichier hello.wasm généré est exécuté avec semblance hello.wasm --invoke hello
  • Au démarrage du programme, la mémoire est initialisée sur la pile pour les arguments de ligne de commande, le module WebAssembly décodé et le store contenant l’état du runtime
  • Les arguments de ligne de commande sont analysés, et le programme se termine s’ils sont invalides

Décodage binaire et structure du module

  • L’interpréteur commence par décoder le format binaire de WebAssembly
  • Il vérifie les magic bytes "\0asm" au début du fichier
  • Il vérifie que la version du format binaire est 1
  • Il décode ensuite les sections incluses
  • Une fois le décodage terminé, la structure WasmModule est remplie avec les fonctions, types, imports, data, etc. de hello.wasm
    • types
    • funcs
    • tables
    • mems
    • globals
    • elems
    • datas
    • start
    • imports
    • exports
    • customs
    • meta

Résolution des imports et env::puts

  • La seule fonction hôte actuellement fournie par Semblance est env::puts
  • Les imports de fonctions hôtes sont traités en assignant un native function pointer dans le store, puis en plaçant le funcaddr généré dans le tableau imports du module
  • hostcall_puts utilise l’offset i32 reçu en argument
    • Il récupère un pointeur data depuis la première instance mémoire du store
    • Il affiche la chaîne située à l’offset sur la sortie standard avec printf("%s", ...)
  • En parcourant la liste des imports, si un élément dont le module name est "env" et l’item name est "puts" est trouvé, la fonction hôte est enregistrée via register_hostcall_puts

Instanciation et exécution des opcodes

  • Après la résolution des imports, le module est instancié
  • L’instanciation inclut une étape de validation
    • Elle effectue des tests pour vérifier les types et s’assurer que le module est correctement formé
    • Elle initialise memory, global et table
    • Si une fonction start existe, elle l’appelle
  • Le résultat de l’instanciation est WasmModuleInst, qui contient la structure d’exécution et les exports prêts à l’emploi
  • La fonction exportée est recherchée à partir du nom indiqué par l’argument de ligne de commande --invoke
    • Les exports sont parcourus pour retourner le WasmExportInst dont le nom correspond
    • Il est vérifié que l’export retourné est bien une function
  • L’appel de fonction nécessite l’initialisation de la stack et de l’activation frame
  • Le cœur de l’exécution est l’opcode switch loop
    • Les Wasm instructions du corps de la fonction sont lues dans l’ordre
    • Selon l’opcode, des opérations sont effectuées sur la stack et le store
    • Par exemple, i32.const, i64.const, f32.const, f64.const, i32.ge_s, call, nop, unreachable, end, etc. sont gérés
    • Les opcodes non pris en charge affichent "unhandled opcode [...]" et retournent un trap

Instruction call et appel de fonction hôte

  • Le traitement de WasmOpCall utilise funcidx, la valeur immédiate de l’opcode call, pour trouver l’instance de fonction dans le store
  • Les arguments nécessaires sont pop de la stack à partir des informations de type de la fonction
  • Le type de l’instance de fonction est vérifié afin de distinguer une fonction Wasm d’une native host function
  • S’il s’agit d’une fonction hôte, le function pointer hostfunc enregistré est exécuté comme native call
  • S’il y a une valeur de retour, elle est push à nouveau sur la stack
  • Lorsque la fonction hello atteint l’opcode end, l’opcode switch loop se termine et l’exécution revient à main

Résultat et limites restantes

  • Le résultat de l’exécution s’affiche ainsi sur la sortie standard
    • Hello, World!
    • Ok []
  • Le projet peut exécuter un simple programme “Hello, World!”
  • La couverture des opcodes n’est pas complète
  • Le code est désordonné, lent et présente des fuites mémoire
  • Il peut être vulnérable à des modules malveillants
  • Malgré cela, l’auteur a beaucoup appris sur la WebAssembly Core Specification, et cela l’a aidé à sortir de sa zone de confort en tant qu’ingénieur
  • Après avoir construit directement un interpréteur, il estime avoir acquis assez de connaissances sur WebAssembly pour contribuer à des runtimes industriels comme Wasmtime
  • Il pourra continuer à ajouter quelques opcodes, mais compte mettre fin à cette phase où le projet est son projet principal

1 commentaires

 
GN⁺ 2025-02-05
Avis sur Hacker News
  • J’ai déjà créé un interpréteur Wasm en Scheme, donc ça me fait plaisir de voir davantage de personnes en implémenter un elles-mêmes.
    Ce n’est pas aussi difficile qu’on pourrait le croire ; je recommande de jeter un œil à la spécification et d’essayer. Il n’est pas nécessaire d’implémenter toutes les instructions : en faire juste assez pour s’amuser suffit largement.

  • Petit conseil d’un autre implémenteur : les spec-tests contiennent beaucoup de formats texte Wasm étranges dont la compilation n’est pas intuitive, mais avec le convertisseur wast2json, on peut produire une description JSON plus simple en plus des fichiers Wasm binaires classiques.

    • C’est aussi l’approche utilisée par Chicory : https://github.com/dylibso/chicory
      Plus vous rendez cette suite de tests rapide à exécuter, plus cela aide la vitesse d’itération et la précision.
      Il nous a fallu un certain temps pour tout faire fonctionner, mais une fois ce cap franchi, nous sommes arrivés très vite au point où nous pouvions exécuter à peu près n’importe quoi. Même si la suite de tests n’est pas exhaustive, elle couvre environ 95 % des cas : https://github.com/WebAssembly/testsuite
    • Je construis aussi un compilateur, et c’est utile.
      J’avais déjà vu ces tests auparavant, mais je ne savais pas comment les utiliser, ce qui était frustrant et déroutant.
  • À propos de l’interprétation directe de Wasm, cet article m’a paru assez intéressant : https://arxiv.org/abs/2205.01183
    Je m’en suis inspiré pour créer https://github.com/peterseymour/winter, et j’ai appris que Wasm n’est pas aussi simple qu’on pourrait le penser.

  • Question de débutant : quand on n’écrit pas directement du code pour la cible, comment fait-on le débogage d’un interpréteur ?
    Je me demande aussi jusqu’où une approche consistant à faire du fuzzing de chaînes d’opcodes peut aider, quelle est l’ampleur des différences pratiques entre un moteur Wasm côté serveur et un moteur basé sur navigateur, et combien de travail il faudrait pour remplacer l’un par l’autre.

  • Approche intéressante et excellent travail.
    Pour ceux qui veulent voir le cœur du projet, il se trouve globalement dans ce fichier : https://github.com/irrio/semblance/blob/main/src/wrun.c
    Avec le recul, je me dis que cela aurait été bien si le projet avait suivi Wasm-C-API (https://github.com/WebAssembly/wasm-c-api) comme interface standard.
    C’est déjà une API C, et la plupart des runtimes Wasm comme Wasmer, V8, wasmi, etc. l’ont adoptée, ce qui permettrait aux développeurs familiers de cette API de tester le projet plus facilement.
    Si l’auteur connaît suffisamment Wasm et souhaite contribuer à Wasmer, les patchs et améliorations sont également les bienvenus.

    • L’auteur semble clairement assez familier avec Wasm, et il est probablement aussi assez au courant pour éviter l’entreprise qui a tenté de déposer la marque WebAssembly.
      Il a parlé de « préoccupations compréhensibles concernant le fait que Wasmer, soutenu par du capital-risque, ait tenté d’enregistrer comme marque WebAssembly, le nom d’une organisation à but non lucratif », ce qui revient à reconnaître la faute.
    • Wasmer avec Installed-Size: 266 MB, c’est quoi ce délire ?
    • À ma connaissance, dire que Wasmer a adopté Wasm-C-API est assez loin de la réalité.
      https://github.com/wasmerio/wasmer/issues/2615 a été laissé à l’abandon puis fermé automatiquement.
  • Question un peu plus polémique : je me demande s’il est prévu d’ajouter des instructions provisoires de tail call.
    Côté spécification Wasm, cela a été rejeté au motif que c’était trop « haut niveau », mais le comité C a lui aussi déjà rejeté une proposition de Dennis Ritchie.
    Malgré tout, je miserais toujours sur Ritchie, et il me semble que Rob Pike allait aussi dans ce sens.
    Sinon, pourquoi aurait-il créé Golang ? Les tail calls ne sont haut niveau que lorsque les appels le sont aussi.

  • Vous devriez jeter un œil à Orca. Je pense que vous pourriez bien y contribuer.
    [0] https://orca-app.dev

  • Moi aussi, j’ai décidé de me concentrer sur un seul projet au lieu de passer d’une nouveauté brillante à une autre, avec une exception : je confie des tâches ingrates à des assistants IA.
    C’est assez frustrant, c’est le moins qu’on puisse dire.
    Et j’allais aussi suggérer d’utiliser une bibliothèque comme libffi pour gérer plusieurs arguments, entre autres, lors des appels depuis C.

  • Quand WebAssembly est utilisé comme API de plugins dans des endroits comme zed, je me demande comment les développeurs de plugins déboguent leur code.
    Par exemple, est-il possible de déboguer avec des points d’arrêt, et peut-on obtenir une trace de pile si le code plante ?