🔝 Retour au Sommaire
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.
// 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.wasmLe 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.
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.wasmLe 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>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.
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=createMathLibLe flag --no-entry indique qu'il n'y a pas de fonction main() — c'est une bibliothèque, pas un programme autonome.
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.
| 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) |
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.
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.
// 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=1Le flag --bind (ou -lembind) active Embind.
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.
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); 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.
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.
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++.
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).
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).
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.
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.
# 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=createAppLe 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
.dataest 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());
}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é.
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.
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.
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.
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()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.
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 |
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.
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.
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 --bindAvec -gsource-map, les DevTools affichent le code source C++ original dans le panel Sources, avec des breakpoints et une inspection des variables.
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.wasmLe 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.
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.
| 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 |
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.cppPour 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| 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 :
- Compiler le code C++ existant avec
emcc— souvent ça "marche" directement pour le calcul pur. - Exposer les fonctions nécessaires via
extern "C"+EMSCRIPTEN_KEEPALIVEou Embind. - Intégrer dans une page web via le module JavaScript généré.
- Adapter les parties qui dépendent du système (fichiers, réseau, threads) aux contraintes du navigateur.
- 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.