Introduce new logging API, fixes #858

PiperOrigin-RevId: 930744288
Change-Id: I6ec1203b55c031390f3eef23192e2337508ce886
This commit is contained in:
Yuval Tassa
2026-06-11 14:36:17 -07:00
committed by Copybara-Service
parent a2abaf7aef
commit 58f6d52491
45 changed files with 2586 additions and 422 deletions
+180
View File
@@ -597,6 +597,90 @@ struct MjLROpt {
bool owned_ = false;
};
struct MjLogConfig {
~MjLogConfig();
MjLogConfig();
explicit MjLogConfig(mjLogConfig *ptr);
MjLogConfig(const MjLogConfig &);
MjLogConfig &operator=(const MjLogConfig &);
std::unique_ptr<MjLogConfig> copy();
mjLogConfig* get() const;
void set(mjLogConfig* ptr);
mjtBool logto_console() const {
return ptr_->logto_console;
}
void set_logto_console(mjtBool value) {
ptr_->logto_console = value;
}
mjtBool logto_file() const {
return ptr_->logto_file;
}
void set_logto_file(mjtBool value) {
ptr_->logto_file = value;
}
emscripten::val logfile() const {
return emscripten::val(emscripten::typed_memory_view(1024, ptr_->logfile));
}
int topics() const {
return ptr_->topics;
}
void set_topics(int value) {
ptr_->topics = value;
}
private:
mjLogConfig* ptr_;
bool owned_ = false;
};
struct MjLogMessage {
~MjLogMessage();
MjLogMessage();
explicit MjLogMessage(mjLogMessage *ptr);
mjLogMessage* get() const;
void set(mjLogMessage* ptr);
int level() const {
return ptr_->level;
}
void set_level(int value) {
ptr_->level = value;
}
int topic() const {
return ptr_->topic;
}
void set_topic(int value) {
ptr_->topic = value;
}
emscripten::val subject() const {
return emscripten::val(emscripten::typed_memory_view(1024, ptr_->subject));
}
std::string body() const {
return ptr_->body ? std::string(ptr_->body) : "";
}
std::string func() const {
return ptr_->func ? std::string(ptr_->func) : "";
}
std::string file() const {
return ptr_->file ? std::string(ptr_->file) : "";
}
int line() const {
return ptr_->line;
}
void set_line(int value) {
ptr_->line = value;
}
mjtBool timestamp() const {
return ptr_->timestamp;
}
void set_timestamp(mjtBool value) {
ptr_->timestamp = value;
}
private:
mjLogMessage* ptr_;
bool owned_ = false;
};
struct MjOption {
~MjOption();
MjOption();
@@ -7194,6 +7278,51 @@ void MjLROpt::set(mjLROpt* ptr) {
ptr_ = ptr;
}
MjLogConfig::MjLogConfig(mjLogConfig *ptr) : ptr_(ptr) {}
MjLogConfig::~MjLogConfig() {
if (owned_ && ptr_) {
delete ptr_;
}
}
MjLogConfig::MjLogConfig() : ptr_(new mjLogConfig()) {
owned_ = true;
}
MjLogConfig::MjLogConfig(const MjLogConfig &other) : MjLogConfig() {
*ptr_ = *other.get();
}
MjLogConfig& MjLogConfig::operator=(const MjLogConfig &other) {
if (this == &other) {
return *this;
}
*ptr_ = *other.get();
return *this;
}
std::unique_ptr<MjLogConfig> MjLogConfig::copy() {
return std::make_unique<MjLogConfig>(*this);
}
mjLogConfig* MjLogConfig::get() const {
return ptr_;
}
void MjLogConfig::set(mjLogConfig* ptr) {
ptr_ = ptr;
}
MjLogMessage::MjLogMessage(mjLogMessage *ptr) : ptr_(ptr) {}
MjLogMessage::~MjLogMessage() {
if (owned_ && ptr_) {
delete ptr_;
}
}
MjLogMessage::MjLogMessage() : ptr_(new mjLogMessage()) {
owned_ = true;
}
mjLogMessage* MjLogMessage::get() const {
return ptr_;
}
void MjLogMessage::set(mjLogMessage* ptr) {
ptr_ = ptr;
}
MjOption::MjOption(mjOption *ptr) : ptr_(ptr) {}
MjOption::~MjOption() {
if (owned_ && ptr_) {
@@ -8552,6 +8681,11 @@ int mj_setLengthRange_wrapper(const MjModel& m, const MjData& d, int index, cons
return result;
}
void mju_info_wrapper(int topic, const String& msg) {
CHECK_VAL(msg);
mju_info(topic, "%s", msg.as<const std::string>().data());
}
void mj_Euler_wrapper(const MjModel& m, MjData& d) {
mj_Euler(m.get(), d.get());
}
@@ -10364,6 +10498,12 @@ void mju_fill_wrapper(const val& res, mjtNum val) {
mju_fill(res_.data(), val, n);
}
MjLogConfig mju_getLogConfig_wrapper() {
MjLogConfig result;
*result.get() = mju_getLogConfig();
return result;
}
void mju_insertionSort_wrapper(const val& list) {
UNPACK_VALUE(mjtNum, list);
int n = list_.size();
@@ -10393,6 +10533,10 @@ int mju_mat2Rot_wrapper(const val& quat, const NumberArray& mat) {
return mju_mat2Rot(quat_.data(), mat_.data());
}
void mju_message_wrapper(const MjLogMessage& msg) {
mju_message(msg.get());
}
void mju_mulMatMat_wrapper(const val& res, const NumberArray& mat1, const NumberArray& mat2, int r1, int c1, int c2) {
UNPACK_VALUE(mjtNum, res);
UNPACK_ARRAY(mjtNum, mat1);
@@ -10642,6 +10786,10 @@ void mju_scl3_wrapper(const val& res, const NumberArray& vec, mjtNum scl) {
mju_scl3(res_.data(), vec_.data(), scl);
}
void mju_setLogConfig_wrapper(const MjLogConfig& config) {
mju_setLogConfig(*config.get());
}
void mju_sparse2dense_wrapper(const val& res, const NumberArray& mat, int nr, int nc, const NumberArray& rownnz, const NumberArray& rowadr, const NumberArray& colind) {
UNPACK_VALUE(mjtNum, res);
UNPACK_ARRAY(mjtNum, mat);
@@ -11258,6 +11406,17 @@ EMSCRIPTEN_BINDINGS(mujoco_bindings) {
.value("mjLIMITED_FALSE", mjLIMITED_FALSE)
.value("mjLIMITED_TRUE", mjLIMITED_TRUE)
.value("mjLIMITED_AUTO", mjLIMITED_AUTO);
enum_<mjtLogLevel>("mjtLogLevel")
.value("mjLOG_DEBUG", mjLOG_DEBUG)
.value("mjLOG_INFO", mjLOG_INFO)
.value("mjLOG_WARNING", mjLOG_WARNING)
.value("mjLOG_ERROR", mjLOG_ERROR);
enum_<mjtLogTopic>("mjtLogTopic")
.value("mjTOPIC_NONE", mjTOPIC_NONE)
.value("mjTOPIC_TIME_STP", mjTOPIC_TIME_STP)
.value("mjTOPIC_TIME_CMP", mjTOPIC_TIME_CMP)
.value("mjTOPIC_SLEEP", mjTOPIC_SLEEP)
.value("mjNTOPIC", mjNTOPIC);
enum_<mjtMark>("mjtMark")
.value("mjMARK_NONE", mjMARK_NONE)
.value("mjMARK_EDGE", mjMARK_EDGE)
@@ -11817,6 +11976,23 @@ EMSCRIPTEN_BINDINGS(mujoco_bindings) {
.property("tolrange", &MjLROpt::tolrange, &MjLROpt::set_tolrange, reference())
.property("useexisting", &MjLROpt::useexisting, &MjLROpt::set_useexisting, reference())
.property("uselimit", &MjLROpt::uselimit, &MjLROpt::set_uselimit, reference());
emscripten::class_<MjLogConfig>("MjLogConfig")
.constructor<>()
.function("copy", &MjLogConfig::copy, take_ownership())
.property("logfile", &MjLogConfig::logfile)
.property("logto_console", &MjLogConfig::logto_console, &MjLogConfig::set_logto_console, reference())
.property("logto_file", &MjLogConfig::logto_file, &MjLogConfig::set_logto_file, reference())
.property("topics", &MjLogConfig::topics, &MjLogConfig::set_topics, reference());
emscripten::class_<MjLogMessage>("MjLogMessage")
.constructor<>()
.property("body", &MjLogMessage::body, reference())
.property("file", &MjLogMessage::file, reference())
.property("func", &MjLogMessage::func, reference())
.property("level", &MjLogMessage::level, &MjLogMessage::set_level, reference())
.property("line", &MjLogMessage::line, &MjLogMessage::set_line, reference())
.property("subject", &MjLogMessage::subject)
.property("timestamp", &MjLogMessage::timestamp, &MjLogMessage::set_timestamp, reference())
.property("topic", &MjLogMessage::topic, &MjLogMessage::set_topic, reference());
emscripten::class_<MjModel>("MjModel")
// mj_loadXML is deprecated and will be removed in a future release
.class_function("mj_loadXML", emscripten::select_overload<std::unique_ptr<MjModel>(std::string)>(&mj_loadXML_wrapper_1))
@@ -13534,6 +13710,7 @@ EMSCRIPTEN_BINDINGS(mujoco_bindings) {
function("mju_eye", &mju_eye_wrapper);
function("mju_f2n", &mju_f2n_wrapper);
function("mju_fill", &mju_fill_wrapper);
function("mju_getLogConfig", &mju_getLogConfig_wrapper);
function("mju_insertionSort", &mju_insertionSort_wrapper);
function("mju_insertionSortInt", &mju_insertionSortInt_wrapper);
function("mju_isBad", &mju_isBad);
@@ -13541,6 +13718,7 @@ EMSCRIPTEN_BINDINGS(mujoco_bindings) {
function("mju_mat2Quat", &mju_mat2Quat_wrapper);
function("mju_mat2Rot", &mju_mat2Rot_wrapper);
function("mju_max", &mju_max);
function("mju_message", &mju_message_wrapper);
function("mju_min", &mju_min);
function("mju_mulMatMat", &mju_mulMatMat_wrapper);
function("mju_mulMatMatT", &mju_mulMatMatT_wrapper);
@@ -13577,6 +13755,7 @@ EMSCRIPTEN_BINDINGS(mujoco_bindings) {
function("mju_round", &mju_round);
function("mju_scl", &mju_scl_wrapper);
function("mju_scl3", &mju_scl3_wrapper);
function("mju_setLogConfig", &mju_setLogConfig_wrapper);
function("mju_sigmoid", &mju_sigmoid);
function("mju_sign", &mju_sign);
function("mju_sparse2dense", &mju_sparse2dense_wrapper);
@@ -13636,6 +13815,7 @@ EMSCRIPTEN_BINDINGS(mujoco_bindings) {
function("mj_saveModel", &mj_saveModel_wrapper);
function("mj_saveLastXML", &mj_saveLastXML_wrapper);
function("mj_setLengthRange", &mj_setLengthRange_wrapper);
function("mju_info", &mju_info_wrapper);
// mj_compile is bound using two overloads to handle the optional MjVFS argument,
// as using std::optional<MjVFS> caused memory errors due to missing copy/move constructors.
function("mj_compile", emscripten::select_overload<std::unique_ptr<MjModel>(const MjSpec&)>(&mj_compile_wrapper_1));
+18 -1
View File
@@ -15,7 +15,9 @@
"""Utility functions for code generation."""
import os
from typing import Union
from introspect import ast_nodes
from wasm.codegen.generators import constants
def write_to_file(filepath: str, content: str) -> None:
@@ -87,12 +89,27 @@ def get_inner_value_type(
return param.type.inner_type
def is_struct_value_type(
t: Union[ast_nodes.ValueType, ast_nodes.ArrayType, ast_nodes.PointerType],
) -> bool:
"""Checks if a type is a struct passed by value."""
if isinstance(t, ast_nodes.ValueType):
return (
t.name not in constants.PRIMITIVE_TYPES
and t.name != "void"
and not t.name.startswith("mjf")
)
return False
def should_be_wrapped(func: ast_nodes.FunctionDecl) -> bool:
"""Checks if a MuJoCo function needs a wrapper function."""
if get_pointer_return_inner_value_type(func):
return True
if is_struct_value_type(func.return_type):
return True
for param in func.parameters:
if get_inner_value_type(param):
if get_inner_value_type(param) or is_struct_value_type(param.type):
return True
return False
+4
View File
@@ -149,6 +149,7 @@ _SKIPPED_MEMORY_FUNCTIONS: tuple[str, ...] = (
"mju_error",
"mju_free",
"mju_malloc",
"mju_setLogHandler",
"mju_strncpy",
"mju_warning",
# go/keep-sorted end
@@ -230,6 +231,7 @@ MANUAL_WRAPPER_FUNCTIONS: tuple[str, ...] = (
"mj_saveModel",
"mj_setLengthRange",
"mju_error",
"mju_info",
# go/keep-sorted end
)
@@ -276,6 +278,8 @@ STRUCTS_TO_BIND: list[str] = list(
NO_DEFAULT_CONSTRUCTORS: tuple[str, ...] = (
# go/keep-sorted start
"mjContact",
"mjLogConfig",
"mjLogMessage",
"mjPreContact",
"mjSolverStat",
"mjStatistic",
+21
View File
@@ -154,6 +154,14 @@ def get_param_string(p: ast_nodes.FunctionParameterDecl) -> str:
return f"const NumberArray& {p.name}"
else:
return f"const val& {p.name}"
elif common.is_struct_value_type(p.type):
# Struct by value parameters
value_type = cast(ast_nodes.ValueType, p.type)
const_qualifier = "const " if value_type.is_const else "const "
return (
f"{const_qualifier}{common.capitalize(value_type.name)}&"
f" {p.name}"
)
else:
# This case should ideally not be reached if AST is well-formed
# and types are categorized by the helper booleans correctly.
@@ -183,6 +191,8 @@ def get_params_string_maybe_with_conversion(
native_params.append(f"{p.name}.get()")
elif param_is_primitive_value(p):
native_params.append(p.name)
elif common.is_struct_value_type(p.type):
native_params.append(f"*{p.name}.get()")
else:
raise TypeError(
f"Unhandled parameter type for conversion: {p.type} for param"
@@ -201,6 +211,14 @@ def get_compatible_return_code(func: ast_nodes.FunctionDecl) -> str:
return f"{c_call};"
if func.return_type.name in constants.PRIMITIVE_TYPES:
return f"return {c_call};"
if common.is_struct_value_type(func.return_type):
struct_name = func.return_type.name
w = common.wrapped_struct_name(struct_name)
builder = code_builder.CodeBuilder()
builder.line(f"{w} result;")
builder.line(f"*result.get() = {c_call};")
builder.line("return result;")
return builder.to_string()
if inner_type := common.get_pointer_return_inner_value_type(func):
if inner_type.name == "char":
@@ -230,6 +248,9 @@ def get_compatible_return_type(func: ast_nodes.FunctionDecl) -> str:
and func.return_type.name in constants.PRIMITIVE_TYPES
):
return f"{func.return_type.name}"
if common.is_struct_value_type(func.return_type):
return_type = cast(ast_nodes.ValueType, func.return_type)
return common.wrapped_struct_name(return_type.name)
return "val"
+30
View File
@@ -229,6 +229,36 @@ def _generate_field_data(
binding=_get_property_binding(f, w, setter=False, reference=True),
)
# Case 2.5: const char* without array_extent (C strings like body, func).
elif (
inner_type_name == "char"
and not is_dynamically_sized
):
builder = code_builder.CodeBuilder()
if f.type.inner_type.is_const:
with builder.function(f"std::string {f.name}() const"):
builder.line(
f'return ptr_->{f.name} ? std::string(ptr_->{f.name}) : "";'
)
else:
with builder.function(f"std::string {f.name}() const"):
builder.line(
f'return ptr_->{f.name} ? std::string(ptr_->{f.name}) : "";'
)
with builder.function(f"void set_{f.name}(const std::string& value)"):
with builder.block(f"if (ptr_->{f.name})"):
builder.line(
f"std::strncpy(ptr_->{f.name}, value.c_str(),"
f" sizeof(ptr_->{f.name}));"
)
return WrappedFieldData(
declaration=builder.to_string(),
typename=_get_field_struct_type(f, s),
binding=_get_property_binding(
f, w, setter=not f.type.inner_type.is_const, reference=True
),
)
# Case 3: Non-dynamically sized pointer fields to other structs.
elif (
not is_dynamically_sized
+6
View File
@@ -833,6 +833,11 @@ int mj_setLengthRange_wrapper(const MjModel& m, const MjData& d, int index, cons
return result;
}
void mju_info_wrapper(int topic, const String& msg) {
CHECK_VAL(msg);
mju_info(topic, "%s", msg.as<const std::string>().data());
}
// {{ WRAPPER_FUNCTIONS }}
EMSCRIPTEN_BINDINGS(mujoco_bindings) {
@@ -879,6 +884,7 @@ EMSCRIPTEN_BINDINGS(mujoco_bindings) {
function("mj_saveModel", &mj_saveModel_wrapper);
function("mj_saveLastXML", &mj_saveLastXML_wrapper);
function("mj_setLengthRange", &mj_setLengthRange_wrapper);
function("mju_info", &mju_info_wrapper);
// mj_compile is bound using two overloads to handle the optional MjVFS argument,
// as using std::optional<MjVFS> caused memory errors due to missing copy/move constructors.
function("mj_compile", emscripten::select_overload<std::unique_ptr<MjModel>(const MjSpec&)>(&mj_compile_wrapper_1));