How Ammo.js Maps Pointers to JavaScript Objects

This article explains how ammo.js bridges the gap between high-level JavaScript and low-level C++ memory management. You will learn how WebAssembly pointers map to JavaScript wrapper objects, how data is read from the heap, and how to properly manage this memory to avoid leaks in your applications.

The WebAssembly Memory Heap

Because ammo.js is a direct port of the Bullet Physics engine via Emscripten, it does not run natively in the JavaScript garbage-collected virtual machine. Instead, it operates inside a compiled WebAssembly (Wasm) environment.

This environment utilizes a single, contiguous block of memory represented as a massive JavaScript ArrayBuffer (commonly referred to as the heap). Within this heap, C++ objects are allocated at specific byte offsets.

What is a Pointer in ammo.js?

In standard C++, a pointer is a variable that holds the memory address of another variable. In ammo.js, because the memory is just a large ArrayBuffer, a pointer is represented as a simple JavaScript number (integer). This number corresponds to the index/byte offset of the object’s start location inside the WebAssembly memory buffer.

For example, if a btVector3 object is allocated at the memory offset of 1048576 bytes, the pointer to this object is simply the integer 1048576.

The JavaScript Wrapper Object

To make these memory addresses usable in JavaScript, ammo.js generates wrapper classes. When you instantiate an object:

const vector = new Ammo.btVector3(1, 2, 3);

Two distinct operations occur behind the scenes: 1. Wasm Memory Allocation: Space for three 32-bit floating-point numbers (12 bytes total for x, y, and z coordinates) is allocated in the WebAssembly heap. 2. JS Wrapper Creation: A JavaScript object is created. This wrapper acts as a handle and contains a reference to the raw memory address, which is exposed via the .ptr property.

If you inspect vector.ptr, you will see an integer representing its physical location in the Wasm heap.

How Method Calls Work

When you call a method on an ammo.js object, the wrapper coordinates with the WebAssembly module using its stored pointer.

const y = vector.y();

When this JavaScript method is called, the wrapper passes its internal .ptr numerical address back to the compiled WebAssembly function. The WebAssembly code then reads the memory offset corresponding to the “y” coordinate (pointer offset + 4 bytes) directly from the shared heap and returns the value back to JavaScript.

Reconstructing Objects with wrapPointer

Often, physics simulations return raw pointers (integers) during collision detection or raycasting. To interact with these raw pointers in JavaScript, ammo.js provides a utility to wrap an existing memory address back into a usable JavaScript class instance:

// Assume 'rawAddress' is an integer pointer returned from a collision event
const colShape = Ammo.wrapPointer(rawAddress, Ammo.btCollisionShape);

This does not allocate new memory on the heap. Instead, it creates a new JavaScript wrapper object that points directly to the pre-existing C++ object at that specific memory address.

The Importance of Manual Memory Management

JavaScript features automatic garbage collection, meaning objects are deleted from memory when they are no longer referenced. However, the JavaScript garbage collector has no visibility into the WebAssembly heap.

If a JavaScript wrapper object is garbage collected, only the small JS wrapper is deleted. The underlying C++ object remains in the WebAssembly heap forever, causing a memory leak.

To prevent this, you must manually free the allocated memory using Ammo.destroy() when an object is no longer needed:

// Free the memory allocated in the WebAssembly heap
Ammo.destroy(vector);

Calling destroy deallocates the C++ memory at the pointer address, keeping the WebAssembly heap clean and preventing your application from running out of memory.