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.