Add support for vfs assets in mj_compile function to MuJoCo WASM.

Regarding mj_compile VFS argument:
I was encountering a memory error when using `std::optional<MjVFS>` with `mj_compile` (because vfs should be an optional argument of the function). This is likely because `MjVFS` doesn't have copy/move constructors, which are required for types used with `std::optional`, causing issues with memory management when std::optional copies the MjVFS object.
To solve this and to make vfs an optional argument to `mj_compile`, I replaced `mj_compile_wrapper` with two overloaded functions. This avoids using `std::optional<MjVFS>` and resolves the memory issue, while still allowing mj_compile to be called with or without the vfs argument from JavaScript.

But what if we instead add those copy/move constructors so it works with `std::optional`?:
While making `MjVFS` copyable or movable would theoretically allow it to work with std::optional, it's not a feasible approach in this case.
The MjVFS struct manages file data in memory, allocating buffers when files are added and freeing them when `mj_deleteVFS` is called in its destructor. A copy would require a deep copy of the entire virtual filesystem, but MuJoCo does not provide a public API function (like a  "mj_copyVFS") to do this. Implementing a deep copy manually would be complex and fragile. A shallow copy would result in multiple MjVFS objects pointing to the same memory, leading to double-free errors when their destructors run.
Given these constraints, MjVFS cannot safely be made copyable. The approach of providing two overloaded C++ functions is better.

PiperOrigin-RevId: 855740541
Change-Id: Ia08b6db8868ad245f07145db3fb0322cb9f737cb
This commit is contained in:
Google DeepMind
2026-01-13 08:35:51 -08:00
committed by Copybara-Service
parent 7e21f18fd3
commit 37762e3f70
5 changed files with 157 additions and 28 deletions
+18 -15
View File
@@ -64,12 +64,16 @@ _SKIPPED_PLUGIN_FUNCTIONS: tuple[str, ...] = (
# Functions that are bound as class methods
_SKIPPED_CLASS_METHODS: tuple[str, ...] = (
# go/keep-sorted start
"mj_addBufferVFS",
"mj_copyData",
"mj_copyModel",
"mj_copySpec",
"mj_defaultVFS",
"mj_deleteData",
"mj_deleteFileVFS",
"mj_deleteModel",
"mj_deleteSpec",
"mj_deleteVFS",
"mj_loadXML",
"mj_makeData",
"mj_makeSpec",
@@ -113,11 +117,9 @@ _SKIPPED_ASSET_CACHE_FUNCTIONS: tuple[str, ...] = (
# Omitted Virtual Filesystem (VFS) functions
_SKIPPED_VFS_FUNCTIONS: tuple[str, ...] = (
# go/keep-sorted start
"mj_addBufferVFS",
"mj_addFileVFS",
"mj_defaultVFS",
"mj_deleteFileVFS",
"mj_deleteVFS",
"mj_mountVFS",
"mj_unmountVFS",
# go/keep-sorted end
)
@@ -187,17 +189,6 @@ _SKIPPED_UTILITY_FUNCTIONS: tuple[str, ...] = (
# go/keep-sorted end
)
# Functions that require special wrappers.
# These functions are not bound automatically but are written by hand instead.
MANUAL_WRAPPER_FUNCTIONS: tuple[str, ...] = (
# go/keep-sorted start
"mj_compile",
"mj_saveLastXML",
"mj_setLengthRange",
"mju_error",
# go/keep-sorted end
)
# List of functions that should be skipped during the code generation process.
SKIPPED_FUNCTIONS: tuple[str, ...] = (
_SKIPPED_CLASS_METHODS
@@ -232,12 +223,24 @@ SKIPPED_STRUCTS: tuple[str, ...] = (
# go/keep-sorted end
)
# Functions that require special wrappers.
# These functions are not bound automatically but are written by hand instead.
MANUAL_WRAPPER_FUNCTIONS: tuple[str, ...] = (
# go/keep-sorted start
"mj_compile",
"mj_saveLastXML",
"mj_setLengthRange",
"mju_error",
# go/keep-sorted end
)
# Structs for which header generation is done manually.
# mjvScene is included here because buffer sizes need to be calculated based on
# introspect doc strings, which was considered a brittle unreliable solution in
# the past.
MANUAL_STRUCTS_HEADERS: tuple[str, ...] = (
"mjvScene",
"mjVFS",
)
# Structs for which source code generation is done manually.
MANUAL_STRUCTS_SOURCES: tuple[str, ...] = (
+3 -2
View File
@@ -272,8 +272,9 @@ def generate(
function_bindings = []
for func in sorted(functions, key=lambda f: f.name):
w = common.wrapped_function_name(func)
function_bindings.append(f'function("{func.name}", &{w});')
if func.name not in constants.MANUAL_WRAPPER_FUNCTIONS:
w = common.wrapped_function_name(func)
function_bindings.append(f'function("{func.name}", &{w});')
bindings_content = "\n".join(function_bindings)
return [