Skip to content

Latest commit

 

History

History
929 lines (690 loc) · 31.3 KB

File metadata and controls

929 lines (690 loc) · 31.3 KB

🔝 Retour au Sommaire

43.4.2 — Compilation et intégration JavaScript

Module 15 : Interopérabilité · Niveau Expert


Introduction

La section précédente a installé et configuré Emscripten. Cette section couvre le workflow complet : compiler un projet C++ vers WebAssembly, appeler des fonctions C++ depuis JavaScript (et inversement), exposer des classes C++ complètes via Embind, utiliser le système de fichiers virtuel, gérer les appels asynchrones, et déployer le résultat en production.

Le fil conducteur est le même que dans les sections Python (43.2) et Rust (43.3) : le code C++ métier reste inchangé ; la couche d'intégration est séparée et déclarative. La différence est la cible d'exécution — un navigateur sandboxé plutôt qu'un processus natif.


43.4.2.1 — Compilation de base : du C++ au navigateur

Programme minimal

// main.cpp
#include <cstdio>
#include <vector>
#include <numeric>
#include <cmath>

int main() {
    std::vector<double> data = {1.0, 2.0, 3.0, 4.0, 5.0};
    double mean = std::accumulate(data.begin(), data.end(), 0.0) / data.size();

    double variance = 0.0;
    for (double x : data) {
        variance += (x - mean) * (x - mean);
    }
    variance /= data.size();

    std::printf("Mean: %.2f\n", mean);
    std::printf("Variance: %.2f\n", variance);
    std::printf("Std Dev: %.2f\n", std::sqrt(variance));

    return 0;
}

Ce programme C++ standard, sans aucune modification, compile vers WebAssembly :

# Compilation avec page HTML de test
em++ -std=c++20 -O2 -o stats.html main.cpp

# Fichiers produits
ls stats.*
# stats.html  stats.js  stats.wasm

Le fichier stats.html est une page autonome avec une console Emscripten qui affiche la sortie de printf. Le fichier stats.js contient le code de glue qui charge le module .wasm, configure la mémoire et les imports, et lance main(). Le fichier stats.wasm contient le code machine WebAssembly.

Compilation pour intégration dans une page existante

En production, on ne veut pas la page HTML d'Emscripten mais un module .js + .wasm intégrable dans sa propre interface :

# Produire uniquement le JS + Wasm (pas de HTML)
em++ -std=c++20 -O2 -o stats.js main.cpp \
    -sMODULARIZE=1 \
    -sEXPORT_NAME=createStatsModule

# Résultat : stats.js + stats.wasm

Le flag -sMODULARIZE=1 encapsule le module dans une factory function au lieu de l'exécuter immédiatement. -sEXPORT_NAME=createStatsModule donne un nom à cette factory. Le chargement devient explicite côté JavaScript :

<!-- index.html -->
<script src="stats.js"></script>
<script>
    createStatsModule().then(Module => {
        console.log("Module chargé, main() a été exécuté");
    });
</script>

Module ES6

Pour les projets web modernes utilisant des bundlers (Vite, Webpack, Rollup) :

em++ -std=c++20 -O2 -o stats.mjs main.cpp \
    -sMODULARIZE=1 \
    -sEXPORT_ES6=1 \
    -sEXPORT_NAME=createStatsModule
// app.js (ES6 module)
import createStatsModule from './stats.mjs';

const Module = await createStatsModule();

Le suffixe .mjs et le flag -sEXPORT_ES6=1 produisent un module ES6 standard, importable avec import.


43.4.2.2 — Exposer des fonctions C++ à JavaScript

Le mécanisme fondamental : extern "C" + EMSCRIPTEN_KEEPALIVE

Comme pour toute interopérabilité C++ (section 43.1), la frontière passe par extern "C". La spécificité Emscripten est l'attribut EMSCRIPTEN_KEEPALIVE, qui empêche le linker d'éliminer la fonction lors de l'optimisation (le dead code elimination de LLVM supprime les fonctions non référencées par main()) :

// mathlib.cpp
#include <emscripten.h>
#include <cmath>
#include <cstdint>

extern "C" {

EMSCRIPTEN_KEEPALIVE  
double compute_distance(double x1, double y1, double x2, double y2) {  
    double dx = x2 - x1;
    double dy = y2 - y1;
    return std::sqrt(dx * dx + dy * dy);
}

EMSCRIPTEN_KEEPALIVE  
int32_t fibonacci(int32_t n) {  
    if (n <= 1) return n;
    int32_t a = 0, b = 1;
    for (int32_t i = 2; i <= n; ++i) {
        int32_t tmp = a + b;
        a = b;
        b = tmp;
    }
    return b;
}

EMSCRIPTEN_KEEPALIVE  
double array_mean(const double* data, int32_t length) {  
    if (length <= 0) return 0.0;
    double sum = 0.0;
    for (int32_t i = 0; i < length; ++i) {
        sum += data[i];
    }
    return sum / length;
}

}
em++ -std=c++20 -O2 -o mathlib.js mathlib.cpp \
    --no-entry \
    -sEXPORTED_RUNTIME_METHODS=ccall,cwrap \
    -sALLOW_MEMORY_GROWTH=1 \
    -sMODULARIZE=1 \
    -sEXPORT_NAME=createMathLib

Le flag --no-entry indique qu'il n'y a pas de fonction main() — c'est une bibliothèque, pas un programme autonome.

Appel depuis JavaScript : ccall et cwrap

Emscripten fournit deux API JavaScript pour appeler les fonctions C exportées :

ccall — appel direct, une fois :

const Module = await createMathLib();

// ccall(funcName, returnType, argTypes, args)
const dist = Module.ccall('compute_distance', 'number',
    ['number', 'number', 'number', 'number'],
    [0, 0, 3, 4]);
console.log(dist);  // 5

const fib = Module.ccall('fibonacci', 'number', ['number'], [10]);  
console.log(fib);   // 55  

cwrap — crée un wrapper réutilisable :

const Module = await createMathLib();

// cwrap(funcName, returnType, argTypes) → function
const computeDistance = Module.cwrap('compute_distance', 'number',
    ['number', 'number', 'number', 'number']);

const fibonacci = Module.cwrap('fibonacci', 'number', ['number']);

// Appels multiples sans re-spécifier les types
console.log(computeDistance(0, 0, 3, 4));  // 5  
console.log(computeDistance(1, 1, 4, 5));  // 5  
console.log(fibonacci(20));                // 6765  

cwrap est préféré pour les fonctions appelées fréquemment — il évite la résolution des types à chaque appel.

Types supportés par ccall/cwrap

Type C++ Type ccall/cwrap Notes
int32_t, int, bool 'number' Tous les entiers et flottants
float, double 'number' Pas de distinction int/float côté JS
const char* 'string' Conversion automatique JS string ↔ C string
void null Retour sans valeur
int32_t*, double* 'number' Adresse dans le heap Wasm (voir section mémoire)

Passer des tableaux : accès à la mémoire Wasm

Les tableaux C ne peuvent pas être passés directement par ccall. Il faut allouer de la mémoire dans le heap Wasm, copier les données, appeler la fonction, et libérer :

const Module = await createMathLib();

// Données JavaScript
const jsData = [1.5, 2.3, 4.7, 3.1, 5.0];

// Allouer de la mémoire dans le heap Wasm (8 octets par double)
const nBytes = jsData.length * 8;  // Float64 = 8 bytes  
const ptr = Module._malloc(nBytes);  

// Copier les données dans le heap Wasm
Module.HEAPF64.set(jsData, ptr / 8);  // /8 car HEAPF64 est indexé en Float64

// Appeler la fonction C avec le pointeur
const mean = Module._array_mean(ptr, jsData.length);  
console.log("Mean:", mean);  // Mean: 3.32  

// Libérer la mémoire
Module._free(ptr);

Les vues typées sur le heap Wasm :

Vue Type C Taille
Module.HEAP8 int8_t 1 octet
Module.HEAPU8 uint8_t 1 octet
Module.HEAP16 int16_t 2 octets
Module.HEAP32 int32_t 4 octets
Module.HEAPF32 float 4 octets
Module.HEAPF64 double 8 octets

Ce mécanisme est l'équivalent Wasm de la manipulation de buffers NumPy en pybind11 (section 43.2.3.5) — on manipule directement la mémoire sous-jacente pour éviter les copies. Pour des usages fréquents, les wrappers Embind (section suivante) éliminent cette plomberie manuelle.


43.4.2.3 — Embind : exposer des classes C++ à JavaScript

Pourquoi Embind

ccall et cwrap fonctionnent pour les fonctions C simples, mais ils ne supportent ni les classes, ni les méthodes, ni les surcharges, ni les types complexes. Embind comble cette lacune — c'est le pybind11 du monde WebAssembly. Il permet d'exposer des classes C++ complètes à JavaScript avec une syntaxe déclarative quasiment identique à pybind11.

Syntaxe de base

// engine.cpp
#include <emscripten/bind.h>
#include <string>
#include <vector>
#include <numeric>
#include <cmath>

class StatEngine {  
public:  
    explicit StatEngine(const std::string& name) : name_(name) {}

    void load(const std::vector<double>& data) {
        data_ = data;
    }

    double mean() const {
        if (data_.empty()) return 0.0;
        return std::accumulate(data_.begin(), data_.end(), 0.0) / data_.size();
    }

    double stddev() const {
        if (data_.size() < 2) return 0.0;
        double m = mean();
        double var = 0.0;
        for (double x : data_) {
            var += (x - m) * (x - m);
        }
        return std::sqrt(var / data_.size());
    }

    size_t count() const { return data_.size(); }
    const std::string& name() const { return name_; }

private:
    std::string name_;
    std::vector<double> data_;
};

// ── Bindings Embind ──────────────────────────────────────────
using namespace emscripten;

EMSCRIPTEN_BINDINGS(stat_engine) {
    // Enregistrer les conversions pour std::vector<double>
    register_vector<double>("VectorDouble");

    class_<StatEngine>("StatEngine")
        .constructor<std::string>()
        .function("load", &StatEngine::load)
        .function("mean", &StatEngine::mean)
        .function("stddev", &StatEngine::stddev)
        .function("count", &StatEngine::count)
        .property("name", &StatEngine::name);
}

La syntaxe est familière après la section pybind11 (43.2.2) : même structure .constructor<>(), .function(), .property().

em++ -std=c++20 -O2 -o engine.js engine.cpp \
    --bind \
    -sMODULARIZE=1 \
    -sEXPORT_NAME=createEngine \
    -sALLOW_MEMORY_GROWTH=1

Le flag --bind (ou -lembind) active Embind.

Utilisation côté JavaScript

const Module = await createEngine();

// Créer une instance de la classe C++
const engine = new Module.StatEngine("temperature");

// Créer un vector<double> et le remplir
const data = new Module.VectorDouble();  
data.push_back(22.1);  
data.push_back(23.4);  
data.push_back(21.8);  
data.push_back(24.5);  
data.push_back(22.9);  

// Appeler les méthodes
engine.load(data);  
console.log(`${engine.name}: mean=${engine.mean().toFixed(2)}, `  
          + `stddev=${engine.stddev().toFixed(2)}, n=${engine.count()}`);
// temperature: mean=22.94, stddev=0.93, n=5

// IMPORTANT : libérer les objets C++ manuellement
data.delete();  
engine.delete();  

Point critique : delete(). Contrairement à Python (garbage collector) ou Rust (drop automatique), JavaScript ne détruit pas automatiquement les objets C++ encapsulés par Embind. Le développeur doit appeler .delete() explicitement pour déclencher le destructeur C++. L'oubli de .delete() provoque des fuites mémoire dans le heap Wasm — le garbage collector JavaScript ne peut pas récupérer cette mémoire car elle n'est pas gérée par le GC JS.

Enums

enum class Precision { Low, Medium, High };

EMSCRIPTEN_BINDINGS(enums) {
    enum_<Precision>("Precision")
        .value("Low",    Precision::Low)
        .value("Medium", Precision::Medium)
        .value("High",   Precision::High);
}
const precision = Module.Precision.High;  
engine.setPrecision(precision);  

Surcharge de fonctions

Embind gère la surcharge via select_overload :

class Processor {  
public:  
    void process(int value);
    void process(double value);
    void process(const std::string& value);
};

EMSCRIPTEN_BINDINGS(processor) {
    class_<Processor>("Processor")
        .constructor<>()
        .function("processInt",
                  select_overload<void(int)>(&Processor::process))
        .function("processDouble",
                  select_overload<void(double)>(&Processor::process))
        .function("processString",
                  select_overload<void(const std::string&)>(&Processor::process));
}

Comme en pybind11, JavaScript n'a pas de surcharge native, donc chaque variante reçoit un nom distinct.

Héritage

class Shape {  
public:  
    virtual ~Shape() = default;
    virtual double area() const = 0;
    virtual std::string name() const = 0;
};

class Circle : public Shape {  
public:  
    explicit Circle(double r) : radius_(r) {}
    double area() const override { return 3.14159265 * radius_ * radius_; }
    std::string name() const override { return "Circle"; }
    double radius() const { return radius_; }
private:
    double radius_;
};

EMSCRIPTEN_BINDINGS(shapes) {
    class_<Shape>("Shape")
        .function("area", &Shape::area, pure_virtual())
        .function("name", &Shape::name, pure_virtual());

    class_<Circle, base<Shape>>("Circle")
        .constructor<double>()
        .property("radius", &Circle::radius);
}
const circle = new Module.Circle(5.0);  
console.log(circle.name());   // "Circle"  
console.log(circle.area());   // 78.54  
circle.delete();  

Le polymorphisme fonctionne : une fonction JavaScript qui accepte un Shape acceptera un Circle.

Val : manipuler des objets JavaScript depuis C++

Embind fournit emscripten::val pour accéder aux objets JavaScript depuis le code C++ :

#include <emscripten/val.h>

using namespace emscripten;

void log_to_console(const std::string& message) {
    val::global("console").call<void>("log", message);
}

val get_browser_info() {
    val navigator = val::global("navigator");
    val result = val::object();
    result.set("userAgent", navigator["userAgent"]);
    result.set("language", navigator["language"]);
    return result;
}

EMSCRIPTEN_BINDINGS(js_interop) {
    function("logToConsole", &log_to_console);
    function("getBrowserInfo", &get_browser_info);
}
Module.logToConsole("Hello from C++ via console.log!");

const info = Module.getBrowserInfo();  
console.log(info.userAgent);  
console.log(info.language);  

val est l'équivalent de py::object en pybind11 — un handle vers un objet du langage hôte, manipulable dynamiquement depuis C++.


43.4.2.4 — Appeler JavaScript depuis C++

EM_JS : JavaScript inline dans le C++

La macro EM_JS permet de définir une fonction JavaScript directement dans le code C++ :

#include <emscripten.h>

// Déclarer une fonction JS appelable depuis C++
EM_JS(void, show_alert, (const char* message), {
    alert(UTF8ToString(message));
});

EM_JS(double, get_window_width, (), {
    return window.innerWidth;
});

EM_JS(void, update_progress, (double percent), {
    const bar = document.getElementById('progress');
    if (bar) bar.style.width = percent + '%';
});

void process_data() {
    update_progress(0.0);

    // ... traitement lourd ...
    for (int i = 0; i < 100; ++i) {
        // ... calcul ...
        update_progress(static_cast<double>(i + 1));
    }

    double width = get_window_width();
    std::printf("Window width: %.0f px\n", width);
}

EM_JS génère une fonction extern "C" côté C++ et une implémentation JavaScript côté glue. Les types supportés sont les scalaires (int, double) et les pointeurs (const char*, converti avec UTF8ToString).

EM_ASM : JavaScript anonyme en ligne

Pour les snippets courts, EM_ASM insère du JavaScript anonyme sans déclarer de fonction :

#include <emscripten.h>

void notify_user(int count) {
    EM_ASM({
        console.log("Processed " + $0 + " items");
        document.title = "Done: " + $0 + " items";
    }, count);
}

$0, $1, etc. référencent les arguments passés. EM_ASM est pratique pour le prototypage, mais EM_JS est préféré en production (meilleure lisibilité, meilleure optimisation).


43.4.2.5 — Le système de fichiers virtuel

Le problème

Un programme C++ qui lit des fichiers avec std::ifstream ou fopen s'attend à un système de fichiers. Le navigateur n'en fournit pas — le code Wasm s'exécute dans une sandbox sans accès au disque de l'utilisateur. Emscripten résout ce problème avec un système de fichiers virtuel en mémoire.

MEMFS : système de fichiers en mémoire (par défaut)

Par défaut, Emscripten monte un système de fichiers en mémoire (MEMFS) à la racine /. Les fichiers peuvent être pré-chargés au moment du build ou créés dynamiquement à l'exécution.

Pré-charger des fichiers (--preload-file)

# Structure du projet
# assets/
#   config.json
#   data.csv

em++ -std=c++20 -O2 -o app.js main.cpp \
    --preload-file assets@/assets \
    -sMODULARIZE=1 \
    -sEXPORT_NAME=createApp

Le flag --preload-file assets@/assets :

  • Emballe le contenu du répertoire assets/ dans un fichier .data.
  • Monte ce contenu dans le chemin virtuel /assets/ dans le FS Emscripten.
  • Le fichier .data est téléchargé automatiquement au chargement du module.

Le code C++ accède aux fichiers normalement :

#include <fstream>
#include <string>

void load_config() {
    std::ifstream file("/assets/config.json");
    if (!file.is_open()) {
        std::fprintf(stderr, "Cannot open config\n");
        return;
    }

    std::string content((std::istreambuf_iterator<char>(file)),
                         std::istreambuf_iterator<char>());
    std::printf("Config loaded: %zu bytes\n", content.size());
}

Embarquer des fichiers (--embed-file)

em++ -std=c++20 -O2 -o app.js main.cpp \
    --embed-file assets@/assets

--embed-file insère les données directement dans le fichier .js (en base64) au lieu de créer un .data séparé. L'avantage est un seul fichier à déployer ; l'inconvénient est une taille de .js plus grande et un décodage base64 au chargement. Pour les petits fichiers (configs, shaders), --embed-file est pratique. Pour les gros assets (textures, datasets), --preload-file est préféré.

Accès au FS depuis JavaScript

Emscripten expose le système de fichiers virtuel côté JavaScript :

const Module = await createApp();

// Écrire un fichier dans le FS virtuel avant utilisation par le C++
Module.FS.writeFile('/runtime/input.txt', 'Hello from JavaScript');

// Appeler du code C++ qui lit ce fichier
Module._process_input();  // Lit /runtime/input.txt

// Lire un fichier produit par le C++
const output = Module.FS.readFile('/runtime/output.csv', { encoding: 'utf8' });  
console.log(output);  

Cet accès bidirectionnel permet un pattern courant : JavaScript envoie des données dans le FS virtuel, le code C++ les traite et produit un résultat dans le FS virtuel, JavaScript récupère le résultat.


43.4.2.6 — Gestion de l'asynchronisme et de la boucle principale

Le problème de la boucle infinie

Un programme C++ interactif (jeu, simulation, interface graphique) utilise typiquement une boucle infinie :

// Code natif classique
while (!should_quit) {
    process_input();
    update_state();
    render();
}

Dans le navigateur, une boucle infinie bloque le thread principal et gèle l'interface. Emscripten fournit emscripten_set_main_loop pour adapter ce pattern :

#include <emscripten.h>

void main_loop_iteration() {
    process_input();
    update_state();
    render();
}

int main() {
    initialize();

    // Remplacer la boucle infinie par un callback
    // 0 = utiliser requestAnimationFrame (cadence du navigateur)
    // true = simuler une boucle infinie
    emscripten_set_main_loop(main_loop_iteration, 0, true);

    // Ce point n'est jamais atteint tant que la boucle tourne
    return 0;
}

emscripten_set_main_loop enregistre la fonction comme callback de requestAnimationFrame. Le navigateur appelle la fonction à chaque frame (~60 fps), en rendant le contrôle au thread principal entre chaque appel. L'interface reste réactive.

Asyncify : transformer du code synchrone en asynchrone

Certains codes C++ effectuent des attentes synchrones (sleep, lectures réseau bloquantes) qui ne sont pas compatibles avec le modèle événementiel du navigateur. Emscripten fournit Asyncify, une transformation du code qui permet de "suspendre" l'exécution Wasm et de la reprendre après un événement asynchrone :

em++ -std=c++20 -O2 -o app.js main.cpp \
    -sASYNCIFY \
    -sASYNCIFY_STACK_SIZE=65536
#include <emscripten.h>

// emscripten_sleep "suspend" l'exécution Wasm (via Asyncify)
void long_computation() {
    for (int i = 0; i < 100; ++i) {
        heavy_compute_step(i);

        // "Dormir" 0ms — rend le contrôle au navigateur pour une frame
        emscripten_sleep(0);
    }
}

Asyncify a un coût : le binaire Wasm est plus gros (~10-20 %) et l'exécution est légèrement plus lente à cause de l'instrumentation. Il doit être utilisé judicieusement, pour les cas où la refactorisation vers un modèle asynchrone n'est pas réaliste.


43.4.2.7 — Intégration CMake complète

Projet complet avec Embind, fichiers et optimisation

cmake_minimum_required(VERSION 3.24)  
project(data_processor LANGUAGES CXX)  

set(CMAKE_CXX_STANDARD 20)  
set(CMAKE_CXX_STANDARD_REQUIRED ON)  

# ─── Bibliothèque C++ (indépendante de Wasm) ─────────────────
add_library(core STATIC
    src/engine.cpp
    src/parser.cpp
    src/stats.cpp
)
target_include_directories(core PUBLIC include/)

if(EMSCRIPTEN)
    # ─── Module Wasm ──────────────────────────────────────────
    add_executable(dataproc
        src/bindings.cpp       # Code Embind uniquement
    )
    target_link_libraries(dataproc PRIVATE core)

    set_target_properties(dataproc PROPERTIES
        SUFFIX ".js"
    )

    target_link_options(dataproc PRIVATE
        --bind                              # Activer Embind
        -sWASM=1
        -sMODULARIZE=1
        -sEXPORT_NAME=createDataProcessor
        -sALLOW_MEMORY_GROWTH=1
        -sEXPORTED_RUNTIME_METHODS=FS        # Exposer le FS virtuel
        --preload-file ${CMAKE_SOURCE_DIR}/assets@/assets
    )

    # Optimisation pour la taille en Release
    if(CMAKE_BUILD_TYPE STREQUAL "Release")
        target_compile_options(dataproc PRIVATE -Os)
        target_link_options(dataproc PRIVATE
            -Os
            --closure 1                      # Minification JS avancée
            -flto                            # Link-Time Optimization
        )
    endif()

else()
    # ─── Exécutable natif (tests, développement) ──────────────
    add_executable(dataproc_native
        src/main_native.cpp
    )
    target_link_libraries(dataproc_native PRIVATE core)

    # Tests Google Test
    enable_testing()
    add_executable(tests tests/test_engine.cpp tests/test_parser.cpp)
    target_link_libraries(tests PRIVATE core GTest::gtest_main)
    add_test(NAME unit_tests COMMAND tests)
endif()

Architecture du projet

data_processor/
├── CMakeLists.txt
├── CMakePresets.json          # native-debug, native-release, wasm-release
├── include/
│   ├── engine.h               # API C++ pure
│   ├── parser.h
│   └── stats.h
├── src/
│   ├── engine.cpp             # Code métier (aucune ref à Emscripten)
│   ├── parser.cpp
│   ├── stats.cpp
│   ├── bindings.cpp           # Embind (dépend d'Emscripten)
│   └── main_native.cpp        # Point d'entrée natif
├── assets/
│   └── sample_data.csv
├── tests/
│   ├── test_engine.cpp        # Google Test (natif)
│   └── test_parser.cpp
└── web/
    ├── index.html             # Interface web
    └── app.js                 # Code JavaScript applicatif

Le point clé est la séparation : engine.cpp, parser.cpp et stats.cpp ne contiennent aucune référence à Emscripten. Ils compilent et passent les tests en natif. Le fichier bindings.cpp est la seule couche qui dépend d'Emscripten. Le code métier est testé avec Google Test en natif, et les tests d'intégration web vérifient le comportement via le navigateur.


43.4.2.8 — Optimisation pour la production

Réduire la taille du module

La taille du .wasm et du .js impacte directement le temps de chargement :

# Compilation optimisée pour la taille
em++ -Os -flto -o app.js main.cpp --bind \
    --closure 1 \
    -sMODULARIZE=1 \
    -sEXPORT_NAME=createApp \
    -sALLOW_MEMORY_GROWTH=1 \
    -sFILESYSTEM=0               # Désactiver le FS si non utilisé
    -sASSERTIONS=0               # Désactiver les assertions Emscripten
    -sDISABLE_EXCEPTION_CATCHING=1  # Désactiver try/catch (réduit la taille)
Technique Impact sur la taille Notes
-Os / -Oz -20 à -40 % vs -O2 Privilégier pour le web
-flto -5 à -15 % Link-Time Optimization
--closure 1 -30 à -50 % sur le JS Minification avancée du code de glue
-sFILESYSTEM=0 -50 à -100 Ko Si le FS virtuel n'est pas utilisé
-sDISABLE_EXCEPTION_CATCHING=1 -10 à -20 % Supprime le support des exceptions C++
-sASSERTIONS=0 -5 à -10 % Supprime les assertions de debug
Compression gzip/brotli (serveur) -60 à -80 % du transfert Wasm se compresse très bien

Servir avec compression

Le .wasm est un format binaire qui se compresse extrêmement bien. Servir le module avec la compression Brotli ou gzip réduit la taille de transfert de 60 à 80 % :

# Nginx — activer la compression pour Wasm
gzip on;  
gzip_types application/wasm application/javascript;  

# Brotli (encore plus efficace, si disponible)
brotli on;  
brotli_types application/wasm application/javascript;  

Un module .wasm de 2 Mo se réduit typiquement à 400-600 Ko après compression Brotli — comparable à une image haute résolution.

Streaming compilation

Les navigateurs modernes supportent la streaming compilation : le module Wasm est compilé pendant qu'il est téléchargé, pas après. Pour en bénéficier, le serveur doit servir le .wasm avec le type MIME correct (application/wasm) et supporter HTTP/2 ou HTTP/3. Emscripten utilise WebAssembly.instantiateStreaming() par défaut quand c'est possible.


43.4.2.9 — Débogage

Source maps

Emscripten peut générer des source maps qui permettent de déboguer le code C++ directement dans les DevTools du navigateur :

em++ -std=c++20 -O0 -g -gsource-map -o app.js main.cpp --bind

Avec -gsource-map, les DevTools affichent le code source C++ original dans le panel Sources, avec des breakpoints et une inspection des variables.

DWARF debugging (Chrome)

Chrome supporte le débogage DWARF natif pour WebAssembly via l'extension C/C++ DevTools Support :

em++ -std=c++20 -O0 -g4 -o app.js main.cpp --bind \
    -sSEPARATE_DWARF_URL=app.debug.wasm

Le flag -g4 génère les informations de débogage DWARF complètes, et -sSEPARATE_DWARF_URL les place dans un fichier séparé pour ne pas alourdir le module de production.

Assertions Emscripten

En mode développement, les assertions Emscripten détectent les erreurs courantes (accès mémoire invalide, appels à des fonctions non exportées, dépassement de pile) :

# Développement : assertions activées
em++ -O0 -g -sASSERTIONS=2 -sSAFE_HEAP=1 -sSTACK_OVERFLOW_CHECK=2 ...

# Production : assertions désactivées
em++ -Os -sASSERTIONS=0 ...

-sSAFE_HEAP=1 vérifie les accès mémoire (analogue à AddressSanitizer) et -sSTACK_OVERFLOW_CHECK=2 détecte les dépassements de pile. Ces flags ralentissent l'exécution et ne doivent pas être activés en production.


43.4.2.10 — Limitations et contournements

Ce qui ne fonctionne pas (ou différemment) en Wasm

Fonctionnalité C++ Comportement en Wasm Contournement
Threads natifs (std::thread) Web Workers + SharedArrayBuffer -pthread, headers COOP/COEP requis
Système de fichiers réel Système de fichiers virtuel (MEMFS) --preload-file, --embed-file, FS API
Sockets TCP/UDP Non supportés directement WebSockets (-sPROXY_POSIX_SOCKETS)
fork() / exec() Non supportés Repenser l'architecture
SIMD complet SIMD 128-bit Wasm (partiel) -msimd128, vérifier le support navigateur
Signaux (signal()) Émulation limitée Éviter dans le code Wasm
dlopen() / plugins Emscripten dynamique expérimental Side modules (-sSIDE_MODULE)
Exceptions C++ Supportées mais coûteuses en taille -fwasm-exceptions (natif, plus efficace)
mmap Émulation partielle Préférer l'allocation standard

Exceptions : choix de l'implémentation

Emscripten supporte deux mécanismes d'exceptions C++ :

  • Emscripten exceptions (par défaut) : émulées en JavaScript, compatibles avec tous les navigateurs, mais coûteuses en taille (~10-20 % du binaire).
  • Wasm native exceptions (-fwasm-exceptions) : utilisant la proposal Exception Handling de WebAssembly, plus rapides et plus compactes, supportées par Chrome et Firefox en 2026, Safari en cours.
# Exceptions natives Wasm (recommandé si le support navigateur est suffisant)
em++ -fwasm-exceptions -o app.js main.cpp

Pour les projets qui ne lancent pas d'exceptions (ou qui utilisent std::expected / codes d'erreur), désactiver complètement les exceptions réduit significativement la taille :

em++ -fno-exceptions -sDISABLE_EXCEPTION_CATCHING=1 -o app.js main.cpp

Résumé

Mécanisme Usage Complexité
ccall / cwrap Fonctions C simples (extern "C") Faible
Embind (--bind) Classes, méthodes, propriétés, enums Modérée
EM_JS / EM_ASM Appeler JavaScript depuis C++ Faible
emscripten::val Manipuler des objets JS depuis C++ Modérée
Système de fichiers virtuel I/O fichiers transparente Faible
emscripten_set_main_loop Boucles interactives (jeux, simulations) Faible
Asyncify Code synchrone bloquant → asynchrone Élevée

Le workflow complet pour un projet C++ vers WebAssembly suit une progression naturelle :

  1. Compiler le code C++ existant avec emcc — souvent ça "marche" directement pour le calcul pur.
  2. Exposer les fonctions nécessaires via extern "C" + EMSCRIPTEN_KEEPALIVE ou Embind.
  3. Intégrer dans une page web via le module JavaScript généré.
  4. Adapter les parties qui dépendent du système (fichiers, réseau, threads) aux contraintes du navigateur.
  5. Optimiser la taille et les performances pour la production.

L'ensemble du chapitre 43 se referme ici. De extern "C" (section 43.1) à Embind (cette section), en passant par pybind11 (43.2) et cxx (43.3), le fil conducteur est le même : le code C++ métier reste au centre, inchangé, et des couches d'adaptation spécialisées l'ouvrent vers d'autres langages et d'autres plateformes d'exécution. La maîtrise de ces mécanismes d'interopérabilité est ce qui permet au C++ de rester pertinent dans un écosystème logiciel de plus en plus polyglotte.

📎 Ce chapitre conclut le Module 15 (Interopérabilité). Le chapitre suivant (44) aborde les Patterns de Conception en C++ — Singleton, Factory, Builder, Observer, CRTP, Type Erasure — qui structurent l'architecture des projets C++ professionnels.

⏭️ Module 16 : Patterns et Architecture