From 3bbb5489910769f225baaed618263ce3ca2cb645 Mon Sep 17 00:00:00 2001 From: MatiasManevi Date: Tue, 24 Feb 2026 15:04:18 -0300 Subject: [PATCH] Add memory management section to readme --- wasm/README.md | 78 ++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 57 insertions(+), 21 deletions(-) diff --git a/wasm/README.md b/wasm/README.md index 32293a4b..1572e141 100644 --- a/wasm/README.md +++ b/wasm/README.md @@ -122,38 +122,62 @@ joint using `data.jnt('myjoint')`. For more details and examples of how to use named access, please refer to the [named access tests](tests/bindings_test.ts#L1876-L2378) and [documentation](https://mujoco.readthedocs.io/en/stable/python.html#named-access). -## Usage Guide -When interacting with MuJoCo objects through the WASM bindings, it's important to understand how data is accessed. Properties on objects like `MjModel` and `MjData` can expose data in two ways: by copy or by reference. +### Memory Management +Embind-wrapped C++ object handles created or returned into JavaScript live on the +WebAssembly heap and are **not** garbage-collected by the JS runtime. -### Install -```sh -npm install mujoco +Any heap-allocated C++ object exposed to JS (e.g. via `new Module.MyClass(...)` +or returned as a pointer/reference from a binding) must be explicitly freed +when no longer needed to avoid memory leaks. + +Use the generated `.delete()` method on wrapped instances to destroy the +underlying C++ object: + +```typescript + const obj = new Module.MyClass(...); + // ... use obj ... + obj.delete(); // free the C++ memory ``` -```ts -import loadMujoco from 'mujoco'; +Be careful to call `.delete()` exactly once per created object (double-delete +is an error). In JS code paths that may throw or return early, ensure +deletion happens in finally blocks or wrap lifetime management to avoid leaks. -const mujoco = await loadMujoco(); - -const model = mujoco.MjModel.fromXMLString(` - - - - - -`); - -const data = new mujoco.MjData(model); -mujoco.mj_step(model, data); -``` +> [!IMPORTANT] +> _Embind's documentation strongly recommends that JavaScript code explicitly deletes any C++ object handles it has received._ ### Copy vs. Reference +When interacting with MuJoCo objects through the WASM bindings, it's important to understand how data is accessed. Properties on objects like `MjModel` and `MjData` can expose data in two ways: by copy or by reference. + #### 1. By Copy (Value-based access) Some properties return a copy of the data at the time of access. This is common for complex data structures that need to be marshalled from C++ to JavaScript. -A key example is `MjData.contact`. When you access `data.contact`, you get a new array containing the contacts at that specific moment in the simulation. If you step the simulation forward, this array will not be updated. You must access `data.contact` again to get the new contact information. +A key example is `MjData.contact`. When you access `data.contact`, you get an object containing a copy of the contacts at that specific moment in the simulation. + +If you step the simulation forward, they will not be updated. You must access `data.contact` again to get the new contact information. + +The object you get is a JavaScript proxy interface generated by [Emscripten’s Embind library](https://emscripten.org/docs/porting/connecting_cpp_and_javascript/embind.html#built-in-type-conversions) when you expose a `std::vector` using `register_vector`. It is essentially a "bridge" object. + +```typescript +export interface MjContactVec extends ClassHandle { + /** Appends a new element to the end of the vector, increasing its length by one. */ + push_back(_0: MjContact): void; + + /** Resizes the vector to contain the specified number of elements, filling new slots with the provided value. */ + resize(_0: number, _1: MjContact): void; + + /** Returns the total number of elements currently stored in the vector. */ + size(): number; + + /** Retrieves the element at the specified index, or returns undefined if the index is out of bounds. */ + get(_0: number): MjContact | undefined; + + /** Overwrites the element at the specified index; returns true if successful or false if the index is invalid. */ + set(_0: number, _1: MjContact): boolean; +} +``` Example: ```typescript @@ -165,9 +189,14 @@ mujoco.mj_step(model, data); // `contacts` is now stale. To get the new contacts, you must access the property again: const newContacts = data.contact; + +// Remember to delete all created objects when they are no longer needed. +contacts.delete(); +newContacts.delete(); ``` #### 2. By Reference (View-based access) + Many properties, especially large numerical arrays, return a live view directly into the WebAssembly memory. This is highly efficient as it avoids copying large amounts of data. A key example is `MjData.qpos` (joint positions). When you get a reference to this array, it points directly to the simulation's state data. Any changes in the simulation (e.g., after a call to `mj_step`) will be immediately reflected in this array. @@ -183,9 +212,13 @@ mujoco.mj_step(model, data); // `qpos` is automatically updated. console.log(qpos[0]); // Print new position + +// Remember to delete all created objects when they are no longer needed. +data.delete(); ``` ### Data Layout: Row-Major Matrices + When a function from the MuJoCo C API returns a matrix (or needs a matrix as input), these are represented in the JavaScript bindings as flat, one-dimensional `TypedArray`'s. The elements are stored in row-major order. For example, a 3x10 matrix will be returned as a flat array with 30 elements. The first 10 elements represent the first row, the next 10 represent the second row, and so on. @@ -202,11 +235,13 @@ const element = matrix[i * nCols + j]; ``` ### Working with Out Parameters + Many functions in the MuJoCo C API use "out parameters" to return data. This means instead of returning a value, they write the result into one of the arguments passed to them by reference (using pointers). In our JavaScript bindings, you'll need to handle these cases specifically. There are two main scenarios you'll encounter: #### 1. Array-like Out Parameters + When a function expects a pointer to a primitive type (like `mjtNum*` or `int*`) to write an array of values, you need to pre-allocate memory for the result on the JavaScript side. We provide helper classes for this: `mujoco.Uint8Buffer`, `mujoco.DoubleBuffer`, `mujoco.FloatBuffer`, and `mujoco.IntBuffer`. Here's how to use them: @@ -242,6 +277,7 @@ try { ``` #### 2. Struct Out Parameters (e.g., mjvCamera*, mjvScene*) + When a function modifies a struct passed by pointer, you should pass an instance of the corresponding JavaScript wrapper class. The underlying C++ struct will be modified in place. Example: Updating a scene