23 points par GN⁺ 2025-06-08 | 1 commentaires | Partager sur WhatsApp
  • Décrit en détail, à partir d’un exemple concret de solveur de Rubik’s Cube, le processus de création d’une web app exécutée dans le navigateur en portant du code C/C++ vers WebAssembly avec Emscripten
  • Traite étape par étape des difficultés concrètes et des solutions rencontrées dans l’environnement navigateur/WebAssembly, de Hello World au multithreading, aux callbacks, au stockage persistant et à la modularisation
  • Met l’accent sur le dépannage en conditions réelles, notamment autour de l’initialisation asynchrone en JavaScript, de l’export de fonctions, de Web Worker et des problèmes liés à Spectre, ainsi que de la persistance via IndexedDB avec IDBFS
  • Souligne à plusieurs reprises que les abstractions d’Emscripten “fuient” souvent en pratique (leaky abstractions), et insiste sur la nécessité de comprendre les limites et la structure interne de la plateforme web
  • Constitue un guide fondé sur l’expérience apportant une aide concrète et des savoir-faire utiles aux développeurs qui veulent porter vers le web des bibliothèques C/C++ existantes, à travers une expérience pratique de migration d’une base de code C complexe avec seulement des connaissances minimales en JavaScript/HTML côté frontend

Introduction

  • Un projet récent consistait à implémenter sous forme de web app un algorithme de solution optimale du Rubik’s Cube
  • Le texte retrace le processus de compilation avec Emscripten d’un solveur optimisé de Rubik’s Cube développé en C pour l’exécuter dans un navigateur web via WebAssembly
  • La principale raison d’utiliser WebAssembly est de pouvoir obtenir sur le web des performances proches du natif par rapport à JavaScript
  • Cet article n’est pas un tutoriel web classique, mais un parcours semé d’embûches destiné aux développeurs souhaitant porter du code C/C++ existant vers le web
  • Même sans grande expérience en développement web, il est possible de suivre avec les bases de HTML, JavaScript et l’usage des outils de développement du navigateur

Mise en place de l’environnement

  • Tous les exemples de code sont disponibles dans le dépôt git et sur github
  • L’installation de Emscripten est nécessaire (voir le site officiel pour la procédure) ; comme serveur web, on peut utiliser darkhttpd ou Python http.server
  • Les exemples du tutoriel ont été testés sous Linux et d’autres systèmes de type UNIX. Pour les utilisateurs de Windows, WSL (Windows Subsystem for Linux) est recommandé

Hello World

  • Si l’on compile un Hello World en C avec la commande emcc -o index.html hello.c, trois fichiers sont générés : index.html (page web), index.wasm (bytecode WebAssembly) et index.js (code glue JavaScript)
  • L’ensemble peut s’exécuter dans le navigateur ou dans Node.js, avec des usages différents selon l’environnement
  • Pour ne générer que le fichier .wasm, il faut utiliser l’option -sSTANDALONE_WASM
  • Emscripten peut produire uniquement le .wasm, mais dans la plupart des cas le code glue JavaScript reste indispensable

Intermezzo I : Qu’est-ce que WebAssembly ?

  • WebAssembly (WASM) est un langage de bas niveau exécuté dans une machine virtuelle haute performance au sein du navigateur web
  • WASM est pris en charge par tous les principaux navigateurs depuis 2017
  • À l’origine, Emscripten transformait le code C/C++ en asm.js, un sous-ensemble de JavaScript, avant de basculer vers WASM avec son arrivée
  • Il existe aussi une représentation textuelle, et sa structure est basée sur une pile. Jusqu’à récemment, seule l’architecture 32 bits était supportée, ce qui empêchait d’utiliser plus de 4 Go de mémoire, mais WASM64 est progressivement adopté par les navigateurs

Compilation d’une bibliothèque

  • Présente un exemple de base où une fonction C multiply() est compilée en WASM puis appelée depuis JavaScript
  • Lors d’une compilation par défaut, Emscripten ajoute un underscore (_) au nom des fonctions (par exemple _multiply)
  • Pour exposer une fonction à l’extérieur, il faut utiliser l’option -sEXPORTED_FUNCTIONS
  • Comme l’initialisation du chargement de la bibliothèque est asynchrone, il faut gérer l’asynchronisme avec onRuntimeInitialized ou await
  • Le code de démonstration se trouve dans le dossier 01_library du dépôt

Intermezzo II : JavaScript et le DOM

  • Pour accéder aux éléments HTML et les modifier depuis JavaScript, il faut utiliser le Document Object Model (DOM)
  • Il est possible de construire une interface dynamique avec des écouteurs d’événements (addEventListener) et des opérateurs/fonctions intégrés
  • Le texte explique, à partir d’exemples, une structure simple d’intégration HTML/JavaScript avec champ de saisie, bouton et affichage de résultat
  • Il aborde aussi des méthodes pratiques pour séparer ou fusionner les scripts et les problèmes associés (par exemple l’usage de defer et l’ordre de chargement des éléments du DOM)

Modularisation et chargement de la bibliothèque

  • Pour inclure plusieurs bibliothèques WASM ou les réutiliser à la fois dans Node.js et sur le web, on peut compiler sous forme de module avec les options MODULARIZE et EXPORT_NAME
  • L’extension .mjs (module ES6) est recommandée pour la compatibilité avec Node.js
  • Le module peut être utilisé aussi bien sur le web que dans Node via une syntaxe du type import MyLibrary from ...

Multithreading

  • En WebAssembly, il est possible de porter du code multithread basé sur pthreads afin d’améliorer les performances
  • Plusieurs threads sont créés dans une fonction pour exécuter des calculs en parallèle (par exemple compter des nombres premiers)
  • À la compilation, les options -pthread et -sPTHREAD_POOL_SIZE= sont nécessaires
  • Dans les navigateurs réels, il faut aussi ajouter des en-têtes HTTP comme Cross-Origin-Opener-Policy: same-origin et Cross-Origin-Embedder-Policy: require-corp
  • Tous les exemples sont disponibles dans le dossier 03_threads du dépôt

Intermezzo III : Web Workers et Spectre

  • Le multithreading d’Emscripten est implémenté via des Web Workers (les Web Workers fonctionnent comme des processus séparés avec une communication par messages)
  • L’utilisation de mémoire partagée (SharedArrayBuffer) est soumise à des contraintes de sécurité
  • Après la découverte de la vulnérabilité Spectre en 2018, les exigences d’isolation cross-origin (cross-origin isolated) et les en-têtes associés sont devenus obligatoires

Attention au blocage du thread principal

  • Si une tâche longue bloque le thread principal de l’interface du navigateur, l’expérience utilisateur se dégrade fortement
  • Pour l’éviter, il faut introduire un web worker afin de séparer clairement le traitement de l’UI/des entrées de celui des calculs
  • La communication entre le thread principal et le worker se fait via des événements avec postMessage et onmessage
  • Le module Emscripten-WASM est chargé dans le web worker pour prendre en charge uniquement les traitements asynchrones

Fonctions de callback

  • Lorsqu’on passe un pointeur de fonction (callback) en paramètre d’une fonction C, il n’est pas possible de l’associer automatiquement à un objet fonction JavaScript
  • Il faut utiliser notamment addFunction() et UTF8ToString() fournis par Emscripten, et ajouter à la compilation les options -sEXPORTED_RUNTIME_METHODS et -sALLOW_TABLE_GROWTH
  • Pour fonctionner de façon stable, les callbacks doivent impérativement être appelés uniquement depuis le thread principal (ils ne sont pas accessibles depuis un web worker)

Stockage persistant

  • Pour stocker durablement des données dans le navigateur de l’utilisateur, on utilise IDBFS (système de fichiers basé sur IndexedDB) fourni par Emscripten
  • La configuration initiale nécessite à la compilation des drapeaux comme --lidbfs.js et --pre-js
  • Côté code C, on peut continuer à utiliser directement les fonctions d’entrée/sortie de fichiers (fopen, fread, fwrite), mais l’application réelle des données et leur synchronisation exigent impérativement un mapping explicite et un traitement de sync en JavaScript
  • En raison du sandbox et des politiques de sécurité du navigateur, l’accès direct au système de fichiers local n’est possible que dans Node.js ; dans le navigateur, il faut passer par un backend comme IDBFS pour un stockage persistant sûr

Conclusion

  • L’ensemble du tutoriel permet d’apprendre en détail une méthode concrète pour exécuter dans le navigateur, de manière sûre et sans perte notable de performances, du code natif C/C++ complexe avec seulement un minimum de JavaScript et de HTML
  • En environnement réel, on y découvre les principales difficultés et solutions autour du multithreading, des callbacks, du traitement asynchrone et de l’intégration du stockage, tout en se familiarisant avec les réglages nécessaires et les contraintes actuelles des navigateurs
  • Les exemples du dépôt Git fournis peuvent servir de base pour une application et une extension à ses propres projets

1 commentaires

 
GN⁺ 2025-06-08
Avis Hacker News
  • J’aurais aimé qu’on souligne le passage de l’extension .js à .mjs ; en pratique, on finit par se heurter à des problèmes quel que soit le choix, et en ayant connu divers systèmes de modules, de dojo à CommonJS, AMD, ESM, webpack, esbuild ou rollup, je compatis à 100 % avec ce constat vécu
    • La transition de commonjs vers esm a été un bouleversement énorme, un peu comme le passage de Python 2 à Python 3, mais pour des bénéfices finalement modestes au regard des attentes, avec surtout plus de complications ; comme beaucoup de bibliothèques ne prennent désormais en charge que esm, on en arrive aujourd’hui à aller dans l’onglet versions de npm pour choisir la version la plus téléchargée sur le dernier mois, avec de fortes chances que ce soit la dernière version commonjs ; esm est clairement un système de modules plus avancé, mais j’ai du mal à comprendre pourquoi tc39 l’a rendu presque délibérément incompatible avec commonjs, notamment avec des éléments comme le top-level await
    • L’histoire des modules en JS relève presque du traumatisme ; maintenant que les import maps arrivent aussi dans le navigateur, je me demande quels nouveaux problèmes « amusants » vont encore apparaître
    • J’ai récemment découvert que l’objet Function peut compiler n’importe quel code JS à l’exécution, et comme je suis dans un environnement où je ne peux même pas utiliser import, c’est devenu une sorte de bouée de sauvetage extrêmement utile ; ce n’est peut-être pas nécessaire dans l’écosystème JS en général, mais pour moi c’est d’une grande aide
    • Donc tout le monde devrait utiliser bun.sh
    • On ne pourrait pas aussi utiliser .esm.js ?
  • S’il faut pointer un autre aspect de cet article qui risque de poser problème à long terme, je recommanderais d’utiliser let ou const au lieu du mot-clé var ; var fonctionne toujours, mais aujourd’hui la plupart des développeurs JS interdisent son usage via leur linter ; comme var ne gère que la portée de fonction, c’est un point qui finit presque toujours par semer la confusion chez les développeurs venant d’autres langages ; côté portage d’apps natives, un exemple cité est celui de raccourcis Ctrl-C / Ctrl-V codés en dur à la compilation, qui marchent sous Linux et Windows mais pas sur Mac ; sur le web, il faut gérer cela en détectant les événements copy et paste, et j’ai même vu des frameworks comme Unity où le copier-coller ne fonctionne pas sur Mac à cause de touches codées en dur ; ce n’est pas important dans la plupart des jeux, mais dès qu’on exporte vers le web une fonctionnalité qui a besoin du copier-coller, cela devient un vrai problème
  • Petit coup de gueule contre le multithreading sur le web/NodeJS : dommage qu’au lieu de primitives de synchronisation comme des mutex ou rwlock permettant de rendre les valeurs elles-mêmes transmissibles entre contextes (par ex. des isolates v8), on ait surtout introduit un SharedArrayBuffer presque inutile dans la pratique ; au final, la synchronisation entre threads revient à faire du thunking et des copies de données via une couche RPC ; notre application de production en entreprise est un énorme logiciel qui consomme 70 à 100 Go de RAM (c’était déjà le cas avant mon arrivée), donc on a fini par trouver une solution étrange basée sur du code natif qui gère directement les pages mémoire et des structures de données personnalisées afin de minimiser la sérialisation et la désérialisation, mais comme v8 encode les chaînes en utf16, manipuler des valeurs JS depuis la couche native coûte cher
    • Avec 100 Go de RAM, on peut se demander si cette application devait vraiment être une web app ; cela ressemble plutôt à un outil interne qui aurait dû être écrit dans un langage comme C#
  • L’écosystème est tellement proche du chaos que le terme « masochiste » paraît presque le plus raisonnable
    • On peut même dire que le chaos est déjà implicite
  • L’article lui-même est bien écrit, et je suis d’autant plus surpris par le fait d’avoir commencé par une voie difficile et complexe ; on ressent bien que le paramétrage du projet est la partie la plus pénible ; bravo aussi d’être tombé directement sur des problèmes de sécurité et d’en-têtes, même si dans ce genre de situation on s’attend souvent à ce que ce soit le CORS ; dans mon entreprise, on build aussi avec emscripten/C++, en y ajoutant WebGPU/shaders et WebAudio, donc la route s’annonce encore plus corsée
  • Avant, j’imaginais vaguement que compiler du code dans le navigateur serait « lent », mais l’auteur explique bien que ce n’est pas le cas ; le projet Emscripten souligne lui aussi que « grâce à la combinaison de LLVM, Emscripten, Binaryen et WebAssembly, le résultat est compact et s’exécute à une vitesse proche du natif » (emscripten.org)
    • Aujourd’hui, c’est pour moi une sorte de « syndrome du bus jaune » : jusqu’à la semaine dernière je ne connaissais pas Emscripten, puis en branchant SDL sur un projet je suis tombé dans CMake sur des commentaires mentionnant les cibles APPLE, MSVC et EMSCRIPTEN, et voilà qu’aujourd’hui encore je retombe sur Emscripten sur HN ; il est sans doute temps que j’y consacre vraiment du temps et que j’approfondisse le sujet
    • L’expression « vitesse presque native » me semble quand même assez subjective ; je n’ai pas trouvé de données chiffrées dans la documentation pour savoir ce que cela représente réellement
  • Article instructif ; de mon côté, j’aimerais aussi compiler en WebAssembly un compilateur écrit en C pour en faire un playground web ; à propos, les navigateurs récents permettent d’utiliser SQLite via JavaScript, et je me demande si cela fonctionne aussi depuis du wasm ; si emscripten pouvait faire le pont entre les appels à l’API sqlite en C et la base SQLite du navigateur, ce serait idéal, donc cela vaut la peine d’en savoir plus
  • Je me demande pourquoi le port 48 a été choisi pour SSL ; y a-t-il une raison particulière ?
    • Réponse : c’est un port choisi au hasard à partir du nom H48 ; cette web app avait besoin d’en-têtes HTTP supplémentaires, donc l’auteur a simplement utilisé un autre port pour l’implémenter sans impacter le reste du site ; il y a aussi une redirection vers https://h48.tronto.net, et il réfléchit soit à améliorer plus tard la configuration de httpd et relayd d’OpenBSD, soit à déplacer le tout vers un domaine séparé