From df0bc5857857e168cc1cfb6324e304a349b981a6 Mon Sep 17 00:00:00 2001 From: Google DeepMind Date: Thu, 12 Mar 2026 06:58:20 -0700 Subject: [PATCH] Add aliases for MjModel loading functions and expose string constants as properties. Introduces `from_xml_path` and `from_binary_path` as more idiomatic aliases for `mj_loadXML` and `mj_loadModel` respectively. Refactors the binding of `mj*STRING` constants. Instead of exposing `get_mj*STRING` functions, these constants are now available as properties on the `Module` object. Runtime getters are added via EM_ASM to lazily call the underlying C++ functions. PiperOrigin-RevId: 882566011 Change-Id: Id3bd5c23704fd122af30a50aed089bd3143fbc96 --- wasm/README.md | 18 +++++++++++++----- wasm/codegen/generated/bindings.cc | 30 +++++++++++++++++++++++++++++- wasm/codegen/generators/structs.py | 10 ++++++++++ wasm/codegen/templates/bindings.cc | 27 ++++++++++++++++++++++++++- wasm/tests/bindings_test.ts | 14 +++++++------- 5 files changed, 85 insertions(+), 14 deletions(-) diff --git a/wasm/README.md b/wasm/README.md index 2938ec5c..ebadb94e 100644 --- a/wasm/README.md +++ b/wasm/README.md @@ -348,23 +348,31 @@ mujoco.mjtDisableBit.mjDSBL_CLAMPCTRL.value ``` ### Constants -Scalar constants will be accessed the same way they are on python, simply: +Scalar constants can be accessed as properties: ```javascript mujoco.mjNEQDATA ``` -Due to Embind limitations, more complex constants that are not scalar, but are -represented in more dimensions are exposed as functions. E.g. to use -`mujoco.mjFRAMESTRING` you will need to call a function: +Non-scalar constants like `mjFRAMESTRING` are also accessed as properties, and +return JavaScript arrays: ```javascript -mujoco.get_mjFRAMESTRING() +mujoco.mjFRAMESTRING ``` This will return a javascript array representation of the values in MuJoCo `mjFRAMESTRING`. +> [!NOTE] +> You will notice constants like `mjFRAMESTRING` are typed as `any`. This is +> because they are bound using `emscripten::val::array()` in C++, and Embind +> maps `emscripten::val` to `any` in TypeScript definition files. While +> `EMSCRIPTEN_DECLARE_VAL_TYPE(StringArray)` could be used to define +> `StringArray` as an alias for `emscripten::val` and hint to Embind how to +> handle conversions in function signatures or when using `.as()` it does not +> change how `emscripten::constant` infers types for properties. + ## Development In order to change the bindings you will need to change the [`bindings.cc`](codegen/generated/bindings.cc) diff --git a/wasm/codegen/generated/bindings.cc b/wasm/codegen/generated/bindings.cc index dfcb4a68..dbf9646d 100644 --- a/wasm/codegen/generated/bindings.cc +++ b/wasm/codegen/generated/bindings.cc @@ -11597,8 +11597,11 @@ EMSCRIPTEN_BINDINGS(mujoco_bindings) { .property("uselimit", &MjLROpt::uselimit, &MjLROpt::set_uselimit, reference()); emscripten::class_("MjModel") .class_function("mj_loadXML", emscripten::select_overload(std::string)>(&mj_loadXML_wrapper_1)) + .class_function("from_xml_path", emscripten::select_overload(std::string)>(&mj_loadXML_wrapper_1)) .class_function("mj_loadXML", emscripten::select_overload(std::string, const MjVFS&)>(&mj_loadXML_wrapper_2)) + .class_function("from_xml_path", emscripten::select_overload(std::string, const MjVFS&)>(&mj_loadXML_wrapper_2)) .class_function("mj_loadModel", &mj_loadModel_wrapper) + .class_function("from_binary_path", &mj_loadModel_wrapper) .constructor() // Binds the functions on MjModel that return accessors. #define X_ACCESSOR(NAME, Name, OBJTYPE, field_name, nfield) \ @@ -13409,6 +13412,7 @@ EMSCRIPTEN_BINDINGS(mujoco_bindings) { .function("GetView", &WasmBuffer::GetView); emscripten::register_vector("mjStringVec"); + emscripten::register_vector>("mjStringVecVec"); emscripten::register_vector("mjIntVec"); emscripten::register_vector("mjIntVecVec"); emscripten::register_vector("mjFloatVec"); @@ -13453,7 +13457,6 @@ EMSCRIPTEN_BINDINGS(mujoco_bindings) { emscripten::constant("mjPI", mjPI); emscripten::constant("mjVERSION_HEADER", mjVERSION_HEADER); - // These complex constants are bound using function() rather than constant() emscripten::function("get_mjDISABLESTRING", &get_mjDISABLESTRING); emscripten::function("get_mjENABLESTRING", &get_mjENABLESTRING); emscripten::function("get_mjFRAMESTRING", &get_mjFRAMESTRING); @@ -13461,6 +13464,31 @@ EMSCRIPTEN_BINDINGS(mujoco_bindings) { emscripten::function("get_mjRNDSTRING", &get_mjRNDSTRING); emscripten::function("get_mjTIMERSTRING", &get_mjTIMERSTRING); emscripten::function("get_mjVISSTRING", &get_mjVISSTRING); + // Bind these complex constants as properties on the module object. + // We use emscripten::constant with emscripten::val::array() to type them + // as `any` in TypeScript. At runtime, the EM_ASM block below overrides + // these properties with getters that return native JavaScript arrays + // (string[] or string[][]) via the get_ functions above, which is more + // performant and idiomatic than vector wrappers. + emscripten::constant("mjDISABLESTRING", emscripten::val::array()); + emscripten::constant("mjENABLESTRING", emscripten::val::array()); + emscripten::constant("mjFRAMESTRING", emscripten::val::array()); + emscripten::constant("mjLABELSTRING", emscripten::val::array()); + emscripten::constant("mjRNDSTRING", emscripten::val::array()); + emscripten::constant("mjTIMERSTRING", emscripten::val::array()); + emscripten::constant("mjVISSTRING", emscripten::val::array()); + EM_ASM({ + if (typeof Module !== "undefined") { + "mjDISABLESTRING mjENABLESTRING mjFRAMESTRING mjLABELSTRING mjRNDSTRING mjTIMERSTRING mjVISSTRING".split(" ").forEach(function(name) { + Object.defineProperty(Module, name, { + get: function() { return Module["get_" + name](); }, + set: function(v) { }, + enumerable: true, + configurable: true + }); + }); + } + }); } } // namespace mujoco::wasm diff --git a/wasm/codegen/generators/structs.py b/wasm/codegen/generators/structs.py index 0517dba9..79c0b788 100644 --- a/wasm/codegen/generators/structs.py +++ b/wasm/codegen/generators/structs.py @@ -553,15 +553,25 @@ def _build_struct_bindings( '.class_function("mj_loadXML",' " emscripten::select_overload(std::string)>(&mj_loadXML_wrapper_1))" ) + builder.line( + '.class_function("from_xml_path",' + " emscripten::select_overload(std::string)>(&mj_loadXML_wrapper_1))" + ) builder.line( '.class_function("mj_loadXML",' " emscripten::select_overload(std::string," " const MjVFS&)>(&mj_loadXML_wrapper_2))" ) + builder.line( + '.class_function("from_xml_path",' + " emscripten::select_overload(std::string," + " const MjVFS&)>(&mj_loadXML_wrapper_2))" + ) f2 = common.wrapped_function_name( introspect_functions.FUNCTIONS["mj_loadModel"] ) builder.line(f'.class_function("mj_loadModel", &{f2})') + builder.line(f'.class_function("from_binary_path", &{f2})') builder.line(".constructor()") builder.line(""" // Binds the functions on MjModel that return accessors. diff --git a/wasm/codegen/templates/bindings.cc b/wasm/codegen/templates/bindings.cc index 8f2a1ecf..27e3ec5a 100644 --- a/wasm/codegen/templates/bindings.cc +++ b/wasm/codegen/templates/bindings.cc @@ -875,6 +875,7 @@ EMSCRIPTEN_BINDINGS(mujoco_bindings) { .function("GetView", &WasmBuffer::GetView); emscripten::register_vector("mjStringVec"); + emscripten::register_vector>("mjStringVecVec"); emscripten::register_vector("mjIntVec"); emscripten::register_vector("mjIntVecVec"); emscripten::register_vector("mjFloatVec"); @@ -919,7 +920,6 @@ EMSCRIPTEN_BINDINGS(mujoco_bindings) { emscripten::constant("mjPI", mjPI); emscripten::constant("mjVERSION_HEADER", mjVERSION_HEADER); - // These complex constants are bound using function() rather than constant() emscripten::function("get_mjDISABLESTRING", &get_mjDISABLESTRING); emscripten::function("get_mjENABLESTRING", &get_mjENABLESTRING); emscripten::function("get_mjFRAMESTRING", &get_mjFRAMESTRING); @@ -927,6 +927,31 @@ EMSCRIPTEN_BINDINGS(mujoco_bindings) { emscripten::function("get_mjRNDSTRING", &get_mjRNDSTRING); emscripten::function("get_mjTIMERSTRING", &get_mjTIMERSTRING); emscripten::function("get_mjVISSTRING", &get_mjVISSTRING); + // Bind these complex constants as properties on the module object. + // We use emscripten::constant with emscripten::val::array() to type them + // as `any` in TypeScript. At runtime, the EM_ASM block below overrides + // these properties with getters that return native JavaScript arrays + // (string[] or string[][]) via the get_ functions above, which is more + // performant and idiomatic than vector wrappers. + emscripten::constant("mjDISABLESTRING", emscripten::val::array()); + emscripten::constant("mjENABLESTRING", emscripten::val::array()); + emscripten::constant("mjFRAMESTRING", emscripten::val::array()); + emscripten::constant("mjLABELSTRING", emscripten::val::array()); + emscripten::constant("mjRNDSTRING", emscripten::val::array()); + emscripten::constant("mjTIMERSTRING", emscripten::val::array()); + emscripten::constant("mjVISSTRING", emscripten::val::array()); + EM_ASM({ + if (typeof Module !== "undefined") { + "mjDISABLESTRING mjENABLESTRING mjFRAMESTRING mjLABELSTRING mjRNDSTRING mjTIMERSTRING mjVISSTRING".split(" ").forEach(function(name) { + Object.defineProperty(Module, name, { + get: function() { return Module["get_" + name](); }, + set: function(v) { }, + enumerable: true, + configurable: true + }); + }); + } + }); } } // namespace mujoco::wasm diff --git a/wasm/tests/bindings_test.ts b/wasm/tests/bindings_test.ts index 4cff1ec9..37cf1548 100644 --- a/wasm/tests/bindings_test.ts +++ b/wasm/tests/bindings_test.ts @@ -683,24 +683,24 @@ describe('MuJoCo WASM Bindings', () => { it('should check constants values', () => { expect(mujoco.mjNEQDATA).toBe(11); - expect(mujoco.get_mjDISABLESTRING()).toEqual([ + expect(mujoco.mjDISABLESTRING).toEqual([ 'Constraint', 'Equality', 'Frictionloss', 'Limit', 'Contact', 'Spring', 'Damper', 'Gravity', 'Clampctrl', 'Warmstart', 'Filterparent', 'Actuation', 'Refsafe', 'Sensor', 'Midphase', 'Eulerdamp', 'AutoReset', 'NativeCCD', 'Island' ]); - expect(mujoco.get_mjRNDSTRING()).toEqual([ + expect(mujoco.mjRNDSTRING).toEqual([ ['Shadow', '1', 'S'], ['Wireframe', '0', 'W'], ['Reflection', '1', 'R'], ['Additive', '0', 'L'], ['Skybox', '1', 'K'], ['Fog', '0', 'G'], ['Haze', '1', '/'], ['Depth', '0', ''], ['Segment', '0', ','], ['Id Color', '0', ''], ['Cull Face', '1', ''] ]); - expect(mujoco.get_mjFRAMESTRING().length) - .toEqual(mujoco.mjtFrame.mjNFRAME.value); - expect(mujoco.get_mjVISSTRING().length) + expect(mujoco.mjFRAMESTRING.length).toEqual(mujoco.mjtFrame.mjNFRAME.value); + expect(mujoco.mjVISSTRING.length) .toEqual(mujoco.mjtVisFlag.mjNVISFLAG.value); - expect(mujoco.get_mjVISSTRING()[mujoco.mjtVisFlag.mjVIS_INERTIA.value]) - .toEqual(['Inertia', '0', 'I']); + expect(mujoco.mjVISSTRING[mujoco.mjtVisFlag.mjVIS_INERTIA.value]).toEqual([ + 'Inertia', '0', 'I' + ]); }); it('should create a spec from XML', () => {