In-Process Plugin or External Controller? Running Robot Logic in Gazebo Sim
Decide whether to load a C++ system plugin into Gazebo Sim or drive your robot from an external process over Gazebo transport, with headless CI in mind.
05 Jun 2026, 15:18 UTC

The decision point: plugin inside, or controller outside?
You have a robot model in Gazebo Sim and need to read sensor data, then send wheel or joint commands. The first architectural choice is where that logic lives: inside the simulator as an SDF system plugin, or in a separate process that talks to gz-sim over Gazebo transport. The wrong choice makes headless batch runs harder to debug and can turn a controller bug into a full simulator crash.
This post compares the two approaches for headless CI and parameter sweeps, then gives a verification routine you can run before committing to either path.
Option 1: an SDF system plugin inside gz-sim
Gazebo Sim loads worlds described in SDF (Simulation Description Format), an XML format for models, sensors, and plugins. A system plugin is compiled C++ code loaded into the gz-sim process through an SDF <plugin> tag. It can implement callbacks such as PreUpdate, Update, and PostUpdate, which run on the simulator's timestep and have direct access to model state and joint commands.
<model name="my_robot">
...
<plugin filename="my_controller.so" name="my_controller::MySystem">
<robot>my_robot</robot>
</plugin>
</model>
The advantage is tight coupling: sensing and actuation happen in the same address space and timestep, with no transport serialization. The risk is shared fate. Because the plugin runs inside gz-sim, a segmentation fault or infinite loop can stall or corrupt the entire simulation. You also must rebuild and reload the plugin whenever the controller changes.
Option 2: an external controller over Gazebo transport
Gazebo transport is the simulator's message bus. Built-in sensor plugins publish odometry, lidar, camera, and IMU data to named topics. An external program can subscribe to those topics, run its own control loop, and publish joint commands back to gz-sim. The simulator binary stays unchanged while you iterate the controller.
Process separation means a controller crash does not kill gz-sim, and you can write the controller in any language with transport bindings. The trade-off is that transport is not a real-time guarantee. Under load, sensor topics and command messages can lag, so closed-loop behavior should be validated with real-time factor and topic latency rather than assumed to match the simulation timestep.
Worked example: verify topics before you choose
Run these commands in a normal user terminal on the machine where gz-sim is installed. You do not need root unless you are installing system packages.
gz sim --version
gz sim -s /path/to/your_world.sdf
gz sim --version prints the installed gz-sim series. Note it: plugin code and SDF tags are not portable across major versions. gz sim -s starts headless, without the graphical user interface, which is appropriate for repeated experiments and CI. Expect the terminal to print simulation statistics including real-time factor; no GUI window should appear. If the command aborts, confirm the installed package includes the gz-sim libraries and development headers for your version.
While the simulation runs, list transport topics in a second terminal:
gz topic -l
gz topic -e /model/my_robot/odometry
Replace /model/my_robot/odometry with a topic from your world. Model-scoped topics typically include the model name, so in multi-robot worlds use unique model names or namespaced topics to avoid collisions. gz topic -e should print messages if the sensor plugin is publishing. If the list is empty or the echo is silent, the sensor may not be enabled in SDF or the world may not have started.
Trade-offs and limits
| Factor | In-process SDF plugin | External transport controller |
|---|---|---|
| Coupling to sim timestep | Direct; runs in PreUpdate/Update/PostUpdate | Indirect; subject to transport latency |
| Crash isolation | None; plugin fault can stall gz-sim | High; controller crash leaves sim running |
| Iteration speed | Rebuild and reload plugin | Restart controller only |
| Language options | C++ (matching gz-sim ABI) | Any language with transport bindings |
| Headless suitability | Good, but harder to debug without GUI | Good; logs and debugging are separate |
Also remember: gz-sim APIs, base classes, and SDF system tags differ across Gazebo Sim major versions. Code written for one release may not compile or load in another. Do not mix Gazebo Classic (gazebo/gzserver) tutorials with Gazebo Sim (gz sim/gz-sim); their symbols, CLI flags, and SDF versions are not interchangeable.
Practical check and closing
Start with an external controller over transport unless you have a measured reason to move logic inside the simulator. Run gz sim -s and watch the real-time factor while enabling and disabling lidar or camera sensors; a significant drop means sensor overhead is affecting your batch runs. Use gz topic -l and gz topic -e to confirm that the topics your controller needs actually exist and carry messages.
If you later decide an in-process plugin is justified, compile a minimal plugin against your installed gz-sim development headers, load it through an SDF <plugin> tag, and confirm the simulation starts without aborting. To revert, remove the <plugin> tag from SDF and delete the built shared object; no simulator state is permanently changed.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.