Advanced Work with WebAssembly and C++: From Theory to Practice
WebAssembly (Wasm) opens unique opportunities for C++ developers: running high-performance code in the browser at near-native speed. However, basic tutorials are often limited to compiling simple functions. In this article, we will delve into advanced techniques: manual memory management, creating efficient bindings, and performance profiling. The material is intended for developers already familiar with the basics of Emscripten and compiling C++ to Wasm.
1. Manual Memory Management: Overcoming Wasm Limitations
By default, WebAssembly uses linear memory managed via malloc/free. For high-load applications (games, image processing, simulations), the standard allocator can become a bottleneck. An advanced approach is implementing a custom memory pool.
Consider an example of a simple fixed-size pool:
// 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); // Initialize free block list 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_;};Such an allocator eliminates fragmentation and speeds up memory allocation/deallocation by hundreds of times. For integration with Emscripten, use 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. Advanced JavaScript Integration: Passing Complex Structures
Passing scalar types (int, float) is simple, but for structures and arrays, serialization is required. The optimal way is to work directly with Wasm linear memory via TypedArray.
Example: passing an array of points from C++ to JavaScript without copying.
// 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(); }}<