diff --git a/src/user/user_objects.cc b/src/user/user_objects.cc index 4c5fc228..7945ec11 100644 --- a/src/user/user_objects.cc +++ b/src/user/user_objects.cc @@ -7157,6 +7157,11 @@ void mjCPlugin::Compile(void) { } } + // if there are no attributes, add a null terminator + if (plugin->nattribute == 0) { + flattened_attributes.push_back('\0'); + } + // anything left in xml_attributes at this stage is not a valid attribute if (!config_attribs_copy.empty()) { std::string error = diff --git a/test/engine/engine_plugin_test.cc b/test/engine/engine_plugin_test.cc index 7a01ac01..5463f1e2 100644 --- a/test/engine/engine_plugin_test.cc +++ b/test/engine/engine_plugin_test.cc @@ -39,7 +39,7 @@ using ::testing::NotNull; constexpr int kNumTruePlugins = 10; constexpr int kNumFakePlugins = 30; -constexpr int kNumTestPlugins = 3; +constexpr int kNumTestPlugins = 4; class BaseTestPlugin { public: @@ -340,6 +340,42 @@ int RegisterPassivePlugin() { return mjp_registerPlugin(&plugin); } +int RegisterNoAttributePlugin() { + mjpPlugin plugin; + mjp_defaultPlugin(&plugin); + + plugin.name = "mujoco.test.noattribute"; + + plugin.nattribute = 0; + plugin.attributes = nullptr; + + plugin.capabilityflags |= mjPLUGIN_PASSIVE; + + plugin.nstate = +[](const mjModel* m, int instance) { return 0; }; + + plugin.init = +[](const mjModel* m, mjData* d, int instance) { + auto* passive = new TestPassive(m, d, instance); + d->plugin_data[instance] = reinterpret_cast(passive); + return 0; + }; + plugin.destroy = +[](mjData* d, int instance) { + delete reinterpret_cast(d->plugin_data[instance]); + d->plugin_data[instance] = 0; + }; + + plugin.reset = +[](const mjModel* m, mjtNum* plugin_state, void* plugin_data, + int instance) { + auto passive = reinterpret_cast(plugin_data); + passive->Reset(); + }; + plugin.compute = +[](const mjModel* m, mjData* d, int instance, int type) { + auto passive = reinterpret_cast(d->plugin_data[instance]); + passive->Compute(); + }; + + return mjp_registerPlugin(&plugin); +} + class EnginePluginTest : public PluginTest { public: // register all plugins @@ -356,6 +392,7 @@ class EnginePluginTest : public PluginTest { RegisterActuatorPlugin(); RegisterPassivePlugin(); + RegisterNoAttributePlugin(); } }; @@ -374,6 +411,9 @@ constexpr char xml[] = R"( + + + @@ -430,6 +470,22 @@ TEST_F(PluginTest, FirstPartyPlugins) { EXPECT_THAT(mjp_pluginCount(), kNumTruePlugins); } +TEST_F(EnginePluginTest, NoAttributePlugin) { + static constexpr char xml[] = R"( + + + + + + + + )"; + std::array error; + mjModel* m = LoadModelFromString(xml, error.data(), error.size()); + EXPECT_THAT(m, testing::NotNull()) << error.data(); + mj_deleteModel(m); +} + TEST_F(EnginePluginTest, MultiplePluginTableBlocks) { EXPECT_EQ(mjp_pluginCount(), kNumTruePlugins + kNumFakePlugins + kNumTestPlugins); @@ -529,7 +585,7 @@ TEST_F(EnginePluginTest, SensorPlugin) { EXPECT_EQ(TestSensor::InitCount(), expected_init_count); EXPECT_EQ(TestSensor::DestroyCount(), expected_destroy_count); - EXPECT_EQ(m->nplugin, 6); + EXPECT_EQ(m->nplugin, 7); EXPECT_EQ(mj_name2id(m, mjOBJ_PLUGIN, "twosensors"), 0); EXPECT_EQ(mj_name2id(m, mjOBJ_PLUGIN, "threesensors"), 1); @@ -547,7 +603,7 @@ TEST_F(EnginePluginTest, SensorPlugin) { m->plugin_stateadr[1]), testing::ElementsAreArray({3*(i+1), 6*j, 3*j})); EXPECT_THAT(*reinterpret_cast(d->plugin_state + - m->plugin_stateadr[4]), + m->plugin_stateadr[5]), testing::ElementsAreArray({5*(i+1), 10*j, 5*j})); EXPECT_THAT(*reinterpret_cast(d->sensordata), testing::ElementsAreArray({ i+1, 2*j, j, @@ -587,7 +643,7 @@ TEST_F(EnginePluginTest, ActuatorPlugin) { EXPECT_EQ(TestActuator::InitCount(), expected_init_count); EXPECT_EQ(TestActuator::DestroyCount(), expected_destroy_count); - EXPECT_EQ(m->nplugin, 6); + EXPECT_EQ(m->nplugin, 7); EXPECT_EQ(mj_name2id(m, mjOBJ_PLUGIN, "actuator2"), 2); mjData* d = mj_makeData(m);