// 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 #include // NOLINT required for PRId64 #include #include #include #include #include #include #include #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 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 cpp_array = convertJSArrayToNumberVector(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) { Resize(element_count); } // Creates a buffer by copying data from a (typed) array static WasmBuffer FromArray(const emscripten::val& array) { return WasmBuffer(array); } // Resizes the buffer to the given element count. // If element count is zero the memory is released. void Resize(int element_count) { if (element_count == 0) { std::vector empty; bytes_.swap(empty); } else { bytes_.resize(element_count * sizeof(T)); } } // Returns the pointer to the data in the buffer uintptr_t GetPointer() { return reinterpret_cast(bytes_.data()); } // Returns the number of elements in the buffer int GetElementCount() { return bytes_.size() / sizeof(T); } // Returns a TypedArray view of the buffer. // Do not cache this value, bytes_.data() is invalidated on Resize! emscripten::val GetView() { return emscripten::val(emscripten::typed_memory_view( bytes_.size() / sizeof(T), reinterpret_cast(bytes_.data()))); } void Zero() { if (!bytes_.empty()) { memset(bytes_.data(), 0, bytes_.size()); } } private: std::vector bytes_; }; template class UnpackedParam { // The C++ representation of the parameter data std::variant, std::span, std::string> 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&& 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(data, count)), repr_(repr), func_(func) {} UnpackedParam(std::string&& str, const char* repr, const char* func) : data_(std::move(str)), repr_(repr), func_(func) {} // Returns true and raises an error if the val is null or undefined. // This function should never be called when unpacking nullable values. 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 FromNullableValue(const emscripten::val& p, const char* repr, const char* func) { if (IsNullOrUndefined(p)) { return UnpackedParam(repr, func); } return FromValue(p, repr, func); } // Create from a nullable Javascript number[]. Call via UNPACK_NULLABLE_ARRAY. static UnpackedParam FromNullableArray(const emscripten::val& p, const char* repr, const char* func) { if (IsNullOrUndefined(p)) { return UnpackedParam(repr, func); } return UnpackedParam(convertJSArrayToNumberVector(p), repr, func); } // Create from a Javascript number[]. Call via UNPACK_ARRAY. static UnpackedParam FromArray(const emscripten::val& p, const char* repr, const char* func) { ErrorOnNullOrUndefined(p, func, "number[]"); return UnpackedParam(convertJSArrayToNumberVector(p), repr, func); } // Create from a nullable Javascript string. Call via UNPACK_NULLABLE_STRING. static UnpackedParam FromNullableString(const emscripten::val& p, const char* repr, const char* func) { if (IsNullOrUndefined(p)) { return UnpackedParam(repr, func); } if (!p.isString()) { mju_error( "[%s] Invalid argument. Expected a string for %s.", StripWrapperSuffix(func).c_str(), repr); return UnpackedParam(repr, func); } static_assert(std::is_same_v, "UNPACK_NULLABLE_STRING requires UnpackedParam"); return UnpackedParam(p.as(), repr, func); } // Creates an UnpackedParam from a Javascript a TypedArray or a WasmBuffer. // Call via UNPACK_VALUE. static UnpackedParam 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(p["byteOffset"].as()); std::size_t count = p["length"].as(); return UnpackedParam(data, count, repr, func); } else if (!p["GetPointer"].isUndefined()) { // C++ WasmBuffer WasmBuffer& buffer = p.as&>(); T* data = reinterpret_cast(buffer.GetPointer()); std::size_t count = buffer.GetElementCount(); return UnpackedParam(data, count, repr, func); } // TODO(manevi): This error message is not 100% accurate, WasmBuffer class // isn't surfaced to JS developers auto param = UnpackedParam(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(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. For // strings, returns the length of the string. std::size_t size() const { if (std::holds_alternative>(data_)) { return std::get>(data_).size(); } else if (std::holds_alternative>(data_)) { return std::get>(data_).size(); } else if (std::holds_alternative(data_)) { return std::get(data_).length(); } 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>(data_)) { return std::get>(data_).data(); } else if (std::holds_alternative>(data_)) { return std::get>(data_).data(); } else if (std::holds_alternative(data_)) { static_assert(std::is_same_v, "Cannot call data() on UnpackedParam with a string unless " "T is char."); return reinterpret_cast(std::get(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>(data_)) { return std::get>(data_).data(); } else if (std::holds_alternative>(data_)) { return const_cast(std::get>(data_).data()); } else if (std::holds_alternative(data_)) { if constexpr (std::is_same_v) { return reinterpret_cast(std::get(data_).data()); } else { mju_error( "[%s] [%s] Cannot call data() on UnpackedParam<%s> holding a string", func().c_str(), repr(), typeid(T).name()); } } return nullptr; } }; // TODO(matijak): When the bindings are fully auto-generated we could replace // these macros with a function calls something like this: // // template // UnpackedParam Unpack(U&& u, const char* u_name, // const std::source_location location = std::source_location::current()) { // return UnpackedParam::FromValue(std::forward(u), u_name, // location.file_name(), location.line(), location.function_name()); // } #define UNPACK_VALUE(T, p) \ UnpackedParam p##_ = UnpackedParam::FromValue(p, #p, __func__) #define UNPACK_ARRAY(T, p) \ UnpackedParam p##_ = UnpackedParam::FromArray(p, #p, __func__) #define UNPACK_NULLABLE_VALUE(T, p) \ UnpackedParam p##_ = UnpackedParam::FromNullableValue(p, #p, __func__) #define UNPACK_NULLABLE_ARRAY(T, p) \ UnpackedParam p##_ = UnpackedParam::FromNullableArray(p, #p, __func__) #define UNPACK_NULLABLE_STRING(p) \ UnpackedParam p##_ = UnpackedParam::FromNullableString( \ 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(x##_.size()) != static_cast(expr)) { \ mju_error("[%s] %s must have size %" PRId64 ", got %" PRId64, \ x##_.func().c_str(), x##_.repr(), static_cast(expr), \ static_cast(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(x##_.size()) != \ static_cast(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(x##_.size()), \ static_cast(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(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(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(x##_.size()), static_cast(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(divisor), \ x##_div.quot, x##_div.rem); \ } } // namespace mujoco::wasm #endif // __EMSCRIPTEN__ #endif // MUJOCO_WASM_UNPACK_H_