Travail avancé avec WebAssembly et C++ : de la théorie à la pratique
WebAssembly (Wasm) offre aux développeurs C++ des possibilités uniques : exécuter du code haute performance dans le navigateur à une vitesse quasi native. Cependant, les tutoriels de base se limitent souvent à la compilation de fonctions simples. Dans cet article, nous approfondissons les techniques avancées : gestion manuelle de la mémoire, création de bindings efficaces et profilage des performances. Ce contenu est destiné aux développeurs déjà familiarisés avec les bases d'Emscripten et la compilation du C++ en Wasm.
1. Gestion manuelle de la mémoire : surmonter les limitations de Wasm
Par défaut, WebAssembly utilise une mémoire linéaire gérée via malloc/free. Pour les applications à forte charge (jeux, traitement d'images, simulations), l'allocateur standard peut devenir un goulot d'étranglement. L'approche avancée consiste à implémenter votre propre pool de mémoire.
Considérons l'exemple d'un simple pool de taille fixe :
// pool_allocator.h#include <cstdint>#include <vector>
class PoolAllocator {public: PoolAllocator(size_t block_size, size_t block_count) : block_size_(block_size), block_count_(block_count) { memory_ = malloc(block_size * block_count); // Initialisation de la liste des blocs libres for (size_t i = 0; i < block_count; ++i) { free_blocks_.push_back( reinterpret_cast<uintptr_t>(memory_) + i * block_size ); } }
~PoolAllocator() { free(memory_); }
void* allocate() { if (free_blocks_.empty()) return nullptr; uintptr_t addr = free_blocks_.back(); free_blocks_.pop_back(); return reinterpret_cast<void*>(addr); }
void deallocate(void* ptr) { free_blocks_.push_back(reinterpret_cast<uintptr_t>(ptr)); }
private: void* memory_; size_t block_size_; size_t block_count_; std::vector<uintptr_t> free_blocks_;};Un tel allocateur élimine la fragmentation et accélère l'allocation/libération de mémoire des centaines de fois. Pour l'intégration avec Emscripten, utilisez EMSCRIPTEN_KEEPALIVE :
// main.cpp#include <emscripten.h>
PoolAllocator* g_allocator = nullptr;
extern "C" { EMSCRIPTEN_KEEPALIVE void init_allocator(size_t block_size, size_t block_count) { g_allocator = new PoolAllocator(block_size, block_count); }
EMSCRIPTEN_KEEPALIVE void* pool_alloc() { return g_allocator ? g_allocator->allocate() : nullptr; }
EMSCRIPTEN_KEEPALIVE void pool_free(void* ptr) { if (g_allocator) g_allocator->deallocate(ptr); }}2. Intégration avancée avec JavaScript : transmission de structures complexes
La transmission de types scalaires (int, float) est simple, mais pour les structures et les tableaux, une sérialisation est nécessaire. La méthode optimale consiste à travailler directement avec la mémoire linéaire de Wasm via TypedArray.
Exemple : transmission d'un tableau de points du C++ vers JavaScript sans copie.
// point.h#pragma pack(push, 1)struct Point { float x; float y; float z;};#pragma pack(pop)
// main.cpp#include <emscripten.h>#include <vector>
std::vector<Point> points;
extern "C" { EMSCRIPTEN_KEEPALIVE void generate_points(int count) { points.resize(count); for (int i = 0; i < count; ++i) { points[i] = {static_cast<float>(i), static_cast<float>(i*2), static_cast<float>(i*3)}; } }
EMSCRIPTEN_KEEPALIVE float* get_points_ptr() { return reinterpret_cast<float*>(points.data()); }
EMSCRIPTEN_KEEPALIVE int get_points_count() { return points.size(); }}<