Add JavaScript bindings and WASM support

Co-authored-by: Matija Kecman <matijak@google.com>
Co-authored-by: Sebastian Noreña Rendón <sebas.norena@creativa77.com.ar>
Co-authored-by: Kyle Bayes <kylebayes@google.com>
PiperOrigin-RevId: 826109484
Change-Id: I327fd54122762bd2e239cc31c602a3eb73051c26
This commit is contained in:
Matija Kecman
2025-10-30 11:10:55 -07:00
committed by Copybara-Service
parent 76e64d96bb
commit f2badc05ac
56 changed files with 22 additions and 24362 deletions
-55
View File
@@ -136,7 +136,6 @@ jobs:
steps:
- uses: actions/checkout@v3
- name: Prepare Linux
if: ${{ runner.os == 'Linux' }}
run: >
@@ -149,15 +148,12 @@ jobs:
libxrandr-dev
libxi-dev
ninja-build
- name: Prepare macOS
if: ${{ runner.os == 'macOS' }}
run: brew install ninja
- uses: actions/setup-python@v4
with:
python-version: "3.11"
- name: Prepare Python
shell: bash
run: |
@@ -173,26 +169,6 @@ jobs:
source venv/bin/activate
python -m pip install --upgrade --require-hashes -r "${repo}/python/build_requirements.txt"
python -m pip install --upgrade --require-hashes -r "${repo}/python/build_requirements_usd.txt"
- name: Setup Node.js for WASM
if: ${{ runner.os == 'Linux' }}
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install NPM Dependencies for WASM
if: ${{ runner.os == 'Linux' }}
working-directory: wasm
run: |
npm ci
- name: Setup Emscripten
if: ${{ runner.os == 'Linux' }}
run: |
git clone https://github.com/emscripten-core/emsdk.git
./emsdk/emsdk install 4.0.10
./emsdk/emsdk activate 4.0.10
- name: Configure MuJoCo
run: >
mkdir build &&
@@ -203,11 +179,9 @@ jobs:
-DCMAKE_INSTALL_PREFIX:STRING=${{ matrix.tmpdir }}/mujoco_install
-DMUJOCO_BUILD_EXAMPLES:BOOL=OFF
${{ matrix.cmake_args }}
- name: Build MuJoCo
working-directory: build
run: cmake --build . --config=Release ${{ matrix.cmake_build_args }}
- name: Copy in the correct VC runtime DLLs (workaround for actions/runner-images#10004)
if: ${{ runner.os == 'Windows' }}
working-directory: build
@@ -218,15 +192,12 @@ jobs:
-Path "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Redist\MSVC\14.*" |
Sort -Descending | Select-Object -First 1).FullName
) 'x64\Microsoft.VC143.CRT\*.dll') "bin\Release"
- name: Test MuJoCo
working-directory: build
run: ctest -C Release --output-on-failure .
- name: Install MuJoCo
working-directory: build
run: cmake --install .
- name: Copy plugins (POSIX)
if: ${{ runner.os != 'Windows' }}
working-directory: build
@@ -235,7 +206,6 @@ jobs:
cp lib/libelasticity.* ${{ matrix.tmpdir }}/mujoco_install/mujoco_plugin &&
cp lib/libsensor.* ${{ matrix.tmpdir }}/mujoco_install/mujoco_plugin &&
cp lib/libsdf_plugin.* ${{ matrix.tmpdir }}/mujoco_install/mujoco_plugin
- name: Copy plugins (Windows)
if: ${{ runner.os == 'Windows' }}
working-directory: build
@@ -243,7 +213,6 @@ jobs:
cp bin/Release/actuator.dll ${{ matrix.tmpdir }}/mujoco_install/mujoco_plugin &&
cp bin/Release/elasticity.dll ${{ matrix.tmpdir }}/mujoco_install/mujoco_plugin &&
cp bin/Release/sensor.dll ${{ matrix.tmpdir }}/mujoco_install/mujoco_plugin
- name: Configure samples
working-directory: sample
run: >
@@ -254,11 +223,9 @@ jobs:
-DCMAKE_INTERPROCEDURAL_OPTIMIZATION:BOOL=OFF
-Dmujoco_ROOT:STRING=${{ matrix.tmpdir }}/mujoco_install
${{ matrix.cmake_args }}
- name: Build samples
working-directory: sample/build
run: cmake --build . --config=Release ${{ matrix.cmake_build_args }}
- name: Configure simulate
working-directory: simulate
run: >
@@ -269,18 +236,15 @@ jobs:
-DCMAKE_INTERPROCEDURAL_OPTIMIZATION:BOOL=OFF
-Dmujoco_ROOT:STRING=${{ matrix.tmpdir }}/mujoco_install
${{ matrix.cmake_args }}
- name: Build simulate
working-directory: simulate/build
run: cmake --build . --config=Release ${{ matrix.cmake_build_args }}
- name: Make Python sdist
shell: bash
working-directory: python
run: >
source ${{ matrix.tmpdir }}/venv/bin/activate &&
./make_sdist.sh
- name: Build Python bindings
if: ${{ runner.os != 'Windows' }}
shell: bash
@@ -291,7 +255,6 @@ jobs:
MUJOCO_PLUGIN_PATH="${{ matrix.tmpdir }}/mujoco_install/mujoco_plugin"
MUJOCO_CMAKE_ARGS="-DCMAKE_INTERPROCEDURAL_OPTIMIZATION:BOOL=OFF ${{ matrix.cmake_args }}"
pip wheel -v --no-deps mujoco-*.tar.gz
- name: Install Python bindings
if: ${{ runner.os != 'Windows' }}
shell: bash
@@ -299,7 +262,6 @@ jobs:
run: >
source ${{ matrix.tmpdir }}/venv/bin/activate &&
pip install --no-index mujoco-*.whl
- name: Test Python bindings
if: ${{ runner.os != 'Windows' }}
shell: bash
@@ -308,19 +270,6 @@ jobs:
run: >
source ${{ matrix.tmpdir }}/venv/bin/activate &&
pytest -v --pyargs mujoco
- name: Build and Test WASM bindings
if: ${{ runner.os == 'Linux' }}
shell: bash
run: |
source emsdk/emsdk_env.sh
export PATH="$(pwd)/node_modules/.bin:$PATH"
emcmake cmake -B build_wasm -DCMAKE_INTERPROCEDURAL_OPTIMIZATION:BOOL=OFF
cmake --build build_wasm
npm run test --prefix ./wasm
- name: Package MJX
if: ${{ runner.os != 'Windows' }}
shell: bash
@@ -328,7 +277,6 @@ jobs:
run:
source ${{ matrix.tmpdir }}/venv/bin/activate &&
python -m build .
- name: Install MJX
if: ${{ runner.os != 'Windows' }}
shell: bash
@@ -337,7 +285,6 @@ jobs:
source ${{ matrix.tmpdir }}/venv/bin/activate &&
pip install --require-hashes -r requirements.txt &&
pip install --no-index dist/mujoco_mjx-*.whl
- name: Test MJX
if: ${{ runner.os != 'Windows' }}
shell: bash
@@ -345,7 +292,6 @@ jobs:
run:
source ${{ matrix.tmpdir }}/venv/bin/activate &&
pytest -n auto -v -k 'not IntegrationTest' --pyargs mujoco.mjx
- name: Notify team chat
shell: bash
env:
@@ -378,4 +324,3 @@ jobs:
-X POST \
-H "Content-Type: application/json" \
--data-raw "${CHATMSG}"
-5
View File
@@ -37,8 +37,3 @@ MUJOCO_LOG.TXT
# Clang cache
.cache/
# JavaScript bindings build
wasm/**/dist/
**/node_modules/
+16 -42
View File
@@ -39,20 +39,14 @@ enable_language(CXX)
list(APPEND CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/cmake")
if(NOT EMSCRIPTEN)
option(MUJOCO_BUILD_EXAMPLES "Build samples for MuJoCo" ON)
option(MUJOCO_BUILD_SIMULATE "Build simulate library for MuJoCo" ON)
option(MUJOCO_BUILD_STUDIO "Build studio library for MuJoCo" OFF)
option(MUJOCO_BUILD_TESTS "Build tests for MuJoCo" ON)
option(MUJOCO_TEST_PYTHON_UTIL "Build and test utility libraries for Python bindings" ON)
option(MUJOCO_WITH_USD "Build with OpenUSD" OFF)
option(MUJOCO_USE_FILAMENT "Use filament rendering" OFF)
option(MUJOCO_USE_FILAMENT_VULKAN "Use vulkan backend for filament rendering" OFF)
endif()
if(EMSCRIPTEN)
option(MUJOCO_BUILD_TESTS_WASM "Build tests for WASM bindings" ON)
endif()
option(MUJOCO_BUILD_EXAMPLES "Build samples for MuJoCo" ON)
option(MUJOCO_BUILD_SIMULATE "Build simulate library for MuJoCo" ON)
option(MUJOCO_BUILD_STUDIO "Build studio library for MuJoCo" OFF)
option(MUJOCO_BUILD_TESTS "Build tests for MuJoCo" ON)
option(MUJOCO_TEST_PYTHON_UTIL "Build and test utility libraries for Python bindings" ON)
option(MUJOCO_WITH_USD "Build with OpenUSD" OFF)
option(MUJOCO_USE_FILAMENT "Use filament rendering" OFF)
option(MUJOCO_USE_FILAMENT_VULKAN "Use vulkan backend for filament rendering" OFF)
# Option to provide a path to an existing USD build directory or to Houdini HFS directory.
set(USD_DIR "" CACHE PATH "Path to an existing USD build directory.")
@@ -106,13 +100,7 @@ else()
set(MUJOCO_RESOURCE_FILES "")
endif()
# Emscripten does not support SHARED libs
if(NOT EMSCRIPTEN)
add_library(mujoco SHARED ${MUJOCO_RESOURCE_FILES})
else()
add_library(mujoco STATIC ${MUJOCO_RESOURCE_FILES})
endif()
add_library(mujoco SHARED ${MUJOCO_RESOURCE_FILES})
target_include_directories(
mujoco
PUBLIC $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
@@ -120,36 +108,27 @@ target_include_directories(
PRIVATE src
)
if(NOT EMSCRIPTEN)
add_subdirectory(plugin/elasticity)
add_subdirectory(plugin/actuator)
add_subdirectory(plugin/sensor)
add_subdirectory(plugin/sdf)
endif()
add_subdirectory(plugin/elasticity)
add_subdirectory(plugin/actuator)
add_subdirectory(plugin/sensor)
add_subdirectory(plugin/sdf)
add_subdirectory(src/engine)
add_subdirectory(src/user)
add_subdirectory(src/xml)
add_subdirectory(src/thread)
if(MUJOCO_USE_FILAMENT AND NOT EMSCRIPTEN)
if(MUJOCO_USE_FILAMENT)
# Note that, by default, the "src/render" and "src/ui" code is added directly
# into the "mujoco" target. However, "mujoco::filament" is a separate,
# explicit target. Therefore, if you want to use MuJoCo with Filament, you
# will need to explicitly add the "mujoco::filament" target as a link
# dependency in your project.
add_subdirectory(src/experimental/filament)
elseif(NOT EMSCRIPTEN)
else()
add_subdirectory(src/render)
add_subdirectory(src/ui)
endif()
if(EMSCRIPTEN)
add_subdirectory(wasm)
if(MUJOCO_BUILD_TESTS_WASM)
add_subdirectory(wasm/tests)
endif()
endif()
target_compile_definitions(mujoco PRIVATE _GNU_SOURCE CCD_STATIC_DEFINE MUJOCO_DLL_EXPORTS -DMC_IMPLEM_ENABLE)
if(MUJOCO_ENABLE_AVX_INTRINSICS)
@@ -256,14 +235,9 @@ if(BUILD_TESTING AND MUJOCO_BUILD_TESTS)
endif()
if(NOT (APPLE AND MUJOCO_BUILD_MACOS_FRAMEWORKS))
set(MUJOCO_TARGETS mujoco)
if (EMSCRIPTEN)
list(APPEND MUJOCO_TARGETS lodepng)
endif()
# Install the libraries.
install(
TARGETS ${MUJOCO_TARGETS}
TARGETS mujoco
EXPORT ${PROJECT_NAME}
RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}" COMPONENT runtime
LIBRARY DESTINATION "${CMAKE_INSTALL_LIBDIR}" COMPONENT runtime
+5 -1
View File
@@ -131,11 +131,15 @@ These packages give users of various languages access to MuJoCo functionality:
DeepMind's related environment stack, includes
[PyMJCF](https://github.com/google-deepmind/dm_control/blob/main/dm_control/mjcf/README.md),
a module for procedural manipulation of MuJoCo models.
- [JavaScript bindings and WebAssembly support](/wasm/README.md) (inspired [stillonearth](https://github.com/stillonearth) and [zalo](https://github.com/zalo)'s community projects).
- [C# bindings and Unity plug-in](https://mujoco.readthedocs.io/en/stable/unity.html)
#### Third-party bindings:
- **WebAssembly**: [mujoco_wasm](https://github.com/zalo/mujoco_wasm) by [@zalo](https://github.com/zalo) with contributions by
[@kevinzakka](https://github.com/kevinzakka), based on the [emscripten build](https://github.com/stillonearth/MuJoCo-WASM) by
[@stillonearth](https://github.com/stillonearth).
:arrow_right: [Click here](https://zalo.github.io/mujoco_wasm/) for a live demo of MuJoCo running in your browser.
- **MATLAB Simulink**: [Simulink Blockset for MuJoCo Simulator](https://github.com/mathworks-robotics/mujoco-simulink-blockset)
by [Manoj Velmurugan](https://github.com/vmanoj1996).
- **Swift**: [swift-mujoco](https://github.com/liuliu/swift-mujoco)
+1 -17
View File
@@ -109,11 +109,7 @@ if(NOT TARGET lodepng)
add_library(lodepng STATIC ${LODEPNG_HEADERS} ${LODEPNG_SRCS})
target_compile_options(lodepng PRIVATE ${MUJOCO_MACOS_COMPILE_OPTIONS})
target_link_options(lodepng PRIVATE ${MUJOCO_MACOS_LINK_OPTIONS})
if(NOT EMSCRIPTEN)
target_include_directories(lodepng PUBLIC ${lodepng_SOURCE_DIR})
else()
target_include_directories(lodepng PUBLIC $<BUILD_INTERFACE:${lodepng_SOURCE_DIR}> $<INSTALL_INTERFACE:include>)
endif()
target_include_directories(lodepng PUBLIC ${lodepng_SOURCE_DIR})
endif()
endif()
@@ -132,10 +128,6 @@ if(NOT TARGET marchingcubecpp)
endif()
set(QHULL_ENABLE_TESTING OFF)
# Patch changes in https://github.com/qhull/qhull/pull/173.patch
set(QHULL_PATCH_COMMAND
git apply --reject --whitespace=fix ${mujoco_SOURCE_DIR}/cmake/qhull-support-emscripten.patch
)
findorfetch(
USE_SYSTEM_PACKAGE
@@ -151,7 +143,6 @@ findorfetch(
TARGETS
qhull
EXCLUDE_FROM_ALL
PATCH_COMMAND ${QHULL_PATCH_COMMAND}
)
# MuJoCo includes a file from libqhull_r which is not exported by the qhull include directories.
# Add it to the target.
@@ -231,12 +222,6 @@ endif()
set(ENABLE_DOUBLE_PRECISION ON)
set(CCD_HIDE_ALL_SYMBOLS ON)
# Patch changes in https://github.com/danfis/libccd/pull/83.patch
set(CCD_PATCH_COMMAND
git apply --reject --whitespace=fix ${mujoco_SOURCE_DIR}/cmake/ccd-support-emscripten.patch
)
# update cmake_minimum_required version for compatibility with newer version of cmake
if(NOT DEFINED CMAKE_POLICY_VERSION_MINIMUM)
set(CMAKE_POLICY_VERSION_MINIMUM ${MUJOCO_CMAKE_MIN_REQ})
@@ -256,7 +241,6 @@ findorfetch(
TARGETS
ccd
EXCLUDE_FROM_ALL
PATCH_COMMAND ${CCD_PATCH_COMMAND}
)
if(CMAKE_POLICY_VERSION_MINIMUM_LOCALLY_DEFINED)
unset(CMAKE_POLICY_VERSION_MINIMUM)
-62
View File
@@ -1,62 +0,0 @@
# Copyright 2025 DeepMind Technologies Limited
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_SOURCE_DIR}/wasm/dist")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++20 -O3 -fexceptions")
set(CMAKE_INSTALL_PREFIX ${PROJECT_SOURCE_DIR}/wasm)
include_directories(${PROJECT_SOURCE_DIR}/include)
include_directories(${PROJECT_SOURCE_DIR}/src)
include_directories(${PROJECT_SOURCE_DIR}/wasm)
link_directories(${CMAKE_BINARY_DIR}/lib)
file(GLOB MUJOCO_WASM_FILES
"codegen/generated/*.cc"
"unpack.cc"
)
if(NOT MUJOCO_WASM_FILES)
message(FATAL_ERROR "No source files found in codegen/generated/")
endif()
add_compile_options(-pthread)
add_compile_options(-fexceptions)
# Set Emscripten linker flags
set(EMCC_LINKER_FLAGS
"--bind"
"-s ASSERTIONS=1"
"-s ALLOW_MEMORY_GROWTH=1"
"-s EXPORT_ES6=1"
"-s MODULARIZE=1"
"-s FORCE_FILESYSTEM=1"
"-s EXPORTED_RUNTIME_METHODS=['ccall','cwrap','FS','MEMFS']"
"-s EXPORT_NAME=loadMujoco"
"-s DISABLE_EXCEPTION_CATCHING=0"
"-gsource-map"
"-g"
"--emit-tsd mujoco_wasm.d.ts"
)
string (REPLACE ";" " " EMCC_LINKER_FLAGS_STR "${EMCC_LINKER_FLAGS}")
add_executable(mujoco_wasm ${MUJOCO_WASM_FILES})
set_target_properties(mujoco_wasm PROPERTIES LINK_FLAGS "${EMCC_LINKER_FLAGS_STR}")
target_link_libraries(mujoco_wasm ccd lodepng mujoco tinyxml2 qhullstatic_r)
install(TARGETS mujoco_wasm DESTINATION ${DIVISIBLE_INSTALL_BIN_DIR})
-219
View File
@@ -1,219 +0,0 @@
# MuJoCo JavaScript Bindings
> [!CAUTION]
> **These bindings are not yet ready for general use. They have been added
> without announcement while we develop the CI we need to accept pull
> requests.**
These are the canonical JavaScript and TypeScript bindings for the MuJoCo
physics engine.
This package provides a high-level API that allows you to interact with the core
MuJoCo engine compiled into a high-performance WebAssembly (WASM) module. These
bindings are developed and maintained by Google DeepMind and are always up to
date with the latest developments in MuJoCo. For brevity, the documentation
below will often refer to “JavaScript” but the concepts apply equally to
TypeScript.
> [!IMPORTANT]
> _These bindings are still a WIP. For details, see the [Future Work](#future-work)
> section. Also note that development has primarily taken place on Linux using
> Google Chrome. If you're working on a different OS or browser, you may
> encounter some rough edges._
## Prerequisites
> [!NOTE]
> Run all the commands in this README from the top-level directory.
- To compile the [`bindings.cc`](codegen/generated/bindings.cc) file, which
generates the `.wasm` WebAssembly file, `.js` JavaScript import, and `.d.ts`
TypeScript declaration file, you will need Emscripten SDK version `4.0.10`.
Later versions may work but are untested. To set up the SDK, do the
following, you can run this anywhere but the rest of the commands in this
README only work in the shell where you source the `emsdk_env.sh` script.
```sh
git clone https://github.com/emscripten-core/emsdk.git
./emsdk/emsdk install 4.0.10
./emsdk/emsdk activate 4.0.10
source ./emsdk/emsdk_env.sh
```
- To easily run the JavaScript tests and the demo application, `node` and `npm`
are required. We recommend managing these using
[nvm](https://github.com/nvm-sh/nvm). There are also various JavaScript
dependencies needed for the tests, demo, and bindings build process. These
dependencies are expected to be located in the `wasm` folder. To install
them and ensure they can be found by later commands, run the following:
```sh
npm install --prefix ./wasm
export PATH="$(pwd)/wasm/node_modules/.bin:$PATH"
```
- To modify the bindings `python3` is required because the [`bindings.cc`](codegen/generated/bindings.cc)
file is generated by a Python script. To run the bindings generator tests,
`absl` is required and `pytest` will be helpful. Set up a Python environment
with these dependencies as follows:
```sh
python3 -m venv .venv
source .venv/bin/activate
pip install -r python/build_requirements.txt
```
> [!TIP]
> _Emscripten is well-documented. We recommend reading the sections covering the
> [Emscripten Compiler Settings](https://emscripten.org/docs/tools_reference/settings_reference.html),
> the [Emscripten SDK](https://emscripten.org/docs/tools_reference/emsdk.html),
> and the [Embind](https://emscripten.org/docs/porting/connecting_cpp_and_javascript/embind.html)
> library. To understand the limitations and caveats related to using the
> browser as a platform, see the
> [Porting](https://emscripten.org/docs/porting/index.html#porting) section._
## User Guide
### Bindings Generation
The [`bindings.cc`](codegen/generated/bindings.cc) file is compiled to generate
to `.wasm` WebAssembly file, `.js` JavaScript import, and `.d.ts` TypeScript
declaration file. These are the files you'll use to call MuJoCo from JavaScript.
To generate them ensure the npm and Emscripten SDK prerequisites are set up and
then run the following:
```sh
emcmake cmake -B build && cmake --build build
```
This command will generate the following folders under the project root:
- `build`: contains MuJoCo compiled using Emscripten.
- `wasm/dist`: contains the WebAssembly module, `.js` and `.d.ts` files.
### Example Application
After generating the bindings you will be ready to write web applications using
MuJoCo. We have provided a basic web application that uses Three.js to render a
simple simulation, to try it run this command:
```sh
npm run dev:demo --prefix ./wasm
```
You may prefer to write your entire app in C++ and compile it using Emscripten.
If you do this, you wont need to use these bindings, since youll be writing
minimal JavaScript, and the granularity of these bindings may be inappropriate
(e.g., you might want to call multiple MuJoCo functions in the C++ callback
invoked by `requestAnimationFrame`).
We have also found that a hybrid approach can be helpful, as it is often more
convenient to work with browser APIs directly in JavaScript. If you choose to
write your application in C++ and compile it using Emscripten, you may want to
copy a subset of the `EMSCRIPTEN_BINDINGS` from `bindings.cc` into your
applications source file.
## Development
In order to change the bindings you will need to change the [`bindings.cc`](codegen/generated/bindings.cc)
file but this should not be done manually. The file is generated using the
Python scripts and template files in the [`codegen`](codegen) folder, to edit
the bindings you will need to change those files and re-generate [`bindings.cc`](codegen/generated/bindings.cc)
using this command:
```sh
PYTHONPATH=python/mujoco python3 -m wasm.codegen.update
```
The codegen scripts use MuJoCos Python introspect library to generate the
Embind `EMSCRIPTEN_BINDINGS` block that binds C++ functions and classes to
JavaScript. The functions and classes that are bound are wrappers around
MuJoCo's C API. These wrappers provide a convenient place to add features like
bounds checking and nice error reporting.
### Testing
1. **JavaScript API tests.**
These verify that a wide variety of MuJoCo functions and classes work
correctly when called from JavaScript. There are also preliminary benchmarks
for JavaScript/C++ shared memory buffers. The enums test is special because
it is generated by a Python script. Run the tests as follows:
```sh
npm run test --prefix ./wasm
```
2. **Bindings generator tests.**
These are relevant when developing or extending the bindings. The following
command finds and runs all `test_*.py` or `*_test.py` files in the `wasm`
folder:
```sh
PYTHONPATH=python/mujoco python3 -m pytest ./wasm
```
> [!NOTE]
> If you add/edit an enum in MuJoCo you will need to run the following command
> to re-creates the [`enums_tests.ts`](tests/enums_tests.ts) file which checks
> that all the enums in the API are bound:
>
> ```sh
> PYTHONPATH=python/mujoco python3 -m wasm.codegen.enums_test_generator
> ```
### Debugging
We provide a “sandbox” app where you can quickly write code to run in your
browser. Write your code in the [`main.ts`](tests/sandbox/main.ts) file and use
the following command to execute it in your browser:
```sh
npm run dev:sandbox --prefix ./wasm
```
The page will be blank since the script only logs to the console output. You
can add your code at the indicated placeholder and use Chrome DevTools for
debugging. It is possible to set up a debug workflow where stack traces and
stepping through code across language boundaries work correctly. Our current
method to do this only works internally at Google, but it should be possible to
replicate the experience with open-source tooling — community suggestions are
welcome!
## Future Work
1. **Bind all useful APIs.**
These bindings are not yet complete. While the main MuJoCo APIs (`mj_step`,
`mj_loadXML`, etc.) are well tested, other APIs (e.g., functions from
`mjspec.h`) remain untested in real web applications (though test code for
the `mjspec` bindings does exist). One notable feature not yet supported in
the WASM bindings, which has proved very useful in the Python bindings, is
named access methods — where data distributed across multiple arrays in C can
be conveniently accessed by name, e.g., `model.geom('mygeom')` or
`data.joint('myjoint')`. Currently, this data must be accessed via the
`mj_name2id` function. Adding support for these features is a high priority,
as it affects user code written in JavaScript.
2. **Improve the developer experience.**
There is still work to be done to improve the developer experience when
developing the WASM bindings. The most obvious issue is that bindings
generation is not yet fully automated. As a result, it is currently less
convenient than we'd like to identify and apply the changes needed to update
the bindings. The goal is to eventually automate all binding code generation
and clearly communicate what changes are required in the WASM bindings as a
result of C++ updates. This problem should only affect developers working on
the MuJoCo engine in C++, not end users writing JavaScript.
3. **Improve the documentation.**
The documentation in this README will eventually be merged into the main
MuJoCo documentation once the bindings are complete and named access is
implemented. We also intend to review the bindings APIs and make adjustments
to minimize differences with the Python bindings (while respecting language
idioms) to reduce the amount of additional documentation required.
4. **Improve the [example](#example-application).**
We aim to provide an example application that can be easily modified and
embedded into a paper project page (see [this example](https://kzakka.com/robopianist/)).
This could be achieved by extending the Three.js example or by compiling the
MuJoCo toolbox C++ code using the Emscripten toolchain. Community suggestions
and contributions are welcome!
-115
View File
@@ -1,115 +0,0 @@
# Copyright 2025 DeepMind Technologies Limited
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Builds WASM bindings for MuJoCo."""
from introspect import enums as introspect_enums
from introspect import functions as introspect_functions
from wasm.codegen.generators import enums
from wasm.codegen.generators import functions
from wasm.codegen.generators import structs
from wasm.codegen.helpers import common
from wasm.codegen.helpers import constants as _constants
from wasm.codegen.helpers import function_utils
class BindingBuilder:
"""Builds WASM bindings for MuJoCo."""
def __init__(
self,
template_path_h: str,
template_path_cc: str,
generated_path_h: str,
generated_path_cc: str,
):
self.generated_path_h = generated_path_h
self.generated_path_cc = generated_path_cc
with open(template_path_h, "r") as f:
self.content_h = f.readlines()
with open(template_path_cc, "r") as f:
self.content_cc = f.readlines()
filtered_functions = {
name: func
for name, func in introspect_functions.FUNCTIONS.items()
if not function_utils.is_excluded_function_name(name)
and name not in _constants.BOUNDCHECK_FUNCS
}
self.enums_generator = enums.Generator(introspect_enums.ENUMS)
self.functions_generator = functions.Generator(filtered_functions)
self.structs_generator = structs.Generator()
def set_enums(self):
"""Generates and sets the enum bindings."""
enum_bindings = self.enums_generator.generate()
self.content_cc = common.replace_lines_containing_marker(
self.content_cc,
"// {{ ENUM_BINDINGS }}",
enum_bindings,
)
return self
def set_headers(self):
"""Generates and sets the struct definitions."""
struct_hdr_markers_and_content = self.structs_generator.generate_header()
for marker, content in struct_hdr_markers_and_content:
self.content_h = common.replace_lines_containing_marker(
self.content_h, marker, content
)
return self
def set_structs(self):
"""Generates and sets the struct bindings."""
struct_src_markers_and_content = (
self.structs_generator.generate_source()
)
for marker, content in struct_src_markers_and_content:
self.content_cc = common.replace_lines_containing_marker(
self.content_cc, marker, content
)
return self
def set_functions(self):
"""Generates and sets the function wrappers and bindings."""
wrapper_functions, function_bindings = (
self.functions_generator.generate()
)
self.content_cc = common.replace_lines_containing_marker(
self.content_cc,
"// {{ WRAPPER_FUNCTIONS }}",
wrapper_functions,
)
self.content_cc = common.replace_lines_containing_marker(
self.content_cc,
"// {{ FUNCTION_BINDINGS }}",
function_bindings,
)
return self
def build(self):
"""Writes the generated content to the output files."""
common.write_to_file(self.generated_path_h, "".join(self.content_h))
common.write_to_file(self.generated_path_cc, "".join(self.content_cc))
def to_string_header(self) -> str:
return "".join(self.content_h)
def to_string_source(self) -> str:
return "".join(self.content_cc)
-67
View File
@@ -1,67 +0,0 @@
# Copyright 2025 DeepMind Technologies Limited
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from pathlib import Path
from absl.testing import absltest
from wasm.codegen import binding_builder
ERROR_MESSAGE = """
The file '{}' needs to be updated, please run:
update.py as described in wasm/README.md""".lstrip()
class BindingsDiffTest(absltest.TestCase):
def setUp(self):
super().setUp()
SCRIPT_DIR = Path(__file__).parent
with open(SCRIPT_DIR / 'generated/bindings.h', 'r') as f:
self.generated_hdr = f.read()
with open(SCRIPT_DIR / 'generated/bindings.cc', 'r') as f:
self.generated_src = f.read()
self.template_path_h = SCRIPT_DIR / 'templates/bindings.h'
self.template_path_cc = SCRIPT_DIR / 'templates/bindings.cc'
self.generated_path_h = SCRIPT_DIR / 'generated/bindings.h'
self.generated_path_cc = SCRIPT_DIR / 'generated/bindings.cc'
self.builder = binding_builder.BindingBuilder(
self.template_path_h,
self.template_path_cc,
self.generated_path_h,
self.generated_path_cc,
)
def test_bindings_source(self):
generator_output = (self.builder.set_enums().set_structs().set_functions().
to_string_source())
self.assertEqual(
generator_output,
self.generated_src,
msg=ERROR_MESSAGE.format('bindings.cc'),
)
def test_bindings_header(self):
generator_output = (self.builder.set_headers().to_string_header())
self.assertEqual(
generator_output,
self.generated_hdr,
msg=ERROR_MESSAGE.format('bindings.h'),
)
if __name__ == '__main__':
absltest.main()
-152
View File
@@ -1,152 +0,0 @@
# Copyright 2025 DeepMind Technologies Limited
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Tests to ensure that all Mujoco functions and structs are correctly handled.
This file contains tests that verify:
- All functions defined in Mujoco's introspect module are either bound in the
generated bindings.cc file or explicitly excluded in constants.py.
- All structs defined in Mujoco's introspect module are either bound in the
generated bindings.cc file or explicitly skipped in SKIPPED_STRUCTS in
constants.py.
These tests help maintain the integrity of the generated WASM bindings by
ensuring that no functions or structs are accidentally missed or incorrectly
handled during the code generation process.
"""
from pathlib import Path
import re
from absl.testing import absltest
from introspect import functions as introspect_functions
from introspect import structs as introspect_structs
from wasm.codegen.helpers import common
from wasm.codegen.helpers import constants
from wasm.codegen.helpers import function_utils
def _get_resource_content(file_path: str) -> str:
"""Reads resource file content using resources.GetResource."""
try:
with open(file_path, 'r') as f:
return f.read()
except FileNotFoundError:
print(f'Warning: Resource {file_path} not found.')
return ''
except IOError as e:
print(f'Error reading resource {file_path}: {e}')
return ''
def _get_bound_functions_from_cc() -> set[str]:
"""Reads bindings.cc and extracts the names of bound functions."""
content = _get_resource_content(
Path(__file__).parent / 'generated/bindings.cc'
)
if not content:
return set()
bound_functions = set()
# Find all strings within function("...") calls.
matches = re.findall(r'function\("([^"]+)"', content)
bound_functions.update(matches)
return bound_functions
def _get_bound_structs_from_cc() -> set[str]:
"""Reads bindings.cc and extracts the names of bound structs."""
content = _get_resource_content(
Path(__file__).parent / 'generated/bindings.cc'
)
if not content:
return set()
bound_structs = set()
# Find all strings within class_<...>("...") calls.
matches = re.findall(r'class_<[^>]+>\("([^"]+)"\)', content)
bound_structs.update(matches)
return bound_structs
class BindingCoverageTest(absltest.TestCase):
def test_function_coverage(self):
"""Asserts that each function is either excluded or bound."""
all_functions = set(introspect_functions.FUNCTIONS.keys())
excluded_functions = {
name
for name in all_functions
if function_utils.is_excluded_function_name(name)
}
bound_functions = _get_bound_functions_from_cc()
missing_functions = []
for func_name in all_functions:
if (
func_name not in excluded_functions
and func_name not in bound_functions
):
missing_functions.append(func_name)
if missing_functions:
error_message = (
f"""The following functions from functions.py are neither excluded in
constants.py nor bound in bindings.cc:
{", ".join(sorted(missing_functions))}
Please either add them to a exclusion list in
constants.py or create a binding in bindings.cc."""
)
self.fail(error_message)
def test_struct_coverage(self):
"""Asserts that each struct is either not bound or bound in structs.cc."""
bound_structs = _get_bound_structs_from_cc()
all_structs = {
common.uppercase_first_letter(struct_name)
for struct_name in introspect_structs.STRUCTS.keys()
}
skipped_structs = {
common.uppercase_first_letter(struct_name)
for struct_name in constants.SKIPPED_STRUCTS
}
missing_structs = []
for struct_name in all_structs:
if (
struct_name not in skipped_structs
and struct_name not in bound_structs
):
missing_structs.append(struct_name)
error_messages = []
if missing_structs:
error_messages.append(
f"""The following structs are defined in structs.py but are neither
bound in bindings.cc nor listed in SKIPPED_STRUCTS:
{", ".join(sorted(missing_structs))}
Please either add them to SKIPPED_STRUCTS or create its binding
in bindings.cc."""
)
if error_messages:
self.fail('\n\n'.join(error_messages))
if __name__ == '__main__':
absltest.main()
-54
View File
@@ -1,54 +0,0 @@
# Copyright 2025 DeepMind Technologies Limited
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Generates TypeScript code that tests Mujoco enums."""
import textwrap
from introspect import enums as introspect_enums
from wasm.codegen.helpers import common
def generate_typescript_enum_tests():
"""Generates TypeScript code that tests Mujoco enums."""
output = textwrap.dedent("""\
import 'jasmine';
import { MainModule } from "../dist/mujoco_wasm"
import loadMujoco from "../dist/mujoco_wasm.js"
let mujoco: MainModule;
describe('Enums', () => {
beforeAll(async () => {
mujoco = await loadMujoco();
});
""")
for enum_decl in introspect_enums.ENUMS.values():
enum_name = enum_decl.name
output += f"""
it('{enum_name} should exist', () => {{
expect(mujoco.{enum_name}).toBeDefined();
}});\n"""
output += "});\n"
return output
if __name__ == "__main__":
ts_test_code = generate_typescript_enum_tests()
output_file = 'wasm/tests/enums_test.ts'
common.write_to_file(output_file, ts_test_code)
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
-34
View File
@@ -1,34 +0,0 @@
# Copyright 2025 DeepMind Technologies Limited
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Generator for the constants."""
from wasm.codegen.helpers import common
# TODO(manevi): Delete this file and use the genrule to handle the file copying
class Generator:
"""Generator for the constants."""
def run(self):
"""Runs the generator."""
template_cc_file, output_cc_file = common.get_file_path(
"templates", "generated", "constants.cc"
)
with open(template_cc_file, "r") as f_template:
template_content = f_template.read()
with open(output_cc_file, "w") as f_output:
f_output.write(template_content)
-47
View File
@@ -1,47 +0,0 @@
# Copyright 2025 DeepMind Technologies Limited
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Generates Embind bindings for MuJoCo enums."""
from typing import Mapping
from introspect import ast_nodes
from wasm.codegen.helpers import code_builder
class Generator:
"""Generates Embind code for MuJoCo enums."""
def __init__(self, enums: Mapping[str, ast_nodes.EnumDecl]):
self.enums = enums
def _generate_enum_binding(self, enum: ast_nodes.EnumDecl) -> str:
"""Generates the Embind code for a single enum."""
code = f'{code_builder.INDENT}enum_<{enum.name}>("{enum.name}")'
for value_name in enum.values:
code += f'\n{2*code_builder.INDENT}.value("{value_name}", {value_name})'
code += ";"
return code
def generate(self) -> str:
"""Generates all Embind code for the provided enums."""
code = []
for enum in self.enums.values():
code.append(self._generate_enum_binding(enum))
return "\n\n".join(code) + "\n"
-62
View File
@@ -1,62 +0,0 @@
# Copyright 2025 DeepMind Technologies Limited
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from absl.testing import absltest
from introspect import ast_nodes
from wasm.codegen.generators import enums
class EnumsGeneratorTest(absltest.TestCase):
def test_generate_enum_bindings(self):
generator = enums.Generator({
"TestEnum": ast_nodes.EnumDecl(
name="TestEnum",
declname="enum TestEnum_",
values={"FIRST_VAL": 0, "SECOND_VAL": 1, "THIRD_VAL": 2},
),
"AnotherEnum": ast_nodes.EnumDecl(
name="AnotherEnum",
declname="enum AnotherEnum_",
values={"ALPHA": 100, "BETA": 200},
),
"EmptyEnum": ast_nodes.EnumDecl(
name="EmptyEnum",
declname="enum EmptyEnum_",
values={},
),
})
expected_code = """ enum_<TestEnum>("TestEnum")
.value("FIRST_VAL", FIRST_VAL)
.value("SECOND_VAL", SECOND_VAL)
.value("THIRD_VAL", THIRD_VAL);
enum_<AnotherEnum>("AnotherEnum")
.value("ALPHA", ALPHA)
.value("BETA", BETA);
enum_<EmptyEnum>("EmptyEnum");
"""
actual_code = generator.generate()
self.assertEqual(actual_code, expected_code)
if __name__ == "__main__":
absltest.main()
-93
View File
@@ -1,93 +0,0 @@
# Copyright 2025 DeepMind Technologies Limited
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Generates Embind bindings for MuJoCo functions."""
import pathlib
from typing import List, Mapping, TypeAlias
from introspect import ast_nodes
from wasm.codegen.helpers import code_builder
from wasm.codegen.helpers import function_utils
FunctionDecl: TypeAlias = ast_nodes.FunctionDecl
FunctionParameterDecl: TypeAlias = ast_nodes.FunctionParameterDecl
PointerType: TypeAlias = ast_nodes.PointerType
ValueType: TypeAlias = ast_nodes.ValueType
Path: TypeAlias = pathlib.Path
class Generator:
"""Generates Embind bindings for MuJoCo functions."""
def __init__(self, functions: Mapping[str, FunctionDecl]):
self.direct_bind_functions: List[FunctionDecl] = []
self.wrapper_bind_functions: List[FunctionDecl] = []
for func in functions.values():
if function_utils.should_be_wrapped(func):
self.wrapper_bind_functions.append(func)
else:
self.direct_bind_functions.append(func)
def _generate_wrappers(self) -> str:
"""Generates Embind bindings for all functions that need wrappers."""
code = []
for func in self.wrapper_bind_functions:
wrapper_code = function_utils.generate_function_wrapper(func)
code.append(wrapper_code)
return "\n\n".join(code)
def _generate_direct_bindable_functions(self) -> str:
"""Generates Embind bindings for all directly bindable functions."""
result = ""
for func in self.direct_bind_functions:
result += code_builder.INDENT
result += self._generate_function_binding(func)
return result
def _generate_function_binding(
self, func: FunctionDecl, is_wrapper=False
) -> str:
"""Generates the Embind code for a single function."""
js_name, cpp_func = func.name, func.name
if is_wrapper:
cpp_func += "_wrapper"
return f'function("{js_name}", &{cpp_func});\n'
def _generate_wrapper_bindable_functions(self) -> str:
"""Generates Embind bindings for all functions that need wrappers."""
result = ""
for func in self.wrapper_bind_functions:
result += code_builder.INDENT
result += self._generate_function_binding(func, True)
return result
def generate(self) -> tuple[str, str]:
"""Generates the bindings file for all functions."""
wrapper_functions = self._generate_wrappers()
function_bindings = self._generate_direct_bindable_functions()
function_bindings += self._generate_wrapper_bindable_functions()
return wrapper_functions, function_bindings
-67
View File
@@ -1,67 +0,0 @@
# Copyright 2025 DeepMind Technologies Limited
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from absl.testing import absltest
from introspect import ast_nodes
from wasm.codegen.generators import functions
class FunctionsGeneratorTest(absltest.TestCase):
def setUp(self):
super().setUp()
self.generator = functions.Generator({})
self.int_type = ast_nodes.ValueType(name="int")
def test_generate_function_binding_simple_case(self):
func_simple_void = ast_nodes.FunctionDecl(
name="do_nothing",
return_type=ast_nodes.ValueType(name="void"),
parameters=tuple(),
doc="doc",
)
self.assertEqual(
self.generator._generate_function_binding(func_simple_void),
'function("do_nothing", &do_nothing);\n',
)
def test_generate_direct_bindable_functions_simple_filter(self):
direct_bind = ast_nodes.FunctionDecl(
name="direct_bind",
return_type=self.int_type,
parameters=(
ast_nodes.FunctionParameterDecl(name="val", type=self.int_type),
),
doc="doc",
)
needs_wrap = ast_nodes.FunctionDecl(
name="needs_wrap",
return_type=ast_nodes.PointerType(inner_type=self.int_type),
parameters=tuple(),
doc="doc",
)
self.generator = functions.Generator({
"direct1": direct_bind,
"wrapped1": needs_wrap,
})
generated_code = self.generator._generate_direct_bindable_functions()
self.assertIn('function("direct_bind", &direct_bind);\n', generated_code)
self.assertNotIn('function("needs_wrap", &needs_wrap);\n', generated_code)
if __name__ == "__main__":
absltest.main()
-88
View File
@@ -1,88 +0,0 @@
# Copyright 2025 DeepMind Technologies Limited
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Generates Embind bindings for MuJoCo structs."""
from typing import Optional
from wasm.codegen.helpers import constants
from wasm.codegen.helpers import structs_parser
from wasm.codegen.helpers import structs_wrappers_data
class Generator:
"""Generates C++ code for binding and wrapping MuJoCo structs."""
def __init__(self):
# Set up the correct input dict based on the structs we want to bind
# and already have a wrapper manually created in the template/bindings.cc
wrapped_structs = structs_wrappers_data.create_wrapped_structs_set_up_data(
constants.STRUCTS_TO_BIND
)
# Traverse the introspect dictionary to get the field
# wrapper/bindings statements set up for each struct
self.structs_to_bind_data = structs_parser.generate_wasm_bindings(
wrapped_structs
)
def generate_header(
self
) -> list[tuple[str, list[Optional[str]]]]:
"""Generates C++ header file for binding and wrapping MuJoCo structs."""
autogenned_struct_definitions = []
markers_and_content = []
# Sort by struct name by dependency to ensure deterministic output order
sorted_struct_names = structs_parser.sort_structs_by_dependency(
constants.STRUCTS_TO_BIND
)
for struct_name in sorted_struct_names:
struct_data = self.structs_to_bind_data[struct_name]
if struct_data.wrapped_header:
autogenned_struct_definitions.append(
struct_data.wrapped_header + "\n"
)
else:
markers_and_content.append((
f"// INSERT-GENERATED-{struct_data.wrap_name}-DEFINITIONS",
[
l.definition if l.definition else ""
for l in struct_data.wrapped_fields
],
))
markers_and_content.append((
"// {{ AUTOGENNED_STRUCT_DEFINITIONS }}",
autogenned_struct_definitions,
))
return markers_and_content
def generate_source(self) -> list[tuple[str, list[str]]]:
"""Generates C++ source file for binding and wrapping MuJoCo structs."""
constructors = [
(
f"// INSERT-GENERATED-{struct_data.wrap_name}-CONSTRUCTOR",
[struct_data.wrapped_source],
)
for _, struct_data in self.structs_to_bind_data.items()
]
properties = [
(
f"// INSERT-GENERATED-{struct_data.wrap_name}-BINDINGS",
[l.binding for l in struct_data.wrapped_fields],
)
for _, struct_data in self.structs_to_bind_data.items()
]
return constructors + properties
-73
View File
@@ -1,73 +0,0 @@
# Copyright 2025 DeepMind Technologies Limited
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Helper class to build code string line by line with indentation."""
INDENT = " "
class CodeBuilder:
"""Helper class to build code string line by line with indentation."""
def __init__(self, indent_str: str = INDENT):
self._lines = []
self._indent_level = 0
self._indent_str = indent_str
def line(self, line_content: str) -> None:
"""Adds a line with indentation, special-casing "private:" and "public:"."""
indent = self._indent_str * self._indent_level
content = line_content.strip()
if content == "private:" or content == "public:":
self._lines.append(indent[:-1] + line_content)
elif content:
self._lines.append(indent + line_content)
else:
self._lines.append("")
def to_string(self) -> str:
"""Returns the complete code string."""
return "\n".join(self._lines)
class IndentBlock:
"""Helper class to manage indentation within a `with` statement."""
def __init__(self, builder: "CodeBuilder", header_line=""):
self._builder = builder
self._header_line = header_line
def __enter__(self):
line = self._header_line
line += " {" if line else "{"
self._builder.line(line)
self._builder._indent_level += 1
return self._builder
def __exit__(self, exc_type, exc_val, exc_tb):
if self._builder._indent_level > 0:
self._builder._indent_level -= 1
self._builder.line("}")
def block(self, header_line="") -> IndentBlock:
"""Creates a block including braces and an optional header before the opening brace.
Use via a `with` statement.
Args:
header_line: Optional header line to add before the opening brace.
Returns:
An IndentBlock instance that manages the indentation.
"""
return self.IndentBlock(self, header_line)
-51
View File
@@ -1,51 +0,0 @@
# Copyright 2025 DeepMind Technologies Limited
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Tests for the code_builder module."""
from absl.testing import absltest
from wasm.codegen.helpers import code_builder
class CodeBuilderTest(absltest.TestCase):
def test_code_builder_functionality(self):
"""Test nested indentation blocks."""
builder = code_builder.CodeBuilder(indent_str=" ")
builder.line("let a = 1")
with builder.block("function myFunc()"):
builder.line("let flag = true")
with builder.block("while (flag)"):
builder.line("a++")
builder.line("flag = a < 10")
builder.line("return a")
builder.line("print('Done')")
expected_lines = [
"let a = 1",
"function myFunc() {",
" let flag = true",
" while (flag) {",
" a++",
" flag = a < 10",
" }",
" return a",
"}",
"print('Done')",
]
self.assertEqual(builder.to_string(), "\n".join(expected_lines))
if __name__ == "__main__":
absltest.main()
-128
View File
@@ -1,128 +0,0 @@
# Copyright 2025 DeepMind Technologies Limited
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Utility functions for code generation."""
import os
import pathlib
from wasm.codegen.helpers import constants
Path = pathlib.Path
def get_default_output_dir() -> str:
"""Gets the default output directory (sibling of 'generated' folder)."""
# Get the directory of the current file (generator/base.py)
current_dir = Path(__file__).parent
# Go up one level to the project root and then down to 'generated'
default_output_dir = str(current_dir.parent / "generated")
return default_output_dir
def get_file_path(
template_dir: str, output_dir: str, filename: str
) -> tuple[str, str]:
"""Constructs the template and output file paths.
Args:
template_dir: The directory containing the template files.
output_dir: The directory where the generated files will be saved.
filename: The name of the file.
Returns:
A tuple containing the template file path and the output file path.
"""
template_file = f"wasm/codegen/{template_dir}/{filename}"
output_file = f"wasm/codegen/{output_dir}/{filename}"
return template_file, output_file
def write_to_file(filepath: str, content: str) -> None:
"""Writes content to a file."""
output_dir = os.path.dirname(filepath)
try:
if output_dir:
os.makedirs(output_dir, exist_ok=True)
with open(filepath, "w") as f:
chars = f.write(content)
print(f"wrote {chars} characters to file '{filepath}'")
except IOError as e:
print(f"Error writing to output file: {filepath} - {e}")
def uppercase_first_letter(input_string: str) -> str:
"""Uppercases the first letter of a string."""
if input_string:
return input_string[0].upper() + input_string[1:]
return input_string
def try_cast_to_scalar_type(value: str) -> int | float | str:
"""Tries to cast a string to an integer, then a float, otherwise returns the original string."""
for type_ in [int, float]:
try:
return type_(value)
except ValueError:
continue
return value
def debug_print(msg: str):
"""Prints a message to the console if STRUCT_DEBUG_MODE is enabled."""
if constants.STRUCT_DEBUG_MODE:
print(msg)
def replace_lines_containing_marker(
lines: list[str],
marker_to_replace: str,
replacement_content: str | list[str],
) -> list[str]:
"""Replaces lines containing a specific marker with new content."""
new_lines = []
replaced = False
for line in lines:
if not replaced and marker_to_replace in line:
indentation = _get_indentation(line)
if isinstance(replacement_content, str):
new_lines.append(indentation + replacement_content)
elif isinstance(replacement_content, list):
for content_line in replacement_content:
if not content_line.strip():
continue
indented_line = (
indentation
+ content_line.replace("\n", "\n" + indentation)
+ "\n"
)
new_lines.append(indented_line)
replaced = True
else:
new_lines.append(line)
return new_lines
def _get_indentation(line: str) -> str:
"""Returns the indentation of the given line as a string of spaces."""
indentation = ""
for char in line:
if char == " ":
indentation += " "
else:
break
return indentation
-37
View File
@@ -1,37 +0,0 @@
# Copyright 2025 DeepMind Technologies Limited
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from absl.testing import absltest
from wasm.codegen.helpers import common
class CommonUtilsTest(absltest.TestCase):
def test_uppercase_first_letter(self):
self.assertEqual(common.uppercase_first_letter(""), "")
self.assertEqual(common.uppercase_first_letter("hello"), "Hello")
self.assertEqual(common.uppercase_first_letter("1st place"), "1st place")
self.assertEqual(common.uppercase_first_letter("!wow"), "!wow")
self.assertEqual(
common.uppercase_first_letter(" leading space"), " leading space"
)
def test_try_cast_to_scalar_type(self):
self.assertEqual(common.try_cast_to_scalar_type("123"), 123)
self.assertEqual(common.try_cast_to_scalar_type("123.456"), 123.456)
self.assertEqual(common.try_cast_to_scalar_type("abc"), "abc")
if __name__ == "__main__":
absltest.main()
-474
View File
@@ -1,474 +0,0 @@
# Copyright 2025 DeepMind Technologies Limited
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Constants used in the code generation process."""
from introspect import structs as introspect_structs
PRIMITIVE_TYPES = {
# go/keep-sorted start
"char",
"double",
"float",
"int",
"mjtByte",
"mjtMeshBuiltin",
"mjtNum",
"mjtObj", # Adding this to the primitives because it is used as int,
"mjtSize",
"size_t",
"uint64_t",
"uintptr_t",
"unsigned char",
"unsigned int",
"void",
# go/keep-sorted end
}
_PLUGIN_FUNCTIONS = [
# go/keep-sorted start
"mj_getPluginConfig",
"mj_loadAllPluginLibraries",
"mj_loadPluginLibrary",
"mjc_distance",
"mjc_getSDF",
"mjc_gradient",
"mjp_defaultPlugin",
"mjp_defaultResourceProvider",
"mjp_getPlugin",
"mjp_getPluginAtSlot",
"mjp_getResourceProvider",
"mjp_getResourceProviderAtSlot",
"mjp_pluginCount",
"mjp_registerPlugin",
"mjp_registerResourceProvider",
"mjp_resourceProviderCount",
# go/keep-sorted end
]
# Functions that are bound as class methods
_CLASS_METHODS = [
# go/keep-sorted start
"mj_compile",
"mj_copyData",
"mj_copyModel",
"mj_copySpec",
"mj_deleteData",
"mj_deleteModel",
"mj_deleteSpec",
"mj_loadXML",
"mj_makeData",
"mj_makeSpec",
"mj_parseXML", # TODO(manevi): Bind this function.
"mj_parseXMLString",
"mj_recompile", # TODO(manevi): Bind this function.
"mj_saveXML", # TODO(manevi): Bind this function.
"mj_saveXMLString", # TODO(manevi): Bind this function.
# go/keep-sorted end
]
# Omitted because not very useful
_WRITABLE_ERROR = [
"mj_printSchema",
]
# Omitted thread management functions
_THREAD_FUNCTIONS = [
# go/keep-sorted start
"mju_bindThreadPool",
"mju_defaultTask",
"mju_taskJoin",
"mju_threadPoolCreate",
"mju_threadPoolDestroy",
"mju_threadPoolEnqueue",
# go/keep-sorted end
]
# Omitted asset cache functions
_ASSET_CACHE_FUNCTIONS = [
# go/keep-sorted start
"mj_clearCache",
"mj_getCache",
"mj_getCacheCapacity",
"mj_getCacheSize",
"mj_setCacheCapacity",
# go/keep-sorted end
]
# Omitted Virtual Filesystem (VFS) functions
_VFS_FUNCTIONS = [
# go/keep-sorted start
"mj_addBufferVFS",
"mj_addFileVFS",
"mj_defaultVFS",
"mj_deleteFileVFS",
"mj_deleteVFS",
# go/keep-sorted end
]
# Omitted irrelevant visual functions
_VISUAL_FUNCTIONS = [
# go/keep-sorted start
"mjv_averageCamera",
"mjv_copyData",
"mjv_copyModel",
"mjv_defaultScene",
"mjv_freeScene",
"mjv_makeScene",
# go/keep-sorted end
]
_MEMORY_FUNCTIONS = [
# go/keep-sorted start
"mj_freeLastXML",
"mj_freeStack",
"mj_loadModel",
"mj_markStack",
"mj_saveModel",
"mj_stackAllocByte",
"mj_stackAllocInt",
"mj_stackAllocNum",
"mj_warning",
"mjs_bodyToFrame",
"mju_boxQPmalloc",
"mju_clearHandlers",
"mju_error",
"mju_error_i",
"mju_error_s",
"mju_free",
"mju_malloc",
"mju_strncpy",
"mju_warning",
"mju_warning_i",
"mju_warning_s",
# go/keep-sorted end
]
_GETTERS_AND_SETTERS = [
# go/keep-sorted start
"mjs_appendFloatVec",
"mjs_appendIntVec",
"mjs_appendString",
"mjs_getDouble",
"mjs_getPluginAttributes",
"mjs_getString",
"mjs_getUserValue",
"mjs_setBuffer",
"mjs_setDouble",
"mjs_setFloat",
"mjs_setInStringVec",
"mjs_setInt",
"mjs_setPluginAttributes",
"mjs_setString",
"mjs_setStringVec",
"mjs_setUserValue",
# go/keep-sorted end
]
_UTILITY_FUNCTIONS = [
# go/keep-sorted start
"mju_getXMLDependencies",
# go/keep-sorted end
]
# List of functions that should be skipped during the code generation process.
SKIPPED_FUNCTIONS = (
_CLASS_METHODS +
_THREAD_FUNCTIONS +
_MEMORY_FUNCTIONS +
_PLUGIN_FUNCTIONS +
_GETTERS_AND_SETTERS +
_VISUAL_FUNCTIONS +
_ASSET_CACHE_FUNCTIONS +
_VFS_FUNCTIONS +
_WRITABLE_ERROR +
_UTILITY_FUNCTIONS
)
# Functions that require special wrappers to infer sizes and make additional
# validation checks. These functions are not bound automatically but are
# written by hand instead.
BOUNDCHECK_FUNCS = [
# go/keep-sorted start
"mj_addM",
"mj_angmomMat",
"mj_applyFT",
"mj_constraintUpdate",
"mj_differentiatePos",
"mj_fullM",
"mj_geomDistance",
"mj_getState",
"mj_integratePos",
"mj_jac",
"mj_jacBody",
"mj_jacBodyCom",
"mj_jacDot",
"mj_jacGeom",
"mj_jacPointAxis",
"mj_jacSite",
"mj_jacSubtreeCom",
"mj_mulJacTVec",
"mj_mulJacVec",
"mj_mulM",
"mj_mulM2",
"mj_multiRay",
"mj_normalizeQuat",
"mj_rne",
"mj_saveLastXML",
"mj_setLengthRange",
"mj_setState",
"mj_solveM",
"mj_solveM2",
"mjd_inverseFD",
"mjd_subQuat",
"mjd_transitionFD",
"mju_L1",
"mju_add",
"mju_addScl",
"mju_addTo",
"mju_addToScl",
"mju_band2Dense",
"mju_bandMulMatVec",
"mju_boxQP",
"mju_cholFactor",
"mju_cholFactorBand",
"mju_cholSolve",
"mju_cholSolveBand",
"mju_cholUpdate",
"mju_copy",
"mju_d2n",
"mju_decodePyramid",
"mju_dense2Band",
"mju_dense2sparse",
"mju_dot",
"mju_encodePyramid",
"mju_eye",
"mju_f2n",
"mju_fill",
"mju_insertionSort",
"mju_insertionSortInt",
"mju_isZero",
"mju_mulMatMat",
"mju_mulMatMatT",
"mju_mulMatTMat",
"mju_mulMatTVec",
"mju_mulMatVec",
"mju_mulVecMatVec",
"mju_n2d",
"mju_n2f",
"mju_norm",
"mju_normalize",
"mju_printMatSparse",
"mju_scl",
"mju_sparse2dense",
"mju_sqrMatTD",
"mju_sub",
"mju_subFrom",
"mju_sum",
"mju_symmetrize",
"mju_transpose",
"mju_zero",
# go/keep-sorted end
]
# List of structs that should be skipped during the code generation process.
SKIPPED_STRUCTS = [
# go/keep-sorted start
"mjCache",
"mjSDF",
"mjTask",
"mjThreadPool",
"mjUI",
"mjrContext",
"mjrRect",
"mjuiDef",
"mjuiItem",
"mjuiSection",
"mjuiState",
"mjuiThemeColor",
"mjuiThemeSpacing",
# go/keep-sorted end
]
# Dictionary that maps anonymous structs to their parent struct and field name.
# Anonymous structs are not defined as independent structs in the MuJoCo
# codebase, but they are part of other structs. This dictionary is used to
# handle them as if they were independent structs.
ANONYMOUS_STRUCTS = {
# go/keep-sorted start
"mjVisualGlobal": {"parent": "mjVisual", "field_name": "global"},
"mjVisualHeadlight": {"parent": "mjVisual", "field_name": "headlight"},
"mjVisualMap": {"parent": "mjVisual", "field_name": "map"},
"mjVisualQuality": {"parent": "mjVisual", "field_name": "quality"},
"mjVisualRgba": {"parent": "mjVisual", "field_name": "rgba"},
"mjVisualScale": {"parent": "mjVisual", "field_name": "scale"},
# go/keep-sorted end
}
# This list is created by subtracting the skipped structs from the list of all
# structs and adding the anonymous structs.
STRUCTS_TO_BIND = list(
(set(introspect_structs.STRUCTS.keys()) - set(SKIPPED_STRUCTS)).union(
ANONYMOUS_STRUCTS.keys()
)
)
# List of structs that do not have a default constructor.
NO_DEFAULT_CONSTRUCTORS = [
# go/keep-sorted start
"mjContact",
"mjSolverStat",
"mjStatistic",
"mjTimerStat",
"mjWarningStat",
"mjsCompiler",
"mjsDefault",
"mjsElement",
"mjsExclude",
"mjsWrap",
"mjvGLCamera",
"mjvLight",
# go/keep-sorted end
]
# List of `mjData` fields where the array size should be obtained from other
# `mjData` members, instead of from `mjModel` members. This is typically the
# case for fields that are dynamically allocated during the simulation.
MJDATA_SIZES = [
# go/keep-sorted start
"contact",
"efc_AR",
"efc_AR_colind",
"efc_AR_rowadr",
"efc_AR_rownnz",
"efc_D",
"efc_J",
"efc_JT",
"efc_JT_colind",
"efc_J_colind",
"efc_J_rowadr",
"efc_J_rownnz",
"efc_J_rowsuper",
"efc_KBIP",
"efc_R",
"efc_aref",
"efc_b",
"efc_diagApprox",
"efc_force",
"efc_frictionloss",
"efc_id",
"efc_island",
"efc_margin",
"efc_pos",
"efc_state",
"efc_type",
"efc_vel",
"iLDiagInv",
"iM_rowadr",
"iM_rownnz",
"iacc",
"iacc_smooth",
"iefc_D",
"iefc_J",
"iefc_JT",
"iefc_JT_colind",
"iefc_JT_rowadr",
"iefc_JT_rownnz",
"iefc_JT_rowsuper",
"iefc_J_colind",
"iefc_J_rowadr",
"iefc_J_rownnz",
"iefc_J_rowsuper",
"iefc_R",
"iefc_aref",
"iefc_force",
"iefc_frictionloss",
"iefc_id",
"iefc_state",
"iefc_type",
"ifrc_constraint",
"ifrc_smooth",
"island_dofadr",
"island_dofnum",
"island_efcadr",
"island_efcind",
"island_efcnum",
"island_idofadr",
"island_iefcadr",
"island_ne",
"island_nefc",
"island_nf",
"island_nv",
"map_efc2iefc",
"map_iefc2efc",
# go/keep-sorted end
]
# Dictionary where keys are the struct names and the values are lists of the
# fields that are manually specified in the structs.h template file.
MANUALLY_ADDED_FIELDS_FROM_TEMPLATE = {
# go/keep-sorted start
"MjData": ["solver", "timer", "warning", "contact"],
"MjSpec": ["option", "visual", "stat", "element", "compiler"],
"MjvScene": [
"model",
"lights",
"camera",
"geoms",
"geomorder",
"flexedgeadr",
"flexedgenum",
"flexvertadr",
"flexvertnum",
"flexfaceadr",
"flexfacenum",
"flexfaceused",
"flexedge",
"flexvert",
"skinfacenum",
"skinvertadr",
"skinvertnum",
"skinvert",
"skinnormal",
"flexface",
"flexnormal",
"flextexcoord",
# go/keep-sorted end
],
}
# Dictionary that maps byte array fields to their corresponding size members.
# When generating the code for these fields, a specific cast to `uint8_t*` is
# required for embind. This dictionary is used to register those fields and
# their sizes.
BYTE_FIELDS = {
"buffer": {"size": "nbuffer"},
"arena": {"size": "narena"},
}
# Boolean flag to enable debug prints during the struct wrapper and binding
# generation process. When set to `True`, it will print additional information
# about the steps being executed.
STRUCT_DEBUG_MODE = False
# These structs require specific function calls for creation and/or deletion,
# or some of their fields need to be handled manually for now;
# making their wrapper constructors/destructors non-trivial.
HARDCODED_WRAPPER_STRUCTS = [
"MjData",
"MjModel",
"MjvScene",
"MjSpec",
"MjVisual",
]
-350
View File
@@ -1,350 +0,0 @@
# Copyright 2025 DeepMind Technologies Limited
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Helper functions for processing and generating bindings for MuJoCo functions."""
from typing import List, Set, Tuple, cast
from introspect import ast_nodes
from wasm.codegen.helpers import code_builder
from wasm.codegen.helpers import common
from wasm.codegen.helpers import constants
PRIMITIVE_TYPES = constants.PRIMITIVE_TYPES
uppercase_first_letter = common.uppercase_first_letter
def param_is_primitive_value(param: ast_nodes.FunctionParameterDecl) -> bool:
"""Checks if param is a primitive value type."""
if isinstance(param.type, ast_nodes.ValueType):
return param.type.name in PRIMITIVE_TYPES
return False
def param_is_pointer_to_primitive_value(
param: ast_nodes.FunctionParameterDecl,
) -> bool:
"""Checks if param is a pointer to a primitive value."""
return (
isinstance(param.type, ast_nodes.PointerType)
or isinstance(param.type, ast_nodes.ArrayType)
) and (
isinstance(param.type.inner_type, ast_nodes.ValueType)
and param.type.inner_type.name in PRIMITIVE_TYPES
)
def param_is_pointer_to_struct(param: ast_nodes.FunctionParameterDecl) -> bool:
"""Checks if param is a pointer to a struct."""
return (
isinstance(param.type, ast_nodes.PointerType)
or isinstance(param.type, ast_nodes.ArrayType)
) and (
isinstance(param.type.inner_type, ast_nodes.ValueType)
and param.type.inner_type.name not in PRIMITIVE_TYPES
)
def return_is_value_of_type(
func: ast_nodes.FunctionDecl, allowed_types: Set[str]
) -> bool:
"""Checks if func returns an allowed value type."""
return (
isinstance(func.return_type, ast_nodes.ValueType)
and func.return_type.name in allowed_types
)
def return_is_pointer_to_struct(func: ast_nodes.FunctionDecl) -> bool:
"""Checks if func returns a pointer to a struct."""
return (
isinstance(func.return_type, ast_nodes.PointerType)
and isinstance(func.return_type.inner_type, ast_nodes.ValueType)
and func.return_type.inner_type.name not in PRIMITIVE_TYPES
)
def return_is_pointer_to_primitive(func: ast_nodes.FunctionDecl) -> bool:
"""Checks if func returns a pointer to a primitive value."""
return (
isinstance(func.return_type, ast_nodes.PointerType)
and isinstance(func.return_type.inner_type, ast_nodes.ValueType)
and func.return_type.inner_type.name in PRIMITIVE_TYPES
)
def get_const_qualifier(func: ast_nodes.FunctionDecl) -> str:
"""Returns the const qualifier of func's return type."""
if (
isinstance(func.return_type, ast_nodes.PointerType)
and isinstance(func.return_type.inner_type, ast_nodes.ValueType)
and func.return_type.inner_type.is_const
):
return "const "
return ""
def should_be_wrapped(func: ast_nodes.FunctionDecl) -> bool:
"""Checks if a MuJoCo function needs a wrapper function."""
return (
return_is_pointer_to_primitive(func)
or return_is_pointer_to_struct(func)
or any(
param_is_pointer_to_primitive_value(param)
or isinstance(param.type, ast_nodes.ArrayType)
or param_is_pointer_to_struct(param)
for param in func.parameters
)
)
def generate_function_wrapper(func: ast_nodes.FunctionDecl) -> str:
"""Generates C++ code for a wrapper function."""
builder = code_builder.CodeBuilder()
# Build function header
params_unpack_statements = get_params_unpack_statements(func.parameters)
wrapper_params_list = get_params_string(func.parameters)
not_nullable_params = get_params_notnullable(func.parameters)
wrapper_params_str = ", ".join(wrapper_params_list)
ret_type = get_compatible_return_type(func)
builder.line(f"{ret_type} {func.name}_wrapper({wrapper_params_str})")
# Build function body
with builder.block():
invoker_params_list = get_params_string_maybe_with_conversion(
func.parameters
)
invoker_params_str = ", ".join(invoker_params_list)
invoker_call = f"{func.name}({invoker_params_str})"
invoker_statement = get_compatible_return_call(func, invoker_call)
for p in not_nullable_params:
builder.line(f"CHECK_VAL({p});")
for unpack_statement in params_unpack_statements:
builder.line(unpack_statement)
builder.line(f"{invoker_statement};")
return builder.to_string()
def get_params_notnullable(
ast_params: Tuple[ast_nodes.FunctionParameterDecl, ...],
) -> List[str]:
"""Generates list of param names for checking if they aren't null/undefined."""
not_nullable_params = []
for p in ast_params:
if (
isinstance(p.type, (ast_nodes.PointerType, ast_nodes.ArrayType))
and isinstance(p.type.inner_type, ast_nodes.ValueType)
# We only check for char because others are checked in the unpacker
# and we don't want to check twice.
and p.type.inner_type.name == "char"
and not p.nullable
):
not_nullable_params.append(p.name)
return not_nullable_params
def get_params_unpack_statements(
ast_params: Tuple[ast_nodes.FunctionParameterDecl, ...],
) -> List[str]:
"""Generates C++ statements to unpack JS values for pointer/array parameters."""
params_unpack_statements = []
for p in ast_params:
if (
isinstance(p.type, (ast_nodes.PointerType, ast_nodes.ArrayType))
and isinstance(p.type.inner_type, ast_nodes.ValueType)
and p.type.inner_type.name in PRIMITIVE_TYPES
):
if p.type.inner_type.name == "char":
# param is Javascript string
continue
if p.type.inner_type.is_const:
# param is Javascript number[]
params_unpack_statements.append(
f"UNPACK_ARRAY({p.type.inner_type.name}, {p.name});"
)
else:
# param is TypedArray or a WasmBuffer
params_unpack_statements.append(
f"UNPACK_VALUE({p.type.inner_type.name}, {p.name});"
)
return params_unpack_statements
def get_params_string(
parameters: Tuple[ast_nodes.FunctionParameterDecl, ...]
) -> List[str]:
"""Generates a list of C++ parameter declarations as strings."""
result = []
for p in parameters:
if (
isinstance(p.type, ast_nodes.PointerType)
and isinstance(p.type.inner_type, ast_nodes.ValueType)
and p.type.inner_type.name not in PRIMITIVE_TYPES
):
# Pointer to struct parameters
const_qualifier = "const " if p.type.inner_type.is_const else ""
result.append(
f"{const_qualifier}{uppercase_first_letter(p.type.inner_type.name)}&"
f" {p.name}"
)
elif (
isinstance(p.type, ast_nodes.ValueType)
and p.type.name in PRIMITIVE_TYPES
):
# Primitive value parameters
const_qualifier = "const " if p.type.is_const else ""
result.append(f"{const_qualifier}{p.type} {p.name}")
elif (
isinstance(p.type, (ast_nodes.PointerType, ast_nodes.ArrayType))
and isinstance(p.type.inner_type, ast_nodes.ValueType)
and p.type.inner_type.name in PRIMITIVE_TYPES
):
# Pointer to primitive value parameters or arrays
if p.type.inner_type.name == "char":
if p.nullable:
result.append(f"const NullableString& {p.name}")
else:
result.append(f"const String& {p.name}")
elif (
p.type.inner_type.name
in ["int", "float", "double", "mjtNum", "mjtByte"]
and p.type.inner_type.is_const
):
result.append(f"const NumberArray& {p.name}")
else:
result.append(f"const val& {p.name}")
else:
# This case should ideally not be reached if AST is well-formed
# and types are categorized by the helper booleans correctly.
raise TypeError(
"Unable to generate param string. Unhandled parameter type:"
f" {p.type} for param '{p.name}'"
)
return result
def get_params_string_maybe_with_conversion(
ast_params: Tuple[ast_nodes.FunctionParameterDecl, ...],
) -> List[str]:
"""Generates C++ expressions for passing compatible params from JS to MuJoCo C-API functions."""
native_params = []
for p in ast_params:
if param_is_pointer_to_struct(p):
native_params.append(f"{p.name}.get()")
elif param_is_primitive_value(p):
native_params.append(p.name)
elif (
isinstance(p.type, (ast_nodes.PointerType, ast_nodes.ArrayType))
and isinstance(p.type.inner_type, ast_nodes.ValueType)
and p.type.inner_type.name in PRIMITIVE_TYPES
and p.type.inner_type.name != "char"
):
native_params.append(f"{p.name}_.data()")
elif (
isinstance(p.type, (ast_nodes.PointerType, ast_nodes.ArrayType))
and isinstance(p.type.inner_type, ast_nodes.ValueType)
and p.type.inner_type.name == "char"
):
const_qualifier = "const " if p.type.inner_type.is_const else ""
native_params.append(
f"{p.name}.as<{const_qualifier}std::string>().data()"
)
else:
raise TypeError(
f"Unhandled parameter type for conversion: {p.type} for param"
f" '{p.name}'"
)
return native_params
def get_compatible_return_call(
func: ast_nodes.FunctionDecl, invoker: str
) -> str:
"""Generates embind compatible return value conversion."""
if return_is_value_of_type(func, {"void"}):
return invoker
if isinstance(func.return_type, ast_nodes.PointerType) and isinstance(
func.return_type.inner_type, ast_nodes.ValueType
):
if func.return_type.inner_type.name == "char":
return f"return std::string({invoker})"
elif func.return_type.inner_type.name == "mjString":
return f"return *{invoker}"
if return_is_pointer_to_struct(func):
return get_converted_struct_to_class(func, invoker)
if return_is_value_of_type(func, PRIMITIVE_TYPES):
return f"return {invoker}"
raise RuntimeError(
"Failed to calculate return value conversion for function"
f" {func.name} that returns '{func.return_type}'"
)
def get_compatible_return_type(func: ast_nodes.FunctionDecl) -> str:
"""Creates embind compatible return type."""
if (
isinstance(func.return_type, ast_nodes.PointerType)
and isinstance(func.return_type.inner_type, ast_nodes.ValueType)
and func.return_type.inner_type.name in ["char", "mjString"]
):
return "std::string"
if (
isinstance(func.return_type, ast_nodes.PointerType)
and isinstance(func.return_type.inner_type, ast_nodes.ValueType)
and func.return_type.inner_type.name not in PRIMITIVE_TYPES
):
const_qualifier = get_const_qualifier(func)
return f"""{const_qualifier}std::optional<{uppercase_first_letter(func.return_type.inner_type.name)}>"""
if (
isinstance(func.return_type, ast_nodes.ValueType)
and func.return_type.name in PRIMITIVE_TYPES
):
return f"{func.return_type.name}"
return "val"
def get_converted_struct_to_class(
func: ast_nodes.FunctionDecl, invoker: str
) -> str:
"""Generates a C++ function invocation for a struct return-type function."""
const_qualifier = get_const_qualifier(func)
return_type = cast(ast_nodes.PointerType, func.return_type)
struct_name = cast(ast_nodes.ValueType, return_type.inner_type).name
class_constructor = uppercase_first_letter(struct_name)
return_str = f"{class_constructor}(result)"
return f"""{const_qualifier}{struct_name}* result = {invoker};
if (result == nullptr) {{
return std::nullopt;
}}
return {return_str}"""
def is_excluded_function_name(func_name: str) -> bool:
"""Checks if a function name should be excluded from direct binding."""
return (
func_name.startswith("mjr_")
or func_name.startswith("mjui_")
or func_name in constants.SKIPPED_FUNCTIONS
)
-236
View File
@@ -1,236 +0,0 @@
# Copyright 2025 DeepMind Technologies Limited
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from typing import TypeAlias
from absl.testing import absltest
from introspect import ast_nodes
from wasm.codegen.helpers import constants
from wasm.codegen.helpers import function_utils
PrimitiveTypes: TypeAlias = constants.PRIMITIVE_TYPES
ValueType: TypeAlias = ast_nodes.ValueType
PointerType: TypeAlias = ast_nodes.PointerType
ArrayType: TypeAlias = ast_nodes.ArrayType
FunctionParameterDecl: TypeAlias = ast_nodes.FunctionParameterDecl
FunctionDecl: TypeAlias = ast_nodes.FunctionDecl
class FunctionUtilsTest(absltest.TestCase):
def setUp(self):
super().setUp()
self.struct_type = ValueType("MyStruct")
self.ptr_to_int = PointerType(ValueType("int"))
self.func_ret_ptr_int = FunctionDecl(
"func_pi", PointerType(ValueType("int")), [], "doc"
)
self.func_ret_ptr_struct = FunctionDecl(
"func_ps", PointerType(ValueType("MyStruct")), [], "doc"
)
def test_return_is_value_of_type(self):
self.assertTrue(
function_utils.return_is_value_of_type(
FunctionDecl("func_i", ValueType("int"), [], "doc"), PrimitiveTypes
)
)
self.assertFalse(
function_utils.return_is_value_of_type(
FunctionDecl("func_s", ValueType("MyStruct"), [], "doc"),
PrimitiveTypes,
)
)
def test_return_is_pointer_to_struct(self):
self.assertTrue(
function_utils.return_is_pointer_to_struct(self.func_ret_ptr_struct)
)
self.assertFalse(
function_utils.return_is_pointer_to_struct(self.func_ret_ptr_int)
)
def test_return_is_pointer_to_primitive(self):
self.assertTrue(
function_utils.return_is_pointer_to_primitive(self.func_ret_ptr_int)
)
self.assertFalse(
function_utils.return_is_pointer_to_primitive(self.func_ret_ptr_struct)
)
def test_param_is_primitive_value(self):
param_prim_val = FunctionParameterDecl("prim_v", ValueType("int"))
param_arr = FunctionParameterDecl(
"arr_v", ArrayType(ValueType("int"), extents=(10,))
)
self.assertTrue(function_utils.param_is_primitive_value(param_prim_val))
self.assertFalse(function_utils.param_is_primitive_value(param_arr))
def test_param_is_pointer_to_primitive_value(self):
param_ptr_to_prim = FunctionParameterDecl("p_prim", self.ptr_to_int)
param_arr_of_prim = FunctionParameterDecl(
"a_prim", ArrayType(ValueType("int"), extents=(10,))
)
param_ptr_to_struct = FunctionParameterDecl(
name="p_struct", type=PointerType(inner_type=self.struct_type)
)
self.assertTrue(
function_utils.param_is_pointer_to_primitive_value(param_ptr_to_prim)
)
self.assertTrue(
function_utils.param_is_pointer_to_primitive_value(param_arr_of_prim)
)
self.assertFalse(
function_utils.param_is_pointer_to_primitive_value(param_ptr_to_struct)
)
def test_param_is_pointer_to_struct(self):
param_arr_of_struct = FunctionParameterDecl(
"a_struct", ArrayType(self.struct_type, extents=(5,))
)
param_ptr_to_struct = FunctionParameterDecl(
"p_struct", PointerType(self.struct_type)
)
param_ptr_to_ptr = FunctionParameterDecl(
"p_ptr", PointerType(self.ptr_to_int)
)
self.assertTrue(
function_utils.param_is_pointer_to_struct(param_arr_of_struct)
)
self.assertTrue(
function_utils.param_is_pointer_to_struct(param_ptr_to_struct)
)
self.assertFalse(
function_utils.param_is_pointer_to_struct(param_ptr_to_ptr)
)
def test_should_be_wrapped_with_primitive_ptr_return(self):
func = FunctionDecl(
name="get_data",
return_type=PointerType(ValueType("int")),
parameters=tuple(),
doc="Returns int pointer",
)
self.assertTrue(function_utils.should_be_wrapped(func))
def test_generate_function_wrapper_for_simple_func(self):
func = FunctionDecl(
name="get_id",
return_type=ValueType("int"),
parameters=tuple(),
doc="Returns an integer ID",
)
result = function_utils.generate_function_wrapper(func)
self.assertEqual(result, """int get_id_wrapper()
{
return get_id();
}""")
def test_generate_function_wrapper_checking_param(self):
parameters = (
FunctionParameterDecl(
name="mat",
type=PointerType(
inner_type=ValueType(name="mjtNum", is_const=True),
),
),
FunctionParameterDecl(
name="nr",
type=ValueType(name="int"),
),
)
func = FunctionDecl(
name="get_id",
return_type=ValueType("int"),
parameters=parameters,
doc="Returns an integer ID",
)
result = function_utils.generate_function_wrapper(func)
self.assertEqual(
result,
"""int get_id_wrapper(const NumberArray& mat, int nr)
{
UNPACK_ARRAY(mjtNum, mat);
return get_id(mat_.data(), nr);
}""",
)
def test_get_params_string_with_struct_ptr(self):
param = FunctionParameterDecl(
name="my_struct",
type=PointerType(ValueType("mystruct")),
)
result = function_utils.get_params_string((param,))
self.assertEqual(result, ["Mystruct& my_struct"])
def test_get_params_string_maybe_with_conversion_struct_ptr(self):
param = FunctionParameterDecl(
name="s",
type=PointerType(ValueType("customstruct")),
)
result = function_utils.get_params_string_maybe_with_conversion((param,))
self.assertEqual(result, ["s.get()"])
def test_get_compatible_return_call(self):
func = FunctionDecl(
name="noop",
return_type=ValueType("void"),
parameters=tuple(),
doc="does nothing",
)
result = function_utils.get_compatible_return_call(func, "noop()")
self.assertEqual(result, "noop()")
def test_get_compatible_return_type(self):
func = FunctionDecl(
name="get_name",
return_type=PointerType(ValueType("char")),
parameters=tuple(),
doc="returns name",
)
result = function_utils.get_compatible_return_type(func)
self.assertEqual(result.strip(), "std::string")
def test_get_converted_struct_to_class(self):
func = FunctionDecl(
name="get_struct",
return_type=PointerType(ValueType("mystruct")),
parameters=tuple(),
doc="returns struct",
)
result = function_utils.get_converted_struct_to_class(func, "get_struct()")
self.assertIn("mystruct* result = get_struct();", result)
self.assertIn("return Mystruct(result)", result)
def test_is_excluded_function_name(self):
self.assertTrue(function_utils.is_excluded_function_name("mjr_function"))
self.assertTrue(function_utils.is_excluded_function_name("mjui_function"))
self.assertTrue(function_utils.is_excluded_function_name("mju_malloc"))
self.assertTrue(function_utils.is_excluded_function_name("mj_makeData"))
self.assertFalse(
function_utils.is_excluded_function_name("mjv_updateScene")
)
self.assertFalse(
function_utils.is_excluded_function_name("mj_normalFunction")
)
self.assertFalse(
function_utils.is_excluded_function_name("mju_someOtherFunction")
)
if __name__ == "__main__":
absltest.main()
@@ -1,195 +0,0 @@
# Copyright 2025 DeepMind Technologies Limited
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Code builder for struct constructor code."""
from typing import List, cast
from introspect import ast_nodes
from introspect import structs as introspect_structs
from wasm.codegen.helpers import code_builder
from wasm.codegen.helpers import common
from wasm.codegen.helpers import constants
from wasm.codegen.helpers import structs_wrappers_data
def _has_nested_wrapper_members(
struct_info: ast_nodes.StructDecl
) -> bool:
"""Checks if the struct contains other wrapped structs as direct members."""
for field in struct_info.fields:
struct_field = cast(ast_nodes.StructFieldDecl, field)
if isinstance(struct_field.type, ast_nodes.ValueType):
if struct_field.type.name in constants.STRUCTS_TO_BIND:
return True
if isinstance(struct_field.type, ast_nodes.ArrayType):
if isinstance(struct_field.type.inner_type, ast_nodes.ValueType):
if struct_field.type.inner_type.name in constants.STRUCTS_TO_BIND:
return True
if isinstance(struct_field.type, ast_nodes.PointerType):
if isinstance(struct_field.type.inner_type, ast_nodes.ValueType):
if struct_field.type.inner_type.name in constants.STRUCTS_TO_BIND:
return True
return False
def _build_struct_header_internal(
struct_name: str,
wrapped_fields: List[structs_wrappers_data.WrappedFieldData],
fields_with_init: List[structs_wrappers_data.WrappedFieldData],
use_shallow_copy: bool = False,
is_mjs: bool = False,
):
"""Builds the C++ header file code for a struct."""
wrapper_name = common.uppercase_first_letter(struct_name)
builder = code_builder.CodeBuilder()
with builder.block(f"struct {wrapper_name}"):
if not is_mjs:
builder.line(f"{wrapper_name}();")
builder.line(f"{wrapper_name}(const {wrapper_name} &);")
builder.line(f"{wrapper_name} &operator=(const {wrapper_name} &);")
builder.line(f"explicit {wrapper_name}({struct_name} *ptr);")
builder.line(f"~{wrapper_name}();")
if use_shallow_copy:
builder.line(f"std::unique_ptr<{wrapper_name}> copy();")
for field in wrapped_fields:
if field.definition and field not in fields_with_init:
for line in field.definition.splitlines():
builder.line(line)
builder.line(f"{struct_name}* get() const {{ return ptr_; }}")
builder.line(f"void set({struct_name}* ptr) {{ ptr_ = ptr; }}")
builder.line("")
builder.line("private:")
builder.line(f"{struct_name}* ptr_;")
builder.line("bool owned_ = false;")
if is_mjs and fields_with_init:
builder.line("")
builder.line("public:")
for field in fields_with_init:
if field.definition:
builder.line(f"{field.definition}")
return builder.to_string()+";"
def build_struct_header(
struct_name: str,
use_shallow_copy: bool = False,
fields_with_init: List[structs_wrappers_data.WrappedFieldData] = [],
wrapped_fields: List[structs_wrappers_data.WrappedFieldData] = [],
):
"""Builds the C++ header file code for a struct."""
struct_info = introspect_structs.STRUCTS.get(struct_name)
if struct_name.startswith("mjs"):
return _build_struct_header_internal(
struct_name,
wrapped_fields,
fields_with_init,
use_shallow_copy,
is_mjs=True,
)
if (
(
common.uppercase_first_letter(struct_name)
not in constants.HARDCODED_WRAPPER_STRUCTS
)
and struct_info
and not _has_nested_wrapper_members(struct_info)
):
return _build_struct_header_internal(
struct_name, wrapped_fields, [], use_shallow_copy, is_mjs=False
)
return ""
def build_struct_source(
struct_name: str,
mj_default_func: str | None = None,
fields_with_init: List[structs_wrappers_data.WrappedFieldData] = [],
use_shallow_copy: bool = False,
):
"""Builds the C++ .cc file code for a struct."""
wrapper_name = common.uppercase_first_letter(struct_name)
is_mjs_struct = "Mjs" in wrapper_name
builder = code_builder.CodeBuilder()
fields_init = ""
if fields_with_init:
fields_init = "".join(
field_with_init.initialization
for field_with_init in fields_with_init
)
# constructor passing native ptr
builder.line(
f"{wrapper_name}::{wrapper_name}({struct_name} *ptr) :"
f" ptr_(ptr){fields_init} {{}}"
)
# constructor with default values
if not is_mjs_struct:
with builder.block(
f"{wrapper_name}::{wrapper_name}() : ptr_(new"
f" {struct_name}){fields_init}"
):
builder.line("owned_ = true;")
if mj_default_func:
builder.line(f"{mj_default_func}(ptr_);")
# copy constructor
if use_shallow_copy and not is_mjs_struct:
with builder.block(
f"{wrapper_name}::{wrapper_name}(const"
f" {wrapper_name} &other)"
+ (f" : {wrapper_name}()" if not is_mjs_struct else "")
):
builder.line("*ptr_ = *other.get();")
if fields_with_init:
for field_with_init in fields_with_init:
if field_with_init.ptr_copy_reset is not None:
builder.line(field_with_init.ptr_copy_reset)
# assignment operator
with builder.block(
f"{wrapper_name}&"
f" {wrapper_name}::operator=(const"
f" {wrapper_name} &other)"
):
with builder.block("if (this == &other)"):
builder.line("return *this;")
builder.line("*ptr_ = *other.get();")
if fields_with_init:
for field_with_init in fields_with_init:
if field_with_init.ptr_copy_reset is not None:
builder.line(field_with_init.ptr_copy_reset)
builder.line("return *this;")
# destructor
if is_mjs_struct:
builder.line(f"{wrapper_name}::~{wrapper_name}() {{}}")
else:
with builder.block(f"{wrapper_name}::~{wrapper_name}()"):
builder.line("if (owned_ && ptr_) delete ptr_;")
# copy function
if use_shallow_copy:
with builder.block(
f"std::unique_ptr<{wrapper_name}>"
f" {wrapper_name}::copy()"
):
builder.line(
f"return std::make_unique<{wrapper_name}>(*this);"
)
return builder.to_string()
@@ -1,140 +0,0 @@
# Copyright 2025 DeepMind Technologies Limited
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from absl.testing import absltest
from introspect import ast_nodes
from wasm.codegen.helpers import struct_constructor_code_builder
from wasm.codegen.helpers import struct_field_handler
StructFieldDecl = ast_nodes.StructFieldDecl
ValueType = ast_nodes.ValueType
PointerType = ast_nodes.PointerType
ArrayType = ast_nodes.ArrayType
StructDecl = ast_nodes.StructDecl
class StructConstructorCodeBuilderTest(absltest.TestCase):
def test_constructor_code_with_default_function(self):
self.assertEqual(
struct_constructor_code_builder.build_struct_source(
"mjLROpt", "mj_defaultLROpt"
),
"""
MjLROpt::MjLROpt(mjLROpt *ptr) : ptr_(ptr) {}
MjLROpt::MjLROpt() : ptr_(new mjLROpt) {
owned_ = true;
mj_defaultLROpt(ptr_);
}
MjLROpt::~MjLROpt() {
if (owned_ && ptr_) delete ptr_;
}
""".strip(),
)
def test_constructor_code_without_default_function(self):
self.assertEqual(
struct_constructor_code_builder.build_struct_source("mjLROpt"),
"""
MjLROpt::MjLROpt(mjLROpt *ptr) : ptr_(ptr) {}
MjLROpt::MjLROpt() : ptr_(new mjLROpt) {
owned_ = true;
}
MjLROpt::~MjLROpt() {
if (owned_ && ptr_) delete ptr_;
}
""".strip(),
)
def test_constructor_code_with_fields_with_init(self):
field_with_init = StructFieldDecl(
name="element",
type=PointerType(
inner_type=ValueType(name="mjsElement"),
),
doc="",
)
wrapped_field_data = struct_field_handler.StructFieldHandler(
field_with_init, "MjsTexture"
).generate()
self.assertEqual(
struct_constructor_code_builder.build_struct_source(
"mjsTexture",
"mjs_defaultTexture",
[wrapped_field_data],
),
"""
MjsTexture::MjsTexture(mjsTexture *ptr) : ptr_(ptr), element(ptr_->element) {}
MjsTexture::~MjsTexture() {}
""".strip(),
)
def test_constructor_code_with_shallow_copy(self):
self.assertEqual(
struct_constructor_code_builder.build_struct_source(
"mjvLight", use_shallow_copy=True
),
"""MjvLight::MjvLight(mjvLight *ptr) : ptr_(ptr) {}
MjvLight::MjvLight() : ptr_(new mjvLight) {
owned_ = true;
}
MjvLight::MjvLight(const MjvLight &other) : MjvLight() {
*ptr_ = *other.get();
}
MjvLight& MjvLight::operator=(const MjvLight &other) {
if (this == &other) {
return *this;
}
*ptr_ = *other.get();
return *this;
}
MjvLight::~MjvLight() {
if (owned_ && ptr_) delete ptr_;
}
std::unique_ptr<MjvLight> MjvLight::copy() {
return std::make_unique<MjvLight>(*this);
}""".strip(),
)
def test_build_struct_header_with_nested_wrappers(self):
self.assertEqual(
struct_constructor_code_builder.build_struct_header("mjData"),
"",
)
def test_build_struct_header_basic_struct(self):
self.assertEqual(
struct_constructor_code_builder.build_struct_header("mjLROpt"),
"""
struct MjLROpt {
MjLROpt();
MjLROpt(const MjLROpt &);
MjLROpt &operator=(const MjLROpt &);
explicit MjLROpt(mjLROpt *ptr);
~MjLROpt();
mjLROpt* get() const { return ptr_; }
void set(mjLROpt* ptr) { ptr_ = ptr; }
private:
mjLROpt* ptr_;
bool owned_ = false;
};
""".strip(),
)
if __name__ == "__main__":
absltest.main()
@@ -1,100 +0,0 @@
# Copyright 2025 DeepMind Technologies Limited
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Class to build the C++ code for a struct field wrapper."""
from introspect import ast_nodes
from wasm.codegen.helpers import code_builder
StructFieldDecl = ast_nodes.StructFieldDecl
ValueType = ast_nodes.ValueType
def build_primitive_type_definition(field: StructFieldDecl) -> str:
"""Builds the C++ code for a primitive type field wrapper."""
if not isinstance(field.type, ValueType):
raise ValueError(f"{field.type} must be ValueType.")
builder = code_builder.CodeBuilder()
# build getter for primitive type field
with builder.block(f"{field.type.name} {field.name}() const"):
builder.line(f"return ptr_->{field.name};")
# build setter for primitive type field
with builder.block(f"void set_{field.name}({field.type.name} value)"):
builder.line(f"ptr_->{field.name} = value;")
return builder.to_string()
def build_memory_view_definition(
field: StructFieldDecl, array_size_str: str, ptr_expr: str
) -> str:
"""Builds the C++ code for a pointer type field wrapper."""
builder = code_builder.CodeBuilder()
with builder.block(f"emscripten::val {field.name}() const"):
builder.line(
"return"
f" emscripten::val(emscripten::typed_memory_view({array_size_str},"
f" {ptr_expr}));"
)
return builder.to_string()
def build_string_field_definition(field: StructFieldDecl) -> str:
"""Builds the C++ code for a string type field wrapper."""
builder = code_builder.CodeBuilder()
with builder.block(f"mjString {field.name}() const"):
builder.line(
f'return (ptr_ && ptr_->{field.name}) ? *(ptr_->{field.name}) : "";'
)
with builder.block(f"void set_{field.name}(const mjString& value)"):
with builder.block(f"if (ptr_ && ptr_->{field.name})"):
builder.line(f"*(ptr_->{field.name}) = value;")
return builder.to_string()
def build_mjvec_pointer_definition(
field: StructFieldDecl, vector_type: str
) -> str:
"""Builds the C++ code for a mjVec type field wrapper."""
ptr_field_expr = f"*(ptr_->{field.name})"
if vector_type == "mjByteVec":
vector_type = "std::vector<uint8_t>"
ptr_field_expr = (
f"*(reinterpret_cast<std::vector<uint8_t>*>(ptr_->{field.name}))"
)
builder = code_builder.CodeBuilder()
with builder.block(f"{vector_type} &{field.name}() const"):
builder.line(f"return {ptr_field_expr};")
return builder.to_string()
def build_simple_property_binding(
field: StructFieldDecl,
struct_wrapper_name: str,
add_setter: bool = False,
add_return_value_policy_as_ref: bool = False,
) -> str:
"""Builds the C++ code for a simple property binding."""
builder = code_builder.CodeBuilder()
setter_txt = ""
if add_setter:
setter_txt = f", &{struct_wrapper_name}::set_{field.name}"
if add_return_value_policy_as_ref:
as_reference_txt = ", reference()"
else:
as_reference_txt = ""
builder.line(
f'.property("{field.name}",'
f" &{struct_wrapper_name}::{field.name}{setter_txt}{as_reference_txt})"
)
return builder.to_string()
@@ -1,171 +0,0 @@
# Copyright 2025 DeepMind Technologies Limited
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from absl.testing import absltest
from introspect import ast_nodes
from wasm.codegen.helpers import struct_field_code_builder
StructFieldDecl = ast_nodes.StructFieldDecl
ValueType = ast_nodes.ValueType
PointerType = ast_nodes.PointerType
ArrayType = ast_nodes.ArrayType
class StructFieldCodeBuilderTest(absltest.TestCase):
def test_primitive_type_definition(self):
field = StructFieldDecl(
name="ngeom",
type=ValueType(name="int"),
doc="number of geoms",
)
self.assertEqual(
struct_field_code_builder.build_primitive_type_definition(field),
"""
int ngeom() const {
return ptr_->ngeom;
}
void set_ngeom(int value) {
ptr_->ngeom = value;
}
""".strip(),
)
def test_memory_view_definition(self):
field = StructFieldDecl(
name="geom_rgba",
type=PointerType(
inner_type=ValueType(name="float"),
),
doc="rgba when material is omitted",
array_extent=("ngeom", 4),
)
self.assertEqual(
struct_field_code_builder.build_memory_view_definition(
field, "ptr_->ngeom * 4", "ptr_->geom_rgba"
),
"""
emscripten::val geom_rgba() const {
return emscripten::val(emscripten::typed_memory_view(ptr_->ngeom * 4, ptr_->geom_rgba));
}
""".strip(),
)
def test_string_field_definition(self):
field = StructFieldDecl(
name="string_field",
type=PointerType(
inner_type=ValueType(name="mjString"),
),
doc="rgba when material is omitted",
)
self.assertEqual(
struct_field_code_builder.build_string_field_definition(field),
"""
mjString string_field() const {
return (ptr_ && ptr_->string_field) ? *(ptr_->string_field) : "";
}
void set_string_field(const mjString& value) {
if (ptr_ && ptr_->string_field) {
*(ptr_->string_field) = value;
}
}
""".strip(),
)
def test_mjvec_pointer_definition(self):
field = StructFieldDecl(
name="vector_field",
type=PointerType(
inner_type=ValueType(name="mjDoubleVec"),
),
doc="",
)
self.assertEqual(
struct_field_code_builder.build_mjvec_pointer_definition(
field, "mjDoubleVec"
),
"""
mjDoubleVec &vector_field() const {
return *(ptr_->vector_field);
}""".strip(),
)
def test_mjbyte_vec_pointer_definition(self):
field = StructFieldDecl(
name="vector_field",
type=PointerType(
inner_type=ValueType(name="mjByteVec"),
),
doc="",
)
self.assertEqual(
struct_field_code_builder.build_mjvec_pointer_definition(
field, "mjByteVec"
),
"""
std::vector<uint8_t> &vector_field() const {
return *(reinterpret_cast<std::vector<uint8_t>*>(ptr_->vector_field));
}""".strip(),
)
def test_simple_property_binding(self):
field = StructFieldDecl(
name="ngeom",
type=ValueType(name="int"),
doc="number of geoms",
)
self.assertEqual(
struct_field_code_builder.build_simple_property_binding(
field, "MjModel"
),
'.property("ngeom", &MjModel::ngeom)',
)
def test_simple_property_binding_with_setter(self):
field = StructFieldDecl(
name="ngeom",
type=ValueType(name="int"),
doc="",
)
self.assertEqual(
struct_field_code_builder.build_simple_property_binding(
field, "MjModel", True
),
'.property("ngeom", &MjModel::ngeom,'
" &MjModel::set_ngeom)",
)
def test_simple_property_binding_with_return_value_policy_as_ref(self):
field = StructFieldDecl(
name="ngeom",
type=ValueType(name="int"),
doc="",
)
self.assertEqual(
struct_field_code_builder.build_simple_property_binding(
field,
"MjModel",
add_setter=True,
add_return_value_policy_as_ref=True,
),
'.property("ngeom", &MjModel::ngeom,'
" &MjModel::set_ngeom,"
" reference())",
)
if __name__ == "__main__":
absltest.main()
@@ -1,358 +0,0 @@
# Copyright 2025 DeepMind Technologies Limited
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Class to handle the different struct field types, and provide the c++ code for the wrappers and bindings."""
import math
from typing import Tuple, Union, cast
from introspect import ast_nodes
from wasm.codegen.helpers import common
from wasm.codegen.helpers import constants
from wasm.codegen.helpers import struct_field_code_builder
from wasm.codegen.helpers import structs_wrappers_data
AnonymousStructDecl = ast_nodes.AnonymousStructDecl
ArrayType = ast_nodes.ArrayType
PointerType = ast_nodes.PointerType
StructFieldDecl = ast_nodes.StructFieldDecl
ValueType = ast_nodes.ValueType
WrappedFieldData = structs_wrappers_data.WrappedFieldData
debug_print = common.debug_print
class StructFieldHandler:
"""Class to handle the different struct field types, and provide the c++ code for the definitions and bindings."""
def __init__(
self,
field: StructFieldDecl,
struct_wrapper_name: str,
):
self.field = field
self.struct_wrapper_name = struct_wrapper_name
self.simple_property_binding = (
struct_field_code_builder.build_simple_property_binding(
self.field, self.struct_wrapper_name
)
)
self.manually_added_fields = (
constants.MANUALLY_ADDED_FIELDS_FROM_TEMPLATE.get(
self.struct_wrapper_name, {}
)
)
def generate(self) -> WrappedFieldData:
"""Generates the C++ definition and binding code for the struct field."""
field_type = self.field.type
if isinstance(field_type, ValueType) and (
field_type.name in constants.PRIMITIVE_TYPES
or field_type.name.startswith("mjt")
):
return self._handle_primitive()
elif isinstance(field_type, PointerType):
return self._handle_pointer()
elif isinstance(field_type, ArrayType):
return self._handle_array()
elif isinstance(field_type, ValueType) and field_type.name.startswith("mj"):
return self._handle_mj_struct()
elif isinstance(field_type, AnonymousStructDecl):
return self._handle_anonymous_struct()
return self._undefined()
def _handle_primitive(self) -> WrappedFieldData:
"""Handles the generation of C++ definition and binding code for primitive fields."""
return WrappedFieldData(
definition=(
struct_field_code_builder.build_primitive_type_definition(
self.field
)
),
binding=struct_field_code_builder.build_simple_property_binding(
self.field,
self.struct_wrapper_name,
add_setter=True,
add_return_value_policy_as_ref=True,
),
is_primitive_or_fixed_size=True,
)
def _handle_pointer(self) -> WrappedFieldData:
"""Handles the generation of C++ definition and binding code for pointer fields."""
if not isinstance(self.field.type, PointerType):
raise ValueError(
f"Expected PointerType, got {type(self.field.type)} for field"
f" {self.field.name}"
)
field_type: PointerType = self.field.type
inner_type_name = (
field_type.inner_type.name
if isinstance(field_type.inner_type, ValueType)
else ""
)
ptr_field_expr = f"ptr_->{self.field.name}"
array_size_str = ""
if self.field.array_extent:
array_size_str = parse_array_extent(
self.field.array_extent, self.struct_wrapper_name, self.field.name
)
elif self.field.name in constants.BYTE_FIELDS.keys():
# for byte fields, we need to cast the pointer to uint8_t*
# so embind can correctly interpret the memory view
ptr_field_expr = (
f"static_cast<uint8_t*>({ptr_field_expr})"
)
# for these byte fields, there is no array_extent, so we add the size of
# in the config file based in the documentation
extent = (constants.BYTE_FIELDS[self.field.name]["size"],)
array_size_str = parse_array_extent(
extent, self.struct_wrapper_name, self.field.name
)
elif inner_type_name == "mjString":
return WrappedFieldData(
definition=struct_field_code_builder.build_string_field_definition(
self.field
),
binding=struct_field_code_builder.build_simple_property_binding(
self.field,
self.struct_wrapper_name,
add_setter=True,
add_return_value_policy_as_ref=True,
),
)
elif inner_type_name.startswith("mj") and inner_type_name.endswith("Vec"):
return WrappedFieldData(
definition=struct_field_code_builder.build_mjvec_pointer_definition(
self.field, inner_type_name
),
binding=struct_field_code_builder.build_simple_property_binding(
self.field,
self.struct_wrapper_name,
add_setter=False,
add_return_value_policy_as_ref=True,
),
)
elif inner_type_name in constants.PRIMITIVE_TYPES:
return self._get_manual_definition(
comment_type="primitive pointer field with complex extents"
)
if (
inner_type_name.startswith("mj")
and inner_type_name not in constants.PRIMITIVE_TYPES
):
debug_print(
f"\tcomplex pointer type: needs manual wrapper: {self.field.name}"
)
# it's a pointer to a single struct,
# like the `element` field in mjs structs
# and the struct is not manually added
if (
not self.field.array_extent
and self.struct_wrapper_name
not in constants.MANUALLY_ADDED_FIELDS_FROM_TEMPLATE.keys()
):
ptr_field = cast(PointerType, self.field.type)
wrapper_field_name = common.uppercase_first_letter(
cast(ValueType, ptr_field.inner_type).name
)
return WrappedFieldData(
definition=f"{wrapper_field_name} {self.field.name};",
binding=struct_field_code_builder.build_simple_property_binding(
self.field,
self.struct_wrapper_name,
add_setter=False,
add_return_value_policy_as_ref=True,
),
initialization=f", {self.field.name}(ptr_->{self.field.name})",
)
else:
debug_print(
"\tcomplex pointer type with array extent: needs manual wrapper:"
f" {self.field.name}"
)
return self._get_manual_definition(comment_type="complex pointer field")
return WrappedFieldData(
definition=(
struct_field_code_builder.build_memory_view_definition(
self.field, array_size_str, ptr_field_expr
)
),
binding=self.simple_property_binding,
)
def _handle_array(self) -> WrappedFieldData:
"""Handles the generation of C++ definition and binding code for array fields."""
field_type = self.field.type
if not isinstance(field_type, ArrayType):
raise ValueError(
f"Expected ArrayType, got {type(field_type)} for field"
f" {self.field.name}"
)
inner_type = field_type.inner_type
size = math.prod(field_type.extents)
if isinstance(inner_type, ValueType):
if inner_type.name in constants.PRIMITIVE_TYPES:
ptr_expr = f"ptr_->{self.field.name}"
if len(field_type.extents) > 1:
# for multi-dimensional arrays, we need to cast the field
# to a pointer, so embind can correctly interpret the memory
# view
ptr_expr = f"reinterpret_cast<{inner_type.name}*>({ptr_expr})"
return WrappedFieldData(
definition=(
struct_field_code_builder.build_memory_view_definition(
self.field, str(size), ptr_expr
)
),
binding=self.simple_property_binding,
is_primitive_or_fixed_size=True,
)
elif inner_type.name.startswith("mj") and not inner_type.name.startswith(
"mjt"
):
debug_print(f"\tarray to vector wrapper needed: {self.field.name}")
return self._get_manual_definition(comment_type="array field")
debug_print(f"\tNOT IMPLEMENTED ARRAY field: {self.field.name}")
return WrappedFieldData(
definition=(
f"// TODO: NOT IMPLEMENTED ARRAY wrapper for {self.field.name}"
),
binding=f"// TODO: NOT IMPLEMENTED ARRAY binding for {self.field.name}",
)
def _handle_mj_struct(self) -> WrappedFieldData:
"""Handles the generation of C++ definition and binding code for mj struct fields."""
if (
isinstance(self.field.type, ValueType)
and self.field.name not in self.manually_added_fields
and self.field.type.name in constants.STRUCTS_TO_BIND
):
# TODO(manevi): Find a better way to do this instead of checking the
# struct wrapper name.
definition = ""
if self.struct_wrapper_name not in constants.HARDCODED_WRAPPER_STRUCTS:
wrapper_field_name = common.uppercase_first_letter(self.field.type.name)
definition = f"{wrapper_field_name} {self.field.name};"
return WrappedFieldData(
definition=definition,
binding=struct_field_code_builder.build_simple_property_binding(
self.field,
self.struct_wrapper_name,
add_setter=False,
add_return_value_policy_as_ref=True,
),
initialization=f", {self.field.name}(&ptr_->{self.field.name})",
ptr_copy_reset=f"{self.field.name}.set(&ptr_->{self.field.name});",
is_primitive_or_fixed_size=True,
)
return self._get_manual_definition(comment_type="struct field")
def _handle_anonymous_struct(self) -> WrappedFieldData:
"""Handles the generation of C++ definition and binding code for anonymous struct fields."""
anonymous_struct_name = ""
for name, value in constants.ANONYMOUS_STRUCTS.items():
if (
common.uppercase_first_letter(value["parent"])
== self.struct_wrapper_name
and value["field_name"] == self.field.name
):
anonymous_struct_name = name
break
if (
isinstance(self.field.type, AnonymousStructDecl)
and self.field.name not in self.manually_added_fields
and anonymous_struct_name in constants.STRUCTS_TO_BIND
):
return WrappedFieldData(
binding=struct_field_code_builder.build_simple_property_binding(
self.field,
self.struct_wrapper_name,
add_setter=False,
add_return_value_policy_as_ref=True,
),
initialization=f", {self.field.name}(&ptr_->{self.field.name})",
ptr_copy_reset=f"{self.field.name}.set(&ptr_->{self.field.name});",
is_primitive_or_fixed_size=True,
)
return self._get_manual_definition(comment_type="anonymous struct field")
def _undefined(self) -> WrappedFieldData:
"""This function adds a TODO comment for fields that are not handled by this class yet."""
return WrappedFieldData(
definition=f"// TODO: UNDEFINED definition for {self.field.name}",
binding=f"// TODO: UNDEFINED binding for {self.field.name}",
)
def _get_manual_definition(self, comment_type: str = "") -> WrappedFieldData:
"""Helper method to generate a comment as a definition for manually added fields."""
if self.field.name in self.manually_added_fields:
return WrappedFieldData(
definition=(
f"// {comment_type} is defined manually. {self.field.name}"
),
binding=self.simple_property_binding,
)
return WrappedFieldData(
definition=(
f"// TODO: Define {comment_type} manually for {self.field.name}"
),
binding=f"// TODO: {self.simple_property_binding}",
)
def parse_array_extent(
extents: Tuple[Union[str, int], ...], wrapper_name: str, field_name: str
) -> str:
"""Parses the array extent of a field, returning a string representing the resolved extents."""
if not extents:
return ""
return " * ".join(
resolve_extent(extent, wrapper_name, field_name) for extent in extents
)
def resolve_extent(
extent: Union[str, int], wrapper_name: str, field_name: str
) -> str:
"""Resolves the extent of an array, handling integers and references to other struct fields.
Args:
extent: The extent to resolve, can be an int or a string referencing a
field.
wrapper_name: The name of the struct wrapper.
field_name: The name of the field being processed.
Returns:
A string representing the resolved extent, either as a number or a field
reference.
"""
if isinstance(extent, int):
return str(extent)
# if starts with mj, it's a mujoco constant,
# so we don't need to get a parent struct ptr
if extent.startswith("mj"):
return str(extent)
if wrapper_name == "MjData" and field_name not in constants.MJDATA_SIZES:
var_name = "model"
else:
var_name = "ptr_"
return f"{var_name}->{extent}"
@@ -1,241 +0,0 @@
# Copyright 2025 DeepMind Technologies Limited
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from absl.testing import absltest
from introspect import ast_nodes
from wasm.codegen.helpers import struct_field_handler
StructFieldDecl = ast_nodes.StructFieldDecl
ValueType = ast_nodes.ValueType
PointerType = ast_nodes.PointerType
ArrayType = ast_nodes.ArrayType
class StructFieldHandlerTest(absltest.TestCase):
def test_scalar_field(self):
"""Test that a scalar type field is handled correctly."""
field_scalar = StructFieldDecl(
name='ngeom',
type=ValueType(name='int'),
doc='number of geoms',
)
field_handler_scalar = struct_field_handler.StructFieldHandler(
field_scalar, 'MjModel'
)
wrapped_field_data = field_handler_scalar.generate()
self.assertEqual(
wrapped_field_data.definition,
"""
int ngeom() const {
return ptr_->ngeom;
}
void set_ngeom(int value) {
ptr_->ngeom = value;
}
""".strip(),
)
self.assertEqual(
wrapped_field_data.binding,
'.property("ngeom", &MjModel::ngeom, &MjModel::set_ngeom, reference())',
)
def test_pointer_type_field(self):
"""Test that a pointer type field is handled correctly."""
field = StructFieldDecl(
name='geom_rgba',
type=PointerType(
inner_type=ValueType(name='float'),
),
doc='rgba when material is omitted',
array_extent=('ngeom', 4),
)
wrapped_field_data = struct_field_handler.StructFieldHandler(
field, 'MjModel'
).generate()
self.assertEqual(
wrapped_field_data.definition,
("""
emscripten::val geom_rgba() const {
return emscripten::val(emscripten::typed_memory_view(ptr_->ngeom * 4, ptr_->geom_rgba));
}
""".strip()),
)
self.assertEqual(
wrapped_field_data.binding,
'.property("geom_rgba", &MjModel::geom_rgba)',
)
def test_pointer_type_field_for_byte_type(self):
"""Test that a pointer type field for a byte type is handled correctly."""
field = StructFieldDecl(
name='buffer',
type=PointerType(
inner_type=ValueType(name='void'),
),
doc='main buffer; all pointers point in it (nbuffer bytes)',
)
wrapped_field_data = struct_field_handler.StructFieldHandler(
field, 'MjData'
).generate()
self.assertEqual(
wrapped_field_data.definition,
("""
emscripten::val buffer() const {
return emscripten::val(emscripten::typed_memory_view(model->nbuffer, static_cast<uint8_t*>(ptr_->buffer)));
}
""".strip()),
)
assert (
wrapped_field_data.binding
== '.property("buffer", &MjData::buffer)'
)
def test_pointer_type_field_for_mj_struct(self):
"""Test that a pointer type field for a mj struct is handled correctly."""
field = StructFieldDecl(
name='element',
type=PointerType(
inner_type=ValueType(name='mjsElement'),
),
doc='',
)
wrapped_field_data = struct_field_handler.StructFieldHandler(
field, 'MjsTexture'
).generate()
self.assertEqual(wrapped_field_data.definition, "MjsElement element;")
self.assertEqual(
wrapped_field_data.binding,
'.property("element", &MjsTexture::element, reference())',
)
self.assertEqual(
wrapped_field_data.initialization,
', element(ptr_->element)',
)
def test_array_type_field(self):
"""Test that an array type field is handled correctly."""
field = StructFieldDecl(
name='gravity',
type=ArrayType(
inner_type=ValueType(name='mjtNum'),
extents=(3,),
),
doc='gravitational acceleration',
)
wrapped_field_data = struct_field_handler.StructFieldHandler(
field, 'MjOption'
).generate()
self.assertEqual(
wrapped_field_data.definition,
("""
emscripten::val gravity() const {
return emscripten::val(emscripten::typed_memory_view(3, ptr_->gravity));
}
""".strip()),
)
self.assertEqual(
wrapped_field_data.binding,
'.property("gravity", &MjOption::gravity)',
)
def test_array_field_with_multi_dimensional_array(self):
"""Test that multi-dimensional arrays are handled correctly."""
field = StructFieldDecl(
name='multi_dim_array',
type=ArrayType(
inner_type=ValueType(name='float'),
extents=(3, 4),
),
doc='description',
)
wrapped_field_data = struct_field_handler.StructFieldHandler(
field, 'MjModel'
).generate()
self.assertEqual(
wrapped_field_data.definition,
"""
emscripten::val multi_dim_array() const {
return emscripten::val(emscripten::typed_memory_view(12, reinterpret_cast<float*>(ptr_->multi_dim_array)));
}
""".strip(),
)
self.assertEqual(
wrapped_field_data.binding,
'.property("multi_dim_array", &MjModel::multi_dim_array)',
)
def test_parse_array_extent(self):
"""Test that parse_array_extent handles various cases correctly."""
self.assertEqual(
struct_field_handler.parse_array_extent((1, 2), 'MjModel', 'geom_rgba'),
'1 * 2',
)
self.assertEqual(
struct_field_handler.parse_array_extent(
(1, 'ngeom'), 'MjModel', 'geom_rgba'
),
'1 * ptr_->ngeom',
)
self.assertEqual(
struct_field_handler.parse_array_extent(
(1, 'mjConstant'), 'MjModel', 'geom_rgba'
),
'1 * mjConstant',
)
def test_resolve_extent(self):
"""Test that resolve_extent handles various cases correctly."""
# for integer just return the number
self.assertEqual(
struct_field_handler.resolve_extent(1, 'MjModel', 'geom_rgba'), '1'
)
# for string that does not start with mj, it's a member of the struct
self.assertEqual(
struct_field_handler.resolve_extent('ngeom', 'MjModel', 'geom_rgba'),
'ptr_->ngeom',
)
# when it's MjData and the field is in MJDATA_SIZES, it should use ptr_->
self.assertEqual(
struct_field_handler.resolve_extent('size_value', 'MjData', 'efc_AR'),
'ptr_->size_value',
)
# when it's MjData and the field is not in MJDATA_SIZES,
# it should use model->
self.assertEqual(
struct_field_handler.resolve_extent(
'size_value', 'MjData', 'data_field'
),
'model->size_value',
)
self.assertEqual(
struct_field_handler.resolve_extent(
'mjConstant', 'MjModel', 'model_field'
),
'mjConstant',
)
if __name__ == '__main__':
absltest.main()
-201
View File
@@ -1,201 +0,0 @@
# Copyright 2025 DeepMind Technologies Limited
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Parser for MuJoCo structs."""
import collections
from typing import Dict, List
from introspect import ast_nodes
from introspect import structs
from wasm.codegen.helpers import common
from wasm.codegen.helpers import constants
from wasm.codegen.helpers import struct_constructor_code_builder
from wasm.codegen.helpers import struct_field_handler
from wasm.codegen.helpers import structs_wrappers_data
WrappedFieldData = structs_wrappers_data.WrappedFieldData
WrappedStructData = structs_wrappers_data.WrappedStructData
StructFieldHandler = struct_field_handler.StructFieldHandler
AnonymousStructDecl = ast_nodes.AnonymousStructDecl
StructFieldDecl = ast_nodes.StructFieldDecl
debug_print = common.debug_print
introspect_structs = structs.STRUCTS
def generate_wasm_bindings(
wrapped_structs: Dict[str, WrappedStructData],
) -> Dict[str, WrappedStructData]:
"""Generates WASM bindings for MuJoCo structs."""
for struct_name, wrap_data in wrapped_structs.items():
if struct_name in introspect_structs:
struct_fields = introspect_structs[struct_name].fields
elif struct_name in constants.ANONYMOUS_STRUCTS:
anonymous_struct = _get_anonymous_struct_field(struct_name)
if not anonymous_struct or not isinstance(
anonymous_struct.type, AnonymousStructDecl
):
raise RuntimeError(f"Anonymous struct not found: {struct_name}")
struct_fields = anonymous_struct.type.fields
else:
raise RuntimeError(f"Struct not found: {struct_name}")
debug_print(f"Wrapping struct: {struct_name}")
fields_with_init: List[WrappedFieldData] = []
for field in struct_fields:
field_gen = StructFieldHandler(field, wrap_data.wrap_name).generate()
# If the struct has at least one non-primitive or fixed size field
# we avoid shallow copy to avoid uninitialized memory.
if not field_gen.is_primitive_or_fixed_size:
wrap_data.use_shallow_copy = False
if field_gen.initialization:
fields_with_init.append(field_gen)
wrap_data.wrapped_fields.append(field_gen)
wrap_data.wrapped_header = (
struct_constructor_code_builder.build_struct_header(
struct_name,
wrap_data.use_shallow_copy,
fields_with_init,
wrap_data.wrapped_fields,
)
)
wrap_data.wrapped_source = (
struct_constructor_code_builder.build_struct_source(
struct_name,
get_default_func_name(struct_name),
fields_with_init,
wrap_data.use_shallow_copy,
)
)
return wrapped_structs
def _get_anonymous_struct_field(
anonymous_structs_key: str,
) -> StructFieldDecl | None:
"""Looks up the given key in the anonymous_structs dict and generates bindings for its fields."""
info = constants.ANONYMOUS_STRUCTS[anonymous_structs_key]
parent_decl = introspect_structs[info["parent"]]
target_field = next(
(
f
for f in parent_decl.fields
if hasattr(f, "name")
and f.name == info["field_name"]
and hasattr(f, "type")
and isinstance(f.type, AnonymousStructDecl)
),
None,
)
return target_field
def get_default_func_name(struct_name: str) -> str:
"""Returns the default function name for the given struct."""
if (
struct_name in constants.ANONYMOUS_STRUCTS.keys()
or struct_name in constants.NO_DEFAULT_CONSTRUCTORS
or (
common.uppercase_first_letter(struct_name)
in constants.MANUALLY_ADDED_FIELDS_FROM_TEMPLATE.keys()
)
):
return ""
elif struct_name.startswith("mjs"):
return f"mjs_default{struct_name.removeprefix('mjs')}"
elif struct_name.startswith("mjv"):
return f"mjv_default{struct_name.removeprefix('mjv')}"
else:
return f"mj_default{struct_name.removeprefix('mj')}"
def _get_field_struct_type(field_type):
"""Extracts the base struct name if the field type is a struct or pointer to a struct."""
if isinstance(field_type, ast_nodes.ValueType):
return field_type.name
if isinstance(field_type, ast_nodes.PointerType):
if isinstance(field_type.inner_type, ast_nodes.ValueType):
return field_type.inner_type.name
return None
def sort_structs_by_dependency(struct_names: List[str]) -> List[str]:
"""Sorts structs based on their field dependencies using topological sort.
Structs with no dependencies on other structs in the list come first.
If struct A has a field of type struct B, B must come before A in the
sorted list.
Args:
struct_names: A list of struct names to sort.
Returns:
A new list of struct names sorted by dependency.
Raises:
RuntimeError: If a cyclic dependency is detected.
"""
adj = collections.defaultdict(list)
in_degree = collections.defaultdict(int)
struct_set = set(struct_names)
sorted_struct_names = sorted(struct_names)
for struct_name in sorted_struct_names:
if struct_name not in introspect_structs:
# Skip anonymous or other structs not in the main introspect map
continue
struct_decl = introspect_structs[struct_name]
for field in struct_decl.fields:
if isinstance(field, ast_nodes.AnonymousStructDecl):
continue
field_type_name = _get_field_struct_type(field.type)
if (
field_type_name
and field_type_name != struct_name
and field_type_name in struct_set
):
if struct_name not in adj[field_type_name]:
adj[field_type_name].append(struct_name)
in_degree[struct_name] += 1
queue = collections.deque(
[name for name in sorted_struct_names if in_degree[name] == 0]
)
sorted_list = []
while queue:
u = queue.popleft()
sorted_list.append(u)
for v in adj[u]:
in_degree[v] -= 1
if in_degree[v] == 0:
queue.append(v)
if len(sorted_list) == len(struct_names):
return sorted_list
else:
remaining = set(struct_names) - set(sorted_list)
raise RuntimeError(
"Cycle detected in struct dependencies, involving: "
f"{', '.join(sorted(list(remaining)))}"
)
-110
View File
@@ -1,110 +0,0 @@
# Copyright 2025 DeepMind Technologies Limited
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Tests for structs_parser."""
from absl.testing import absltest
from introspect import ast_nodes
from introspect import structs as introspect_structs
from wasm.codegen.helpers import structs_parser
from wasm.codegen.helpers import structs_wrappers_data
class StructsParserTest(absltest.TestCase):
def setUp(self):
super().setUp()
self.wrapped_structs = structs_parser.generate_wasm_bindings(
structs_wrappers_data.create_wrapped_structs_set_up_data([
"mjModel",
"mjData",
"mjVisualGlobal",
"mjVisualQuality",
"mjVisual",
])
)
def test_sort_structs_by_dependency(self):
mock_introspect_structs = {
"mjA": ast_nodes.StructDecl(
name="mjA",
declname="mjA",
fields=[
ast_nodes.StructFieldDecl(
doc="", name="b_field", type=ast_nodes.ValueType(name="mjB")
)
],
),
"mjB": ast_nodes.StructDecl(
name="mjB",
declname="mjB",
fields=[
ast_nodes.StructFieldDecl(
doc="", name="c_field", type=ast_nodes.ValueType(name="mjC")
)
],
),
"mjC": ast_nodes.StructDecl(name="mjC", declname="mjC", fields=[]),
"mjD": ast_nodes.StructDecl(name="mjD", declname="mjD", fields=[]),
}
with absltest.mock.patch.dict(
introspect_structs.STRUCTS, mock_introspect_structs
):
struct_names = ["mjA", "mjB", "mjC", "mjD"]
sorted_names = structs_parser.sort_structs_by_dependency(struct_names)
self.assertEqual(sorted_names, ["mjC", "mjD", "mjB", "mjA"])
def test_generate_wasm_bindings(self):
self.assertEqual(self.wrapped_structs["mjModel"].wrap_name, "MjModel")
self.assertEqual(self.wrapped_structs["mjData"].wrap_name, "MjData")
self.assertEqual(
self.wrapped_structs["mjVisualGlobal"].wrap_name, "MjVisualGlobal"
)
self.assertEqual(
self.wrapped_structs["mjVisualQuality"].wrap_name, "MjVisualQuality"
)
self.assertEqual(self.wrapped_structs["mjVisual"].wrap_name, "MjVisual")
self.assertNotEmpty(self.wrapped_structs["mjModel"].wrapped_fields)
self.assertNotEmpty(self.wrapped_structs["mjData"].wrapped_fields)
self.assertNotEmpty(self.wrapped_structs["mjVisualGlobal"].wrapped_fields)
self.assertNotEmpty(self.wrapped_structs["mjVisualQuality"].wrapped_fields)
self.assertNotEmpty(self.wrapped_structs["mjVisual"].wrapped_fields)
def test_generate_wasm_bindings_with_error(self):
with self.assertRaises(RuntimeError):
structs_parser.generate_wasm_bindings(
structs_wrappers_data.create_wrapped_structs_set_up_data(
["mjFakeStruct"]
)
)
with self.assertRaises(RuntimeError):
structs_parser.generate_wasm_bindings(
structs_wrappers_data.create_wrapped_structs_set_up_data(
["mjFakeAnonymousStruct"]
)
)
def test_get_default_func_name_mjv(self):
self.assertEqual(
structs_parser.get_default_func_name("mjvPerturb"), "mjv_defaultPerturb"
)
def test_get_default_func_name(self):
self.assertEqual(
structs_parser.get_default_func_name("mjOption"), "mj_defaultOption"
)
if __name__ == "__main__":
absltest.main()
@@ -1,75 +0,0 @@
# Copyright 2025 DeepMind Technologies Limited
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Classes used by the parser to generate the structs wrappers and bindings statements."""
import dataclasses
from typing import Dict, List
from wasm.codegen.helpers import common
@dataclasses.dataclass
class WrappedFieldData:
"""Data class for struct field definition and binding."""
# Line for struct field binding
binding: str
# Line for struct field definition
definition: str | None = None
# Initialization code for fields that require it
initialization: str | None = None
# Statement to reset the inner pointer when copying the field
ptr_copy_reset: str | None = None
# Whether the field is a primitive or fixed size
is_primitive_or_fixed_size: bool = False
@dataclasses.dataclass
class WrappedStructData:
"""Data class for struct wrapper definition and binding."""
# Name of wrapper struct
wrap_name: str
# List of WrappedFieldData for this struct
wrapped_fields: List[WrappedFieldData]
# Struct header code
wrapped_header: str
# Struct source code
wrapped_source: str
# Whether to use shallow copy for this struct
use_shallow_copy: bool = True
def create_wrapped_structs_set_up_data(
struct_names: List[str],
) -> Dict[str, WrappedStructData]:
"""Creates a dictionary of WrappedStructData for the given struct names."""
return {
name: WrappedStructData(
wrap_name=common.uppercase_first_letter(name),
wrapped_fields=[],
wrapped_header="",
wrapped_source="",
)
for name in struct_names
}
File diff suppressed because it is too large Load Diff
-348
View File
@@ -1,348 +0,0 @@
// Copyright 2025 DeepMind Technologies Limited
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// NOLINTBEGIN(whitespace/line_length)
#ifndef MUJOCO_WASM_CODEGEN_GENERATED_BINDINGS_H_
#define MUJOCO_WASM_CODEGEN_GENERATED_BINDINGS_H_
#include <emscripten.h>
#include <emscripten/bind.h>
#include <cstddef>
#include <cstdint>
#include <memory>
#include <mujoco/mujoco.h>
namespace mujoco::wasm {
// Create the types for anonymous structs
using mjVisualGlobal = decltype(::mjVisual::global);
using mjVisualQuality = decltype(::mjVisual::quality);
using mjVisualHeadlight = decltype(::mjVisual::headlight);
using mjVisualMap = decltype(::mjVisual::map);
using mjVisualScale = decltype(::mjVisual::scale);
using mjVisualRgba = decltype(::mjVisual::rgba);
// {{ AUTOGENNED_STRUCT_DEFINITIONS }}
struct MjVisualGlobal {
MjVisualGlobal();
explicit MjVisualGlobal(mjVisualGlobal *ptr);
MjVisualGlobal(const MjVisualGlobal &);
MjVisualGlobal &operator=(const MjVisualGlobal &);
~MjVisualGlobal();
std::unique_ptr<MjVisualGlobal> copy();
// INSERT-GENERATED-MjVisualGlobal-DEFINITIONS
mjVisualGlobal* get() const { return ptr_; }
void set(mjVisualGlobal* ptr) { ptr_ = ptr; }
private:
mjVisualGlobal* ptr_;
bool owned_ = false;
};
struct MjVisualQuality {
MjVisualQuality();
explicit MjVisualQuality(mjVisualQuality *ptr);
MjVisualQuality(const MjVisualQuality &);
MjVisualQuality &operator=(const MjVisualQuality &);
~MjVisualQuality();
std::unique_ptr<MjVisualQuality> copy();
// INSERT-GENERATED-MjVisualQuality-DEFINITIONS
mjVisualQuality* get() const { return ptr_; }
void set(mjVisualQuality* ptr) { ptr_ = ptr; }
private:
mjVisualQuality* ptr_;
bool owned_ = false;
};
struct MjVisualHeadlight {
MjVisualHeadlight();
explicit MjVisualHeadlight(mjVisualHeadlight *ptr);
MjVisualHeadlight(const MjVisualHeadlight &);
MjVisualHeadlight &operator=(const MjVisualHeadlight &);
~MjVisualHeadlight();
std::unique_ptr<MjVisualHeadlight> copy();
// INSERT-GENERATED-MjVisualHeadlight-DEFINITIONS
mjVisualHeadlight* get() const { return ptr_; }
void set(mjVisualHeadlight* ptr) { ptr_ = ptr; }
private:
mjVisualHeadlight* ptr_;
bool owned_ = false;
};
struct MjVisualMap {
MjVisualMap();
explicit MjVisualMap(mjVisualMap *ptr);
MjVisualMap(const MjVisualMap &);
MjVisualMap &operator=(const MjVisualMap &);
~MjVisualMap();
std::unique_ptr<MjVisualMap> copy();
// INSERT-GENERATED-MjVisualMap-DEFINITIONS
mjVisualMap* get() const { return ptr_; }
void set(mjVisualMap* ptr) { ptr_ = ptr; }
private:
mjVisualMap* ptr_;
bool owned_ = false;
};
struct MjVisualScale {
MjVisualScale();
explicit MjVisualScale(mjVisualScale *ptr);
MjVisualScale(const MjVisualScale &);
MjVisualScale &operator=(const MjVisualScale &);
~MjVisualScale();
std::unique_ptr<MjVisualScale> copy();
// INSERT-GENERATED-MjVisualScale-DEFINITIONS
mjVisualScale* get() const { return ptr_; }
void set(mjVisualScale* ptr) { ptr_ = ptr; }
private:
mjVisualScale* ptr_;
bool owned_ = false;
};
struct MjVisualRgba {
MjVisualRgba();
explicit MjVisualRgba(mjVisualRgba *ptr);
MjVisualRgba(const MjVisualRgba &);
MjVisualRgba &operator=(const MjVisualRgba &);
~MjVisualRgba();
std::unique_ptr<MjVisualRgba> copy();
// INSERT-GENERATED-MjVisualRgba-DEFINITIONS
mjVisualRgba* get() const { return ptr_; }
void set(mjVisualRgba* ptr) { ptr_ = ptr; }
private:
mjVisualRgba* ptr_;
bool owned_ = false;
};
struct MjVisual {
MjVisual();
explicit MjVisual(mjVisual *ptr_);
MjVisual(const MjVisual &);
MjVisual &operator=(const MjVisual &);
~MjVisual();
std::unique_ptr<MjVisual> copy();
// INSERT-GENERATED-MjVisual-DEFINITIONS
mjVisual* get() const { return ptr_; }
void set(mjVisual* ptr) { ptr_ = ptr; }
private:
mjVisual* ptr_;
bool owned_ = false;
public:
MjVisualGlobal global;
MjVisualQuality quality;
MjVisualHeadlight headlight;
MjVisualMap map;
MjVisualScale scale;
MjVisualRgba rgba;
};
struct MjModel {
explicit MjModel(mjModel *m);
explicit MjModel(const MjModel &other);
~MjModel();
std::unique_ptr<MjModel> copy();
// INSERT-GENERATED-MjModel-DEFINITIONS
mjModel* get() const { return ptr_; }
void set(mjModel* ptr) { ptr_ = ptr; }
private:
mjModel* ptr_;
public:
MjOption opt;
MjStatistic stat;
MjVisual vis;
};
struct MjData {
MjData(MjModel *m);
explicit MjData(const MjModel &, const MjData &);
~MjData();
std::vector<MjSolverStat> InitSolverArray();
std::vector<MjTimerStat> InitTimerArray();
std::vector<MjWarningStat> InitWarningArray();
std::vector<MjContact> contact() const;
std::unique_ptr<MjData> copy();
// INSERT-GENERATED-MjData-DEFINITIONS
mjData* get() const { return ptr_; }
void set(mjData* ptr) { ptr_ = ptr; }
private:
mjData* ptr_;
public:
mjModel *model;
std::vector<MjSolverStat> solver;
std::vector<MjTimerStat> timer;
std::vector<MjWarningStat> warning;
};
struct MjvScene {
MjvScene();
MjvScene(MjModel *m, int maxgeom);
// MjvScene(const MjvScene &);
~MjvScene();
std::unique_ptr<MjvScene> copy();
int GetSumFlexFaces() const;
std::vector<MjvLight> InitLightsArray();
std::vector<MjvGLCamera> InitCameraArray();
std::vector<MjvGeom> geoms() const;
emscripten::val geomorder() const {
return emscripten::val(
emscripten::typed_memory_view(ptr_->ngeom, ptr_->geomorder));
}
emscripten::val flexedgeadr() const {
return emscripten::val(
emscripten::typed_memory_view(ptr_->nflex, ptr_->flexedgeadr));
}
emscripten::val flexedgenum() const {
return emscripten::val(
emscripten::typed_memory_view(ptr_->nflex, ptr_->flexedgenum));
}
emscripten::val flexvertadr() const {
return emscripten::val(
emscripten::typed_memory_view(ptr_->nflex, ptr_->flexvertadr));
}
emscripten::val flexvertnum() const {
return emscripten::val(
emscripten::typed_memory_view(ptr_->nflex, ptr_->flexvertnum));
}
emscripten::val flexfaceadr() const {
return emscripten::val(
emscripten::typed_memory_view(ptr_->nflex, ptr_->flexfaceadr));
}
emscripten::val flexfacenum() const {
return emscripten::val(
emscripten::typed_memory_view(ptr_->nflex, ptr_->flexfacenum));
}
emscripten::val flexfaceused() const {
return emscripten::val(
emscripten::typed_memory_view(ptr_->nflex, ptr_->flexfaceused));
}
emscripten::val flexedge() const {
return emscripten::val(
emscripten::typed_memory_view(2 * model->nflexedge, ptr_->flexedge));
}
emscripten::val flexvert() const {
return emscripten::val(
emscripten::typed_memory_view(3 * model->nflexvert, ptr_->flexvert));
}
emscripten::val skinfacenum() const {
return emscripten::val(
emscripten::typed_memory_view(ptr_->nskin, ptr_->skinfacenum));
}
emscripten::val skinvertadr() const {
return emscripten::val(
emscripten::typed_memory_view(ptr_->nskin, ptr_->skinvertadr));
}
emscripten::val skinvertnum() const {
return emscripten::val(
emscripten::typed_memory_view(ptr_->nskin, ptr_->skinvertnum));
}
emscripten::val skinvert() const {
return emscripten::val(
emscripten::typed_memory_view(3 * model->nskinvert, ptr_->skinvert));
}
emscripten::val skinnormal() const {
return emscripten::val(
emscripten::typed_memory_view(3 * model->nskinvert, ptr_->skinnormal));
}
emscripten::val flexface() const {
return emscripten::val(emscripten::typed_memory_view(
9 * MjvScene::GetSumFlexFaces(), ptr_->flexface));
}
emscripten::val flexnormal() const {
return emscripten::val(emscripten::typed_memory_view(
9 * MjvScene::GetSumFlexFaces(), ptr_->flexnormal));
}
emscripten::val flextexcoord() const {
return emscripten::val(emscripten::typed_memory_view(
6 * MjvScene::GetSumFlexFaces(), ptr_->flextexcoord));
}
// INSERT-GENERATED-MjvScene-DEFINITIONS
mjvScene* get() const { return ptr_; }
void set(mjvScene* ptr) { ptr_ = ptr; }
private:
mjvScene* ptr_;
bool owned_ = false;
public:
mjModel *model;
std::vector<MjvLight> lights;
std::vector<MjvGLCamera> camera;
};
struct MjSpec {
MjSpec();
explicit MjSpec(mjSpec *ptr);
MjSpec(const MjSpec &);
MjSpec &operator=(const MjSpec &);
~MjSpec();
std::unique_ptr<MjSpec> copy();
// INSERT-GENERATED-MjSpec-DEFINITIONS
mjSpec* get() const { return ptr_; }
void set(mjSpec* ptr) { ptr_ = ptr; }
private:
mjSpec* ptr_;
bool owned_ = false;
public:
MjOption option;
MjVisual visual;
MjStatistic stat;
MjsCompiler compiler;
MjsElement element;
};
// TODO: Refactor, Structs Manually added so functions.cc compile -- //
struct MjpResourceProvider {
MjpResourceProvider(mjpResourceProvider *ptr_) { ptr = ptr_; };
~MjpResourceProvider() {}
mjpResourceProvider *get() const { return ptr; }
mjpResourceProvider *ptr;
};
struct MjpPlugin {
MjpPlugin(mjpPlugin *ptr_) { ptr = ptr_; };
~MjpPlugin() {}
mjpPlugin *get() const { return ptr; }
mjpPlugin *ptr;
};
// TODO: Factory and debug helper functions, some should be removed when
// functions are generated -- //
std::unique_ptr<MjModel> loadFromXML(std::string filename);
void step(MjModel *model, MjData *data);
void error(const std::string &msg);
void kinematics(MjModel *m, MjData *d);
std::unique_ptr<MjSpec> parseXMLString(const std::string &xml);
std::unique_ptr<MjsBody> findBody(MjSpec *spec, const std::string &name);
std::unique_ptr<MjsGeom> findGeom(MjSpec *spec, const std::string &name);
} // namespace mujoco::wasm
#endif // MUJOCO_WASM_CODEGEN_GENERATED_BINDINGS_H_
// NOLINTEND(whitespace/line_length)
-41
View File
@@ -1,41 +0,0 @@
# Copyright 2025 DeepMind Technologies Limited
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Generates WASM bindings for MuJoCo's API.
This script leverages MuJoCo's introspect dicts to gather information
about its internal structures and then uses a code generation framework to
produce corresponding WASM bindings.
"""
from wasm.codegen import binding_builder
from wasm.codegen.helpers import common
def generate_all_bindings():
"""Generates WASM bindings for MuJoCo."""
template_path_h, generated_path_h = common.get_file_path(
"templates", "generated", "bindings.h"
)
template_path_cc, generated_path_cc = common.get_file_path(
"templates", "generated", "bindings.cc"
)
builder = binding_builder.BindingBuilder(
template_path_h, template_path_cc, generated_path_h, generated_path_cc
)
builder.set_enums().set_headers().set_structs().set_functions().build()
if __name__ == "__main__":
generate_all_bindings()
-498
View File
@@ -1,498 +0,0 @@
// Copyright 2025 DeepMind Technologies Limited
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import * as THREE from "three"
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js"
import loadMujoco from "../dist/mujoco_wasm.js"
declare function loadMujoco(): Promise<MainModule>;
let mujoco: any;
const modelXml = `
<mujoco model="Particle">
<statistic extent="1.5" meansize=".05"/>
<option timestep="0.005" jacobian="sparse"/>
<visual>
<rgba haze="0.15 0.25 0.35 1"/>
<quality shadowsize="4096"/>
<map stiffness="700" shadowscale="0.5" fogstart="10" fogend="15" zfar="40" haze="0.3"/>
</visual>
<worldbody>
<light directional="true" diffuse=".4 .4 .4" specular="0.1 0.1 0.1" pos="0 0 5.0" dir="0 0 -1" castshadow="false"/>
<light directional="true" diffuse=".6 .6 .6" specular="0.2 0.2 0.2" pos="0 0 4" dir="0 0 -1"/>
<geom name="ground" type="plane" size="0 0 1" pos="0 0 0" quat="1 0 0 0" condim="1"/>
<body mocap="true" pos="-.1 .05 0" zaxis=".5 0 1">
<geom type="capsule" size=".1 .1" group="1" condim="1"/>
</body>
</worldbody>
<option solver="CG" tolerance="1e-6" timestep=".01"/>
<size memory="1G"/>
<visual>
<map stiffness="100"/>
</visual>
<default>
<default class="wall">
<geom type="plane" size=".5 .5 .05"/>
</default>
</default>
<worldbody>
<geom name="+x" class="wall" zaxis="1 0 0" pos="-.5 0 -.25"/>
<geom name="-x" class="wall" zaxis="-1 0 0" pos=".5 0 -.25"/>
<geom name="+y" class="wall" zaxis="0 1 0" pos="0 -.5 -.25"/>
<geom name="-y" class="wall" zaxis="0 -1 0" pos="0 .5 -.25"/>
<replicate count="10" offset=".07 0 0">
<replicate count="10" offset="0 .07 0">
<replicate count="10" offset="0 0 .07">
<body pos="-.315 -.315 1">
<joint type="slide" axis="1 0 0"/>
<joint type="slide" axis="0 1 0"/>
<joint type="slide" axis="0 0 1"/>
<geom size=".025" rgba=".8 .2 .1 1" condim="1"/>
</body>
</replicate>
</replicate>
</replicate>
</worldbody>
</mujoco>
`;
// Backport of CapsuleGeometry class introduced in THREE.js r139
class CapsuleGeometry extends THREE.BufferGeometry {
readonly parameters: {
readonly radius: number,
readonly length: number,
readonly capSegments: number,
readonly radialSegments: number
};
constructor(radius = 1, length = 1, capSegments = 4, radialSegments = 8) {
const path = new THREE.Path();
path.absarc(0, -length / 2, radius, Math.PI * 1.5, 0, false);
path.absarc(0, length / 2, radius, 0, Math.PI * 0.5, false);
const latheGeometry =
new THREE.LatheGeometry(path.getPoints(capSegments), radialSegments);
super();
this.setIndex(latheGeometry.getIndex());
this.setAttribute('position', latheGeometry.getAttribute('position'));
this.setAttribute('normal', latheGeometry.getAttribute('normal'));
this.setAttribute('uv', latheGeometry.getAttribute('uv'));
this.type = 'CapsuleGeometry';
this.parameters = {
radius,
length,
capSegments,
radialSegments,
};
}
}
class App {
// TODO(matijak): We can use better types here by doing the following:
// https://emscripten.org/docs/porting/connecting_cpp_and_javascript/embind.html#typescript-definitions
mjModel: any;
mjData: any;
mjvOption: any;
mjvPerturb: any;
mjvCamera: any;
mjvScene: any;
paused = false;
frameId: number|null = null;
maxGeoms: number = 2 ** 15;
scene: THREE.Scene;
renderer: THREE.WebGLRenderer;
camera: THREE.PerspectiveCamera;
controls: OrbitControls;
meshes: THREE.Mesh[] = [];
bufferGeometryCache = new Map<string, THREE.BufferGeometry>();
constructor() {
this.mjvPerturb = new mujoco.MjvPerturb();
this.mjvOption = new mujoco.MjvOption();
this.mjvCamera = new mujoco.MjvCamera();
this.scene = new THREE.Scene();
this.renderer = new THREE.WebGLRenderer();
this.renderer.setSize(window.innerWidth, window.innerHeight);
this.renderer.shadowMap.enabled = true;
this.renderer.shadowMap.type = THREE.PCFSoftShadowMap;
document.body.appendChild(this.renderer.domElement);
this.camera = new THREE.PerspectiveCamera(
45, window.innerWidth / window.innerHeight, .1, 1000);
this.camera.up.set(0, 0, 1); // Mujoco uses z-up
this.camera.position.set(-2, 0, 2);
this.controls = new OrbitControls(this.camera, this.renderer.domElement);
}
dispose() {
// Release all C++ objects
if (this.mjvScene) {
this.mjvScene.delete();
}
if (this.mjvCamera) {
this.mjvCamera.delete();
}
if (this.mjvPerturb) {
this.mjvPerturb.delete();
}
if (this.mjvOption) {
this.mjvOption.delete();
}
if (this.mjData) {
this.mjData.delete();
}
if (this.mjModel) {
this.mjModel.delete();
}
// Release all the THREE.js objects
this.meshes.forEach((mesh) => {
if (mesh.material) {
if (Array.isArray(mesh.material)) {
mesh.material.forEach(material => material.dispose());
} else {
mesh.material.dispose();
}
}
if (mesh.geometry) {
mesh.geometry.dispose();
}
});
this.bufferGeometryCache.clear();
if (this.controls) {
this.controls.dispose();
}
if (this.renderer) {
this.renderer.dispose();
}
// Stop the animation loop since we've disposed of all the data
if (this.frameId) {
cancelAnimationFrame(this.frameId);
this.frameId = null;
}
}
loadModel(xmlContent: string) {
// Write xml as a file so that mujoco can find it
(mujoco as any).FS.writeFile('/working/model.xml', xmlContent);
this.mjModel = mujoco.MjModel.loadFromXML('/working/model.xml');
if (!app.mjModel) {
throw new Error('Failed to load model');
}
this.mjData = new mujoco.MjData(this.mjModel);
if (!this.mjData) {
throw new Error('Failed to load data');
}
this.initScene();
}
pauseButton() {
this.paused = !this.paused;
const button = document.getElementById('pause-button');
if (button) {
button.textContent = this.paused ? 'Resume' : 'Pause';
}
}
// TODO(matijak): Fix the bug where contact cylinders are wrong if the
// simulation is reset while they are being visualized
resetButton() {
if (this.mjModel && this.mjData) {
console.log('Resetting model and data');
mujoco.mj_resetData(this.mjModel, this.mjData);
mujoco.mj_forward(this.mjModel, this.mjData);
this.clearScene();
this.initScene();
}
}
contactButton() {
const index = mujoco.mjtVisFlag.mjVIS_CONTACTPOINT.value;
const value = this.mjvOption.flags[index];
this.mjvOption.flags[index] = !value;
const button = document.getElementById('contact-button');
if (button) {
button.textContent =
this.mjvOption.flags[index] ? 'Hide Contacts' : 'Show Contacts';
}
this.clearScene();
this.initScene();
}
initScene() {
this.mjvScene = new mujoco.MjvScene(this.mjModel, this.maxGeoms);
const pointLight = new THREE.PointLight(0xffffff, .4);
pointLight.position.set(0, 0, 2);
pointLight.castShadow = true;
pointLight.shadow.mapSize.set(2048, 2048);
this.scene.add(pointLight);
const ambientLight = new THREE.AmbientLight(0xffffff, .2);
this.scene.add(ambientLight);
const spotLight = new THREE.SpotLight(0xffffff, .2);
spotLight.position.set(0, 0, 2);
spotLight.target.position.set(0, 0, 0);
spotLight.castShadow = true;
spotLight.shadow.mapSize.set(2048, 2048);
this.scene.add(spotLight);
this.scene.add(spotLight.target);
}
clearScene() {
// clear cached meshes
this.meshes.forEach((mesh) => {
if (mesh.material) {
if (Array.isArray(mesh.material)) {
mesh.material.forEach(material => material.dispose());
} else {
mesh.material.dispose();
}
}
if (mesh.geometry) {
mesh.geometry.dispose();
}
});
this.bufferGeometryCache.clear();
this.meshes.length = 0;
while (this.scene.children.length > 0) {
this.scene.remove(this.scene.children[0]);
}
this.mjvScene.delete();
}
getBufferGeometry(mjvGeom: any): [boolean, THREE.BufferGeometry] {
if (!(mjvGeom instanceof mujoco.MjvGeom)) {
throw new Error('mjvGeom is not an instance of mujoco.MjvGeom');
}
// Lookup the geometry and return it if found
const key = JSON.stringify([mjvGeom.type, mjvGeom.size, mjvGeom.dataid]);
const found = this.bufferGeometryCache.get(key);
if (found) {
return [false, found];
}
// Create geometry
let geom: THREE.BufferGeometry;
if (mjvGeom.type === mujoco.mjtGeom.mjGEOM_PLANE.value) {
geom = new THREE.PlaneGeometry(
2 * (mjvGeom.size[0] ? mjvGeom.size[0] : 10000),
2 * (mjvGeom.size[1] ? mjvGeom.size[1] : 10000));
const uv = geom.getAttribute('uv');
for (let i = 0; i < uv.count; ++i) {
uv.setY(i, 1 - uv.getY(i));
}
} else if (mjvGeom.type === mujoco.mjtGeom.mjGEOM_SPHERE.value) {
geom = new THREE.SphereGeometry(mjvGeom.size[0]);
} else if (mjvGeom.type === mujoco.mjtGeom.mjGEOM_CAPSULE.value) {
geom = new CapsuleGeometry(mjvGeom.size[0], 2 * mjvGeom.size[2], 32, 16);
geom.rotateX(0.5 * Math.PI);
} else if (mjvGeom.type === mujoco.mjtGeom.mjGEOM_BOX.value) {
geom = new THREE.BoxGeometry(
2 * mjvGeom.size[0], 2 * mjvGeom.size[1], 2 * mjvGeom.size[2]);
} else if (mjvGeom.type === mujoco.mjtGeom.mjGEOM_CYLINDER.value) {
geom = new THREE.CylinderGeometry(
mjvGeom.size[0], mjvGeom.size[1], 2 * mjvGeom.size[2], 32);
geom.rotateX(0.5 * Math.PI);
} else if (mjvGeom.type === mujoco.mjtGeom.mjGEOM_ELLIPSOID.value) {
geom = new THREE.SphereGeometry(1);
geom.scale(mjvGeom.size[0], mjvGeom.size[1], mjvGeom.size[2]);
} else {
console.log('Unsupported geom type: ', mjvGeom.type);
geom = new THREE.BufferGeometry();
}
this.bufferGeometryCache.set(key, geom);
return [true, geom];
}
update() {
if (!this.mjModel || !this.mjData) {
return;
}
app.controls.update();
// Simulate physics for 1/60 sec
if (!app.paused) {
let sim_start = app.mjData.time;
while (app.mjData.time - sim_start < 1. / 60.) {
mujoco.mj_step(app.mjModel, app.mjData);
}
}
// Update the mujoco scene
mujoco.mjv_updateScene(
this.mjModel, this.mjData, this.mjvOption, this.mjvPerturb,
this.mjvCamera, mujoco.mjtCatBit.mjCAT_ALL.value, this.mjvScene);
const geoms = this.mjvScene.geoms;
for (let i = 0; i < geoms.size(); i++) {
const mjvGeom = geoms.get(i);
let mesh: THREE.Mesh;
if (i < this.meshes.length) {
mesh = this.meshes[i];
} else {
const mjvGeom = geoms.get(i);
const [added, geom] = this.getBufferGeometry(mjvGeom);
// Create material
let material = new THREE.MeshPhongMaterial();
material.color.setRGB(
mjvGeom.rgba[0], mjvGeom.rgba[1], mjvGeom.rgba[2]);
material.opacity = mjvGeom.rgba[3];
material.transparent = mjvGeom.rgba[3] !== 0;
// Create mesh
mesh = new THREE.Mesh(geom, material);
mesh.castShadow = true;
mesh.receiveShadow = true;
this.meshes.push(mesh);
this.scene.add(mesh);
}
mesh.matrixAutoUpdate = false;
const sz = 1;
mesh.matrix.set(
mjvGeom.mat[0], mjvGeom.mat[1], mjvGeom.mat[2] * sz, mjvGeom.pos[0],
mjvGeom.mat[3], mjvGeom.mat[4], mjvGeom.mat[5] * sz, mjvGeom.pos[1],
mjvGeom.mat[6], mjvGeom.mat[7], mjvGeom.mat[8] * sz, mjvGeom.pos[2],
0, 0, 0, 1);
mesh.matrixWorldNeedsUpdate = true;
mjvGeom.delete();
}
geoms.delete();
}
render() {
this.renderer.render(this.scene, this.camera);
}
run() {
const animate = () => {
try {
this.update();
this.render();
} catch (error) {
console.error('Simulation error:', error);
}
// Request next frame
this.frameId = requestAnimationFrame(animate);
};
// Request first frame
this.frameId = requestAnimationFrame(animate);
}
}
function setupWindowEvents() {
// Add an event listener to clean up when the page is unloaded
// Tip: put "window.dispatchEvent(new Event('unload'))" in the console to test
window.addEventListener('unload', () => {
app.dispose();
(mujoco as any).FS.unmount('/working');
});
window.addEventListener('keydown', (event) => {
if (event.code === 'Backspace') {
app.resetButton();
}
});
window.addEventListener('keydown', (event) => {
if (event.code === 'Space') {
app.pauseButton();
}
});
window.addEventListener('keydown', (event) => {
if (event.key === 'c') {
app.contactButton();
}
});
}
let app: App;
async function main() {
try {
mujoco = await loadMujoco();
// Set up emscripten virtual file system
(mujoco as any).FS.mkdir('/working');
(mujoco as any).FS.mount((mujoco as any).MEMFS, {root: '.'}, '/working');
app = new App();
setupWindowEvents();
// Note: all elements will be destroyed with the page
const pauseButtonElement = document.getElementById('pause-button');
if (pauseButtonElement) {
pauseButtonElement.onclick = () => app.pauseButton();
}
const resetButtonElement = document.getElementById('reset-button');
if (resetButtonElement) {
resetButtonElement.onclick = () => app.resetButton();
}
const contactButtonElement = document.getElementById('contact-button');
if (contactButtonElement) {
contactButtonElement.onclick = () => app.contactButton();
}
app.loadModel(modelXml);
app.run();
} catch (error) {
console.error('Initialization error: ', error);
app.dispose();
}
}
main();
-41
View File
@@ -1,41 +0,0 @@
<!--
Copyright 2025 DeepMind Technologies Limited
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>MuJoCo WebAssembly</title>
<style>
#button-container {
position: fixed;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
display: flex;
gap: 10px;
}
</style>
</head>
<body>
<script type="module" src="./app.ts"></script>
<div id="button-container">
<button id="pause-button">Pause</button>
<button id="reset-button">Reset</button>
<button id="contact-button">Show Contacts</button>
</div>
</body>
</html>
-27
View File
@@ -1,27 +0,0 @@
// Copyright 2025 DeepMind Technologies Limited
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { defineConfig } from "vite"
export default defineConfig({
root: "demo_app",
base: "./",
build: {
outDir: "../demo-dist",
emptyOutDir: true,
},
server: {
open: true,
},
})
-1707
View File
File diff suppressed because it is too large Load Diff
-29
View File
@@ -1,29 +0,0 @@
{
"name": "mujoco_wasm",
"version": "1.0.0-alpha.1",
"description": "MuJoCo JavaScript Bindings",
"directories": {
"example": "examples",
"lib": "lib",
"test": "tests"
},
"scripts": {
"test": "node --loader ts-node/esm --trace-warnings tests/run-tests.mjs",
"dev:sandbox": "vite --config tests/sandbox/vite.sandbox.config.ts",
"build:sandbox": "vite build --config tests/sandbox/vite.sandbox.config.ts",
"dev:demo": "vite --config demo_app/vite.demo.config.ts",
"build:demo": "vite build --config demo_app/vite.demo.config.ts"
},
"author": "Google DeepMind",
"license": "Apache-2.0",
"devDependencies": {
"@types/jasmine": "^5.1.8",
"@types/node": "^24.1.0",
"jasmine": "^5.9.0",
"three": "^0.178.0",
"ts-node": "^10.9.2",
"typescript": "5.8.2",
"vite": "^7.0.6"
},
"type": "module"
}
-60
View File
@@ -1,60 +0,0 @@
# Copyright 2025 DeepMind Technologies Limited
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_SOURCE_DIR}/wasm/dist")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++20 -O3")
set(CMAKE_INSTALL_PREFIX ${PROJECT_SOURCE_DIR}/wasm)
include_directories(${PROJECT_SOURCE_DIR}/include)
include_directories(${PROJECT_SOURCE_DIR}/src)
include_directories(${PROJECT_SOURCE_DIR}/wasm)
link_directories(${CMAKE_BINARY_DIR}/lib)
file(GLOB MUJOCO_WASM_FILES
"benchmark_test.cc"
"../unpack.cc"
)
if(NOT MUJOCO_WASM_FILES)
message(FATAL_ERROR "No source files found")
endif()
add_compile_options(-pthread)
# Set Emscripten linker flags
set(EMCC_LINKER_FLAGS
"--bind"
"-s ASSERTIONS=1"
"-s ALLOW_MEMORY_GROWTH=1"
"-s EXPORT_ES6=1"
"-s MODULARIZE=1"
"-s FORCE_FILESYSTEM=1"
"-s EXPORTED_RUNTIME_METHODS=['ccall','cwrap','FS','MEMFS']"
"-s EXPORT_NAME=loadMujoco"
"-gsource-map"
"-g"
"--emit-tsd mujoco_wasm_benchmark.d.ts"
)
string (REPLACE ";" " " EMCC_LINKER_FLAGS_STR "${EMCC_LINKER_FLAGS}")
add_executable(mujoco_wasm_benchmark ${MUJOCO_WASM_FILES})
set_target_properties(mujoco_wasm_benchmark PROPERTIES LINK_FLAGS "${EMCC_LINKER_FLAGS_STR}")
target_link_libraries(mujoco_wasm_benchmark ccd lodepng mujoco tinyxml2 qhullstatic_r)
install(TARGETS mujoco_wasm_benchmark DESTINATION ${DIVISIBLE_INSTALL_BIN_DIR})
-54
View File
@@ -1,54 +0,0 @@
// Copyright 2025 DeepMind Technologies Limited
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include <emscripten/bind.h>
#include <emscripten/val.h>
#include <algorithm>
#include <mujoco/mujoco.h>
#include "unpack.h"
namespace mujoco::wasm {
using emscripten::val;
EMSCRIPTEN_DECLARE_VAL_TYPE(NumberArray);
// TODO(matijak): Add a benchmark where the buffer shared with JS and C++ is
// created in C++ rather than JS.
val BenchmarkSortNumberArray(const NumberArray& vec) {
UNPACK_ARRAY(mjtNum, vec);
std::sort(vec_.data(), vec_.data() + vec_.size());
return val::array(vec_.data(), vec_.data() + vec_.size());
}
val BenchmarkSortDoubleBuffer(const val& vec) {
UNPACK_VALUE(mjtNum, vec);
std::sort(vec_.data(), vec_.data() + vec_.size());
return vec;
}
EMSCRIPTEN_BINDINGS(mujoco_benchmark_functions) {
emscripten::class_<WasmBuffer<mjtNum>>("DoubleBuffer")
.constructor<int>()
.class_function("FromArray", &WasmBuffer<mjtNum>::FromArray)
.function("GetPointer", &WasmBuffer<mjtNum>::GetPointer)
.function("GetElementCount", &WasmBuffer<mjtNum>::GetElementCount)
.function("GetView", &WasmBuffer<mjtNum>::GetView);
emscripten::function("SortNumberArray", &BenchmarkSortNumberArray);
emscripten::function("SortDoubleBuffer", &BenchmarkSortDoubleBuffer);
emscripten::register_type<NumberArray>("number[]");
}
} // namespace mujoco::wasm
-113
View File
@@ -1,113 +0,0 @@
// Copyright 2025 DeepMind Technologies Limited
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'jasmine';
import { MainModule, DoubleBuffer } from "../dist/mujoco_wasm_benchmark"
import loadMujoco from "../dist/mujoco_wasm_benchmark.js"
describe('MuJoCo WASM Benchmark Tests', () => {
let mujoco: MainModule;
function isNumberArraySorted(arr: number[]): boolean {
for (let i = 0; i < arr.length - 1; i++) {
if (arr[i] > arr[i + 1]) {
return false;
}
}
return true;
}
function verifySorted(state: any, kind: string): void {
let array: number[] = [];
if (kind === 'NumberArray') {
array = state as number[];
} else {
const bufferView = (state as DoubleBuffer).GetView();
array = Array.from(bufferView) as number[];
}
const sorted = isNumberArraySorted(array);
if (!sorted) {
console.error(` Verification failed for ${kind}. Array was ${array}`);
}
}
function runBenchmark(iterations: number, items: number, kind: string) {
let state: any;
let func: (state: any) => any;
if (kind === 'NumberArray') {
state = new Array(items).map(() => Math.random())
func = (state) => mujoco.SortNumberArray(state);
} else if (kind === 'DoubleBuffer') {
state = mujoco.DoubleBuffer.FromArray(
new Array(items).map(() => Math.random()));
func = (state) => mujoco.SortDoubleBuffer(state);
} else {
throw new Error(`Unsupported benchmark type: ${kind}`);
}
// Warmup JIT compiler to get stable results
for (let i = 0; i < 1000; i++) {
const sortedState = func(state);
verifySorted(sortedState, kind);
}
// Measurement
const totalStartTime = performance.now();
for (let i = 0; i < iterations; i++) {
func(state);
}
const totalEndTime = performance.now();
if (kind === 'DoubleBuffer') {
state.delete();
}
// Report results
const totalTime = totalEndTime - totalStartTime;
const avgTimeMilliseconds = totalTime / iterations;
console.log(`Benchmark: "Sort ${kind} ${iterations} iterations with ${
items} items" - AVG time per call: ${
avgTimeMilliseconds.toFixed(2)} ms`);
return {
totalTime, avgTimeMilliseconds,
}
}
beforeAll(async () => {
mujoco = await loadMujoco();
});
it('should benchmark NumberArray and DoubleBuffer and compare results',
() => {
const na1 = runBenchmark(100, 1_000, 'NumberArray');
const na2 = runBenchmark(100, 400_000, 'NumberArray');
const db1 = runBenchmark(100, 1_000, 'DoubleBuffer');
const db2 = runBenchmark(100, 400_000, 'DoubleBuffer');
// The actual time should be much faster than that but the intention of
// the check is to catch huge regressions without a flakey test
const _100ms = 100;
expect(db2.avgTimeMilliseconds).toBeLessThan(_100ms);
expect(db1.totalTime).toBeLessThan(na1.totalTime);
expect(db2.totalTime).toBeLessThan(na2.totalTime);
expect(db1.avgTimeMilliseconds).toBeLessThan(na1.avgTimeMilliseconds);
expect(db2.avgTimeMilliseconds).toBeLessThan(na2.avgTimeMilliseconds);
});
});
File diff suppressed because it is too large Load Diff
-274
View File
@@ -1,274 +0,0 @@
// Copyright 2025 DeepMind Technologies Limited
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'jasmine';
import { MainModule } from "../dist/mujoco_wasm"
import loadMujoco from "../dist/mujoco_wasm.js"
let mujoco: MainModule;
describe('Enums', () => {
beforeAll(async () => {
mujoco = await loadMujoco();
});
it('mjtDisableBit should exist', () => {
expect(mujoco.mjtDisableBit).toBeDefined();
});
it('mjtEnableBit should exist', () => {
expect(mujoco.mjtEnableBit).toBeDefined();
});
it('mjtJoint should exist', () => {
expect(mujoco.mjtJoint).toBeDefined();
});
it('mjtGeom should exist', () => {
expect(mujoco.mjtGeom).toBeDefined();
});
it('mjtCamLight should exist', () => {
expect(mujoco.mjtCamLight).toBeDefined();
});
it('mjtLightType should exist', () => {
expect(mujoco.mjtLightType).toBeDefined();
});
it('mjtTexture should exist', () => {
expect(mujoco.mjtTexture).toBeDefined();
});
it('mjtTextureRole should exist', () => {
expect(mujoco.mjtTextureRole).toBeDefined();
});
it('mjtColorSpace should exist', () => {
expect(mujoco.mjtColorSpace).toBeDefined();
});
it('mjtIntegrator should exist', () => {
expect(mujoco.mjtIntegrator).toBeDefined();
});
it('mjtCone should exist', () => {
expect(mujoco.mjtCone).toBeDefined();
});
it('mjtJacobian should exist', () => {
expect(mujoco.mjtJacobian).toBeDefined();
});
it('mjtSolver should exist', () => {
expect(mujoco.mjtSolver).toBeDefined();
});
it('mjtEq should exist', () => {
expect(mujoco.mjtEq).toBeDefined();
});
it('mjtWrap should exist', () => {
expect(mujoco.mjtWrap).toBeDefined();
});
it('mjtTrn should exist', () => {
expect(mujoco.mjtTrn).toBeDefined();
});
it('mjtDyn should exist', () => {
expect(mujoco.mjtDyn).toBeDefined();
});
it('mjtGain should exist', () => {
expect(mujoco.mjtGain).toBeDefined();
});
it('mjtBias should exist', () => {
expect(mujoco.mjtBias).toBeDefined();
});
it('mjtObj should exist', () => {
expect(mujoco.mjtObj).toBeDefined();
});
it('mjtSensor should exist', () => {
expect(mujoco.mjtSensor).toBeDefined();
});
it('mjtStage should exist', () => {
expect(mujoco.mjtStage).toBeDefined();
});
it('mjtDataType should exist', () => {
expect(mujoco.mjtDataType).toBeDefined();
});
it('mjtConDataField should exist', () => {
expect(mujoco.mjtConDataField).toBeDefined();
});
it('mjtSameFrame should exist', () => {
expect(mujoco.mjtSameFrame).toBeDefined();
});
it('mjtLRMode should exist', () => {
expect(mujoco.mjtLRMode).toBeDefined();
});
it('mjtFlexSelf should exist', () => {
expect(mujoco.mjtFlexSelf).toBeDefined();
});
it('mjtSDFType should exist', () => {
expect(mujoco.mjtSDFType).toBeDefined();
});
it('mjtTaskStatus should exist', () => {
expect(mujoco.mjtTaskStatus).toBeDefined();
});
it('mjtState should exist', () => {
expect(mujoco.mjtState).toBeDefined();
});
it('mjtConstraint should exist', () => {
expect(mujoco.mjtConstraint).toBeDefined();
});
it('mjtConstraintState should exist', () => {
expect(mujoco.mjtConstraintState).toBeDefined();
});
it('mjtWarning should exist', () => {
expect(mujoco.mjtWarning).toBeDefined();
});
it('mjtTimer should exist', () => {
expect(mujoco.mjtTimer).toBeDefined();
});
it('mjtCatBit should exist', () => {
expect(mujoco.mjtCatBit).toBeDefined();
});
it('mjtMouse should exist', () => {
expect(mujoco.mjtMouse).toBeDefined();
});
it('mjtPertBit should exist', () => {
expect(mujoco.mjtPertBit).toBeDefined();
});
it('mjtCamera should exist', () => {
expect(mujoco.mjtCamera).toBeDefined();
});
it('mjtLabel should exist', () => {
expect(mujoco.mjtLabel).toBeDefined();
});
it('mjtFrame should exist', () => {
expect(mujoco.mjtFrame).toBeDefined();
});
it('mjtVisFlag should exist', () => {
expect(mujoco.mjtVisFlag).toBeDefined();
});
it('mjtRndFlag should exist', () => {
expect(mujoco.mjtRndFlag).toBeDefined();
});
it('mjtStereo should exist', () => {
expect(mujoco.mjtStereo).toBeDefined();
});
it('mjtPluginCapabilityBit should exist', () => {
expect(mujoco.mjtPluginCapabilityBit).toBeDefined();
});
it('mjtGridPos should exist', () => {
expect(mujoco.mjtGridPos).toBeDefined();
});
it('mjtFramebuffer should exist', () => {
expect(mujoco.mjtFramebuffer).toBeDefined();
});
it('mjtDepthMap should exist', () => {
expect(mujoco.mjtDepthMap).toBeDefined();
});
it('mjtFontScale should exist', () => {
expect(mujoco.mjtFontScale).toBeDefined();
});
it('mjtFont should exist', () => {
expect(mujoco.mjtFont).toBeDefined();
});
it('mjtGeomInertia should exist', () => {
expect(mujoco.mjtGeomInertia).toBeDefined();
});
it('mjtMeshInertia should exist', () => {
expect(mujoco.mjtMeshInertia).toBeDefined();
});
it('mjtMeshBuiltin should exist', () => {
expect(mujoco.mjtMeshBuiltin).toBeDefined();
});
it('mjtBuiltin should exist', () => {
expect(mujoco.mjtBuiltin).toBeDefined();
});
it('mjtMark should exist', () => {
expect(mujoco.mjtMark).toBeDefined();
});
it('mjtLimited should exist', () => {
expect(mujoco.mjtLimited).toBeDefined();
});
it('mjtAlignFree should exist', () => {
expect(mujoco.mjtAlignFree).toBeDefined();
});
it('mjtInertiaFromGeom should exist', () => {
expect(mujoco.mjtInertiaFromGeom).toBeDefined();
});
it('mjtOrientation should exist', () => {
expect(mujoco.mjtOrientation).toBeDefined();
});
it('mjtButton should exist', () => {
expect(mujoco.mjtButton).toBeDefined();
});
it('mjtEvent should exist', () => {
expect(mujoco.mjtEvent).toBeDefined();
});
it('mjtItem should exist', () => {
expect(mujoco.mjtItem).toBeDefined();
});
it('mjtSection should exist', () => {
expect(mujoco.mjtSection).toBeDefined();
});
});
-10
View File
@@ -1,10 +0,0 @@
{
"extension": {
"karma": {
"client": {
"pingTimeout": 300000
},
"browserDisconnectTimeout": 300000
}
}
}
-40
View File
@@ -1,40 +0,0 @@
// Copyright 2025 DeepMind Technologies Limited
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import Jasmine from 'jasmine';
const jasmine = new Jasmine();
jasmine.loadConfig({
spec_dir: 'tests',
spec_files: ['**/*_test.ts'],
jsLoader: 'import',
random: false,
stopSpecOnExpectationFailure: false,
});
try {
console.log('Starting Jasmine test run...');
const result = await jasmine.execute();
console.log(`Jasmine test run finished. Status: ${result.overallStatus}`);
if (result.overallStatus === 'failed') {
process.exit(1);
}
} catch (error) {
console.error('Test runner script failed:', error);
process.exit(1);
}
-26
View File
@@ -1,26 +0,0 @@
<!--
Copyright 2025 DeepMind Technologies Limited
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>MuJoCo WebAssembly</title>
</head>
<body>
<script type="module" src="./main.ts"></script>
</body>
</html>
-64
View File
@@ -1,64 +0,0 @@
// Copyright 2025 DeepMind Technologies Limited
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { MainModule, MjData, MjModel } from "../../dist/mujoco_wasm"
import loadMujoco from "../../dist/mujoco_wasm.js"
declare function loadMujoco(): Promise<MainModule>;
async function main() {
const mujoco: MainModule = await loadMujoco();
(mujoco as any).FS.mkdir('/working');
(mujoco as any).FS.mount((mujoco as any).MEMFS, {root: '.'}, '/working');
const xmlContent = `
<mujoco model="Box falling">
<option viscosity="1"/>
<worldbody>
<light diffuse=".5 .5 .5" pos="0 0 3" dir="0 0 -1"/>
<geom name="MyFloor" type="plane" size="1 1 0.1" rgba=".9 0 0 1" user="5 4 3 2 1"/>
<geom name="MyWall" type="plane" size="0.1 1 0.1" rgba="0 1 0 1" user="5 4 3"/>
<body pos="0 0 1" name="MyBox">
<joint type="free"/>
<geom name="MyBoxGeom" type="box" size=".1 .2 .3" rgba="0 .9 0 1"/>
</body>
</worldbody>
</mujoco>`;
(mujoco as any).FS.writeFile('/working/hello.xml', xmlContent);
let model: MjModel|undefined;
let data: MjData|undefined;
try {
console.log('Hello world!: Loading model');
model = mujoco.MjModel.loadFromXML('/working/hello.xml');
if (!model) {
throw new Error('Failed to load model');
}
data = new mujoco.MjData(model);
if (!data) {
throw new Error('Failed to load data');
}
// Add your test code here...
} finally {
model?.delete();
data?.delete();
(mujoco as any).FS.unmount('/working');
}
}
main()
-27
View File
@@ -1,27 +0,0 @@
// Copyright 2025 DeepMind Technologies Limited
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { defineConfig } from "vite"
export default defineConfig({
root: 'tests/sandbox',
base: './',
build: {
outDir: '../../sandbox-dist',
emptyOutDir: true,
},
server: {
open: true,
},
})
-11
View File
@@ -1,11 +0,0 @@
{
"compilerOptions": {
"module": "ESNext",
"target": "ES2020",
"moduleResolution": "node",
"esModuleInterop": true,
"strict": true,
"types": ["jasmine"]
},
"include": ["tests/**/*"]
}
-35
View File
@@ -1,35 +0,0 @@
// Copyright 2025 DeepMind Technologies Limited
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include "unpack.h"
#include <cstddef>
#include <cstring>
#include <string>
namespace mujoco::wasm {
std::string StripWrapperSuffix(const char* func) {
const char* suffix = "_wrapper";
size_t name_len = strlen(func);
size_t suffix_len = strlen(suffix);
if (name_len >= suffix_len &&
strcmp(func + name_len - suffix_len, suffix) == 0) {
return std::string(func, name_len - suffix_len);
}
return std::string(func);
}
} // namespace mujoco::wasm
-297
View File
@@ -1,297 +0,0 @@
// Copyright 2025 DeepMind Technologies Limited
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#ifndef MUJOCO_WASM_UNPACK_H_
#define MUJOCO_WASM_UNPACK_H_
#ifdef __EMSCRIPTEN__
#include <emscripten/val.h>
#include <cinttypes> // NOLINT required for PRId64
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <span>
#include <string>
#include <variant>
#include <vector>
#include "engine/engine_util_errmem.h"
namespace mujoco::wasm {
// Helper to strip "_wrapper" from function names.
std::string StripWrapperSuffix(const char* func);
// Utility class to write/read from the Heap shared by C++ and Javascript
template <typename T>
class WasmBuffer {
private:
// Note: Embind does not support binding more than one constructor with the
// same argument count so we bind the factory function fromArray instead
explicit WasmBuffer(const emscripten::val& array) {
std::vector<T> cpp_array = convertJSArrayToNumberVector<T>(array);
bytes_.resize(cpp_array.size() * sizeof(T));
if (cpp_array.size() > 0) {
memcpy(bytes_.data(), cpp_array.data(), bytes_.size());
}
}
public:
// Creates a buffer with the given element count
explicit WasmBuffer(int element_count = 0) {
bytes_.resize(element_count * sizeof(T));
}
// Creates a buffer by copying data from a (typed) array
static WasmBuffer<T> FromArray(const emscripten::val& array) {
return WasmBuffer<T>(array);
}
// Returns the pointer to the data in the buffer
uintptr_t GetPointer() { return reinterpret_cast<uintptr_t>(bytes_.data()); }
// Returns the number of elements in the buffer
int GetElementCount() { return bytes_.size() / sizeof(T); }
// Returns a TypedArray view of the buffer
emscripten::val GetView() {
return emscripten::val(emscripten::typed_memory_view(
bytes_.size() / sizeof(T), reinterpret_cast<const T*>(bytes_.data())));
}
void Zero() {
if (!bytes_.empty()) {
memset(bytes_.data(), 0, bytes_.size());
}
}
private:
std::vector<std::byte> bytes_;
};
template <typename T>
class UnpackedParam {
// The C++ representation of the parameter data
std::variant<std::monostate, std::vector<T>, std::span<T>> data_;
// Printable representations of the param and function name used for errors
const char* repr_;
const char* func_;
explicit UnpackedParam(const char* repr, const char* func)
: data_(std::monostate{}), repr_(repr), func_(func) {}
UnpackedParam(std::vector<T>&& array, const char* repr, const char* func)
: data_(std::move(array)), repr_(repr), func_(func) {}
UnpackedParam(T* data, std::size_t count, const char* repr, const char* func)
: data_(std::span<T>(data, count)), repr_(repr), func_(func) {}
// Returns true and raises an error if the val is null or undefined.
static bool ErrorOnNullOrUndefined(const emscripten::val& p,
const char* func,
const char* expected_type) {
if (p.isUndefined()) {
mju_error("[%s] Invalid argument. Expected a %s, got undefined.",
StripWrapperSuffix(func).c_str(), expected_type);
return true;
} else if (p.isNull()) {
mju_error("[%s] Invalid argument. Expected a %s, got null.",
StripWrapperSuffix(func).c_str(), expected_type);
return true;
}
return false;
}
// Returns true if the val is null or undefined. Use when these are expected.
static bool IsNullOrUndefined(const emscripten::val& p) {
return p.isUndefined() || p.isNull();
}
public:
// Create from a nullable Javascript val. Call via UNPACK_NULLABLE_VALUE.
static UnpackedParam<T> FromNullableValue(const emscripten::val& p,
const char* repr,
const char* func) {
if (IsNullOrUndefined(p)) {
return UnpackedParam<T>(repr, func);
}
return FromValue(p, repr, func);
}
// Create from a nullable Javascript number[]. Call via UNPACK_NULLABLE_ARRAY.
static UnpackedParam<T> FromNullableArray(const emscripten::val& p,
const char* repr,
const char* func) {
if (IsNullOrUndefined(p)) {
return UnpackedParam<T>(repr, func);
}
return UnpackedParam<T>(convertJSArrayToNumberVector<T>(p), repr, func);
}
// Create from a Javascript number[]. Call via UNPACK_ARRAY.
static UnpackedParam<T> FromArray(const emscripten::val& p, const char* repr,
const char* func) {
ErrorOnNullOrUndefined(p, func, "number[]");
return UnpackedParam<T>(convertJSArrayToNumberVector<T>(p), repr, func);
}
// Creates an UnpackedParam from a Javascript a TypedArray or a WasmBuffer.
// Call via UNPACK_VALUE.
static UnpackedParam<T> FromValue(const emscripten::val& p, const char* repr,
const char* func) {
ErrorOnNullOrUndefined(p, func, "TypedArray or WasmBuffer");
if (!p["byteOffset"].isUndefined()) { // Javascript TypedArray
T* data = reinterpret_cast<T*>(p["byteOffset"].as<uintptr_t>());
std::size_t count = p["length"].as<std::size_t>();
return UnpackedParam<T>(data, count, repr, func);
} else if (!p["GetPointer"].isUndefined()) { // C++ WasmBuffer
WasmBuffer<T>& buffer = p.as<WasmBuffer<T>&>();
T* data = reinterpret_cast<T*>(buffer.GetPointer());
std::size_t count = buffer.GetElementCount();
return UnpackedParam<T>(data, count, repr, func);
}
// TODO(manevi): This error message is not 100% accurate, WasmBuffer class
// isn't surfaced to JS developers
auto param = UnpackedParam<T>(repr, func);
mju_error(
"[%s] Invalid argument. Expected TypedArray or WasmBuffer, got "
"unknown type for %s.",
param.func().c_str(), param.repr());
return param;
}
// Returns true if the parameter is not null. Used in if conditions.
explicit operator bool() const {
return !std::holds_alternative<std::monostate>(data_);
}
// Returns the printable representation of the parameter for use in error
// messages.
const char* repr() const { return repr_; }
// Returns the name of the function the parameter is used in.
std::string func() const { return StripWrapperSuffix(func_); }
// Returns the size of the parameter. Returns 0 if the parameter is null.
std::size_t size() const {
if (std::holds_alternative<std::vector<T>>(data_)) {
return std::get<std::vector<T>>(data_).size();
} else if (std::holds_alternative<std::span<T>>(data_)) {
return std::get<std::span<T>>(data_).size();
}
mju_error("[%s] [%s] UnpackedParam is null", func().c_str(), repr());
return 0;
}
// Returns a pointer to the data of the parameter. Returns nullptr if the
// parameter is null.
const T* data() const {
if (std::holds_alternative<std::vector<T>>(data_)) {
return std::get<std::vector<T>>(data_).data();
} else if (std::holds_alternative<std::span<T>>(data_)) {
return std::get<std::span<T>>(data_).data();
}
return nullptr;
}
// Returns a non-const pointer to the data of the parameter. Returns nullptr
// if the parameter is null.
T* data() {
if (std::holds_alternative<std::vector<T>>(data_)) {
return std::get<std::vector<T>>(data_).data();
} else if (std::holds_alternative<std::span<T>>(data_)) {
return const_cast<T*>(std::get<std::span<T>>(data_).data());
}
return nullptr;
}
};
// TODO(matijak): When the bindings are fully auto-generated we could replace
// these macros with a function calls something like this:
//
// template <typename T, typename U>
// UnpackedParam<T> Unpack(U&& u, const char* u_name,
// const std::source_location location = std::source_location::current()) {
// return UnpackedParam<T>::FromValue(std::forward<U>(u), u_name,
// location.file_name(), location.line(), location.function_name());
// }
#define UNPACK_VALUE(T, p) \
UnpackedParam<T> p##_ = UnpackedParam<T>::FromValue(p, #p, __func__)
#define UNPACK_ARRAY(T, p) \
UnpackedParam<T> p##_ = UnpackedParam<T>::FromArray(p, #p, __func__)
#define UNPACK_NULLABLE_VALUE(T, p) \
UnpackedParam<T> p##_ = UnpackedParam<T>::FromNullableValue(p, #p, __func__)
#define UNPACK_NULLABLE_ARRAY(T, p) \
UnpackedParam<T> p##_ = UnpackedParam<T>::FromNullableArray(p, #p, __func__)
// Raises an error if x##_.size() is not equal to expr.
// Assumes UnpackedParam x##_ is defined.
#define CHECK_SIZE(x, expr) \
if (x##_) { \
if (static_cast<int64_t>(x##_.size()) != static_cast<int64_t>(expr)) { \
mju_error("[%s] %s must have size %" PRId64 ", got %" PRId64, \
x##_.func().c_str(), x##_.repr(), static_cast<int64_t>(expr), \
static_cast<int64_t>(x##_.size())); \
} \
}
// Raises an error if x##_.size() is not equal to y##_.size().
// Assumes UnpackedParams x##_ and y##_ are defined.
#define CHECK_SIZES(x, y) \
if (x##_ && y##_) { \
if (static_cast<int64_t>(x##_.size()) != \
static_cast<int64_t>(y##_.size())) { \
mju_error("[%s] %s and %s must have equal size, got %" PRId64 \
" and %" PRId64, \
x##_.func().c_str(), x##_.repr(), y##_.repr(), \
static_cast<int64_t>(x##_.size()), \
static_cast<int64_t>(y##_.size())); \
} \
}
// Raises an error if x##_.size() is not a perfect square.
// Assumes UnpackedParam x##_ is defined. Defines x##_sqrt as an int.
#define CHECK_PERFECT_SQUARE(x) \
const int x##_sqrt = static_cast<int>(round(sqrt(x##_.size()))); \
if (x##_sqrt * x##_sqrt != x##_.size()) { \
mjERROR("[%s] %s must be a perfect square, got %" PRId64, \
x##_.func().c_str(), x##_.repr(), \
static_cast<int64_t>(x##_.size())); \
}
// Raises an error if x##_.size() is not divisible by divisor.
// Assumes UnpackedParam x##_ is defined. Defines x##_div as an std::div_t.
#define CHECK_DIVISIBLE(x, divisor) \
const std::div_t x##_div = \
std::div(static_cast<int>(x##_.size()), static_cast<int>(divisor)); \
if (x##_div.rem != 0) { \
mju_error("[%s] %s must be divisible by %d, got quot=%d rem=%d", \
x##_.func().c_str(), x##_.repr(), static_cast<int>(divisor), \
x##_div.quot, x##_div.rem); \
}
} // namespace mujoco::wasm
#endif // __EMSCRIPTEN__
#endif // MUJOCO_WASM_UNPACK_H_