Goby3 3.6.1
2026.09.15
Loading...
Searching...
No Matches
application.h
Go to the documentation of this file.
1// Copyright 2026:
2// GobySoft, LLC (2013-)
3// Community contributors (see AUTHORS file)
4// File authors:
5// Toby Schneider <toby@gobysoft.org>
6//
7//
8// This file is part of the Goby Underwater Autonomy Project Libraries
9// ("The Goby Libraries").
10//
11// The Goby Libraries are free software: you can redistribute them and/or modify
12// them under the terms of the GNU Lesser General Public License as published by
13// the Free Software Foundation, either version 2.1 of the License, or
14// (at your option) any later version.
15//
16// The Goby Libraries are distributed in the hope that they will be useful,
17// but WITHOUT ANY WARRANTY; without even the implied warranty of
18// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19// GNU Lesser General Public License for more details.
20//
21// You should have received a copy of the GNU Lesser General Public License
22// along with Goby. If not, see <http://www.gnu.org/licenses/>.
23
24#ifndef GOBY_MIDDLEWARE_LANGUAGES_PYTHON_APPLICATION_H
25#define GOBY_MIDDLEWARE_LANGUAGES_PYTHON_APPLICATION_H
26
27#include <chrono> // for microseconds
28#include <csignal> // for signal, raise, sig_atomic_t
29#include <cstdint> // for int64_t
30#include <cstdlib> // for exit
31#include <exception> // for exception_ptr
32#include <iostream> // for cout
33#include <memory> // for unique_ptr
34#include <string> // for string
35#include <vector> // for vector
36
37#include <google/protobuf/text_format.h>
38
39#include <pybind11/pybind11.h>
40#include <pybind11/stl.h>
41
42#include "goby/exception.h"
50#include "goby/time.h"
53
54namespace goby
55{
56namespace middleware
57{
64namespace python
65{
66namespace py = pybind11;
67
70
71namespace detail
72{
76inline void invoke_callback(const py::object& callback, const std::vector<char>& bytes)
77{
78 py::gil_scoped_acquire gil;
79 callback(py::bytes(bytes.data(), bytes.size()));
80}
81
86{
87 void (*quit_fn)(void*) = nullptr;
88 void* quit_ctx = nullptr;
89 volatile std::sig_atomic_t requested = 0;
90};
91
93{
94 static SignalState state;
95 return state;
96}
97
98inline void signal_handler(int signum)
99{
100 SignalState& state = signal_state();
101
102 if (state.quit_fn != nullptr && state.requested == 0)
103 {
104 state.requested = 1;
105 state.quit_fn(state.quit_ctx);
106 }
107 else
108 {
109 // a second signal means the application isn't unwinding (e.g. it is blocked waiting on
110 // data with no loop frequency set), so fall back to the default disposition
111 std::signal(signum, SIG_DFL);
112 std::raise(signum);
113 }
114}
115
122{
123 public:
124 SignalGuard(void (*quit_fn)(void*), void* quit_ctx)
125 {
126 SignalState& state = signal_state();
127 state.quit_fn = quit_fn;
128 state.quit_ctx = quit_ctx;
129 state.requested = 0;
130
131 previous_sigint_ = std::signal(SIGINT, &signal_handler);
132 previous_sigterm_ = std::signal(SIGTERM, &signal_handler);
133 }
134
136 {
137 if (previous_sigint_ != SIG_ERR)
138 std::signal(SIGINT, previous_sigint_);
139 if (previous_sigterm_ != SIG_ERR)
140 std::signal(SIGTERM, previous_sigterm_);
141
142 SignalState& state = signal_state();
143 state.quit_fn = nullptr;
144 state.quit_ctx = nullptr;
145 }
146
147 SignalGuard(const SignalGuard&) = delete;
149
150 private:
151 void (*previous_sigint_)(int){SIG_ERR};
152 void (*previous_sigterm_)(int){SIG_ERR};
153};
154
155} // namespace detail
156
161template <typename AppBase> class Application : public AppBase
162{
163 public:
164 using AppBase::AppBase;
165
166 using Hook = std::function<void()>;
168 using HealthHook = std::function<std::string(const std::string&)>;
169
171 {
172 loop_ = std::move(loop);
173 initialize_ = std::move(initialize);
174 finalize_ = std::move(finalize);
175 health_ = std::move(health);
176 }
177
178 protected:
179 void loop() override
180 {
181 if (loop_)
182 loop_();
183 }
184
191 {
192 AppBase::health(health);
193
194 if (!health_)
195 return;
196
197 std::string before;
198 health.SerializeToString(&before);
199
200 std::string after = health_(before);
201 if (after.empty())
202 return;
203
205 if (updated.ParseFromString(after))
206 health.Swap(&updated);
207 else
208 goby::glog.is_warn() && goby::glog << "Python health() returned a ThreadHealth that "
209 "could not be parsed; keeping the Goby one"
210 << std::endl;
211 }
212
213 // SingleThreadApplication inherits initialize()/finalize() from both Application and Thread.
214 // Only one set is ever invoked for a given application type, but guard anyway so that a
215 // Python override cannot be called twice.
216 void initialize() override
217 {
218 if (initialize_ && !initialize_called_)
219 {
220 initialize_called_ = true;
221 initialize_();
222 }
223 }
224
225 void finalize() override
226 {
227 if (finalize_ && !finalize_called_)
228 {
229 finalize_called_ = true;
230 finalize_();
231 }
232 }
233
234 private:
235 Hook loop_;
236 Hook initialize_;
237 Hook finalize_;
238 HealthHook health_;
239 bool initialize_called_{false};
240 bool finalize_called_{false};
241};
242
246template <typename App> class ApplicationWrapper
247{
248 public:
255 ApplicationWrapper(double loop_frequency_hertz = 0)
256 {
257 if (!App::app_cfg_)
258 throw(goby::Exception(
259 "Goby configuration has not been read. Applications must be started with "
260 "goby.run(), which parses the command line before constructing the application."));
261
262 app_ptr_.reset(new App(loop_frequency_hertz));
263 app_ptr_->set_hooks([this]() { this->loop(); }, [this]() { this->initialize(); },
264 [this]() { this->finalize(); },
265 [this](const std::string& serialized) -> std::string
266 {
267 // health() is called from the Goby event loop, which runs with
268 // the GIL released
269 py::gil_scoped_acquire gil;
270 return this->health(py::bytes(serialized));
271 });
272 }
273
274 virtual ~ApplicationWrapper() = default;
275
278
280 virtual void loop()
281 {
282 // mirrors the diagnostic from Thread::loop()
283 throw(goby::Exception("loop() must be overridden when loop_frequency is non-zero"));
284 }
285
287 virtual void initialize() {}
288
290 virtual void finalize() {}
291
296 virtual std::string health(py::bytes serialized) { return std::string(serialized); }
297
306 static py::bytes configure(const std::vector<std::string>& argv)
307 {
308 using ConfigType = typename App::ConfigType;
309
310 std::vector<char*> argv_c;
311 argv_c.reserve(argv.size() + 1);
312 for (const auto& arg : argv) argv_c.push_back(const_cast<char*>(arg.c_str()));
313 argv_c.push_back(nullptr);
314
315 ProtobufConfigurator<ConfigType> protobuf_cfgtor(static_cast<int>(argv.size()),
316 argv_c.data());
317 // ProtobufConfigurator narrows the access of these overrides, so go through the
318 // interface, as goby::run does
319 const ConfiguratorInterface<ConfigType>& cfgtor = protobuf_cfgtor;
320
321 try
322 {
323 cfgtor.validate();
324 }
326 {
327 cfgtor.handle_config_error(e);
328 throw;
329 }
330
331 // simply print the configuration and exit
332 if (cfgtor.app_configuration().debug_cfg())
333 {
334 std::cout << cfgtor.str() << std::endl;
335 std::exit(EXIT_SUCCESS);
336 }
337
338 App::app_cfg_.reset(new ConfigType(cfgtor.cfg()));
339 App::app3_base_configuration_.reset(
341
342 goby::middleware::detail::configure_simulation_time(*App::app3_base_configuration_);
343
344 return set_configuration(cfgtor.cfg());
345 }
346
353 static py::bytes configure_from_text(const std::string& config_text)
354 {
355 typename App::ConfigType cfg;
356
357 google::protobuf::TextFormat::Parser parser;
358 goby::util::FlexOStreamErrorCollector error_collector(config_text);
359 parser.RecordErrorsTo(&error_collector);
360 parser.AllowPartialMessage(false);
361
362 if (!parser.ParseFromString(config_text, &cfg))
364 "Failed to parse the configuration as Protobuf TextFormat"));
365
366 return set_configuration(cfg);
367 }
368
372 static py::bytes configure_from_serialized(const std::string& config_bytes)
373 {
374 typename App::ConfigType cfg;
375
376 if (!cfg.ParseFromString(config_bytes))
377 throw(middleware::ConfigException("Failed to parse the serialized configuration"));
378
379 return set_configuration(cfg);
380 }
381
387 int run()
388 {
389 detail::SignalGuard signal_guard(&ApplicationWrapper::signal_quit, this);
390 py::gil_scoped_release release_gil;
391 return app_ptr_->__run();
392 }
393
395 void quit(int return_value = 0) { app_ptr_->quit(return_value); }
396
398 std::string app_name() { return app_ptr_->app_name(); }
399
401 py::bytes cfg_serialized()
402 {
403 std::string serialized;
404 app_ptr_->app_cfg().SerializeToString(&serialized);
405 return py::bytes(serialized);
406 }
407
408 void set_loop_frequency_hertz(double loop_frequency_hertz)
409 {
410 app_ptr_->set_loop_frequency_hertz(loop_frequency_hertz);
411 }
412
414 void publish(int layer, const std::string& type_name, int scheme, const std::string& group,
415 const std::string& data)
416 {
417 app_ptr_->publish(Identifier{static_cast<PubSubLayer>(layer), type_name, scheme, group},
418 data);
419 }
420
422 void subscribe(int layer, const std::string& type_name, int scheme, const std::string& group,
423 py::object callback)
424 {
425 app_ptr_->subscribe(Identifier{static_cast<PubSubLayer>(layer), type_name, scheme, group},
426 std::move(callback));
427 }
428
429 private:
432 static py::bytes set_configuration(const typename App::ConfigType& cfg)
433 {
434 ConfigReader::check_required_cfg(cfg, cfg.app().binary());
435
436 App::app_cfg_.reset(new typename App::ConfigType(cfg));
437 App::app3_base_configuration_.reset(new goby::middleware::protobuf::AppConfig(cfg.app()));
438
439 goby::middleware::detail::configure_simulation_time(*App::app3_base_configuration_);
440
441 std::string serialized;
442 cfg.SerializeToString(&serialized);
443 return py::bytes(serialized);
444 }
445
446 static void signal_quit(void* self)
447 {
448 static_cast<ApplicationWrapper*>(self)->app_ptr_->quit();
449 }
450
451 private:
452 std::unique_ptr<App> app_ptr_;
453};
454
456template <typename App> class ApplicationWrapperTrampoline : public ApplicationWrapper<App>
457{
458 public:
459 using ApplicationWrapper<App>::ApplicationWrapper;
460
461 void loop() override { PYBIND11_OVERRIDE(void, ApplicationWrapper<App>, loop, ); }
462 void initialize() override { PYBIND11_OVERRIDE(void, ApplicationWrapper<App>, initialize, ); }
463 void finalize() override { PYBIND11_OVERRIDE(void, ApplicationWrapper<App>, finalize, ); }
464 std::string health(py::bytes serialized) override
465 {
466 PYBIND11_OVERRIDE_NAME(std::string, ApplicationWrapper<App>, "_goby_health", health,
467 serialized);
468 }
469};
470
471namespace detail
472{
481inline void glog_write(int verbosity, const std::string& group, const std::string& text)
482{
483 py::gil_scoped_release release_gil;
484
485 if (!goby::glog.is(static_cast<goby::util::logger::Verbosity>(verbosity)))
486 return;
487
488 if (!group.empty())
490
491 goby::glog << text << std::endl;
492}
493
499inline bool glog_is(int verbosity)
500{
501 auto level = static_cast<goby::util::logger::Verbosity>(verbosity);
502 if (level == goby::util::logger::DIE)
503 return true;
504
505 const auto* buf = dynamic_cast<const goby::util::FlexOStreamBuf*>(goby::glog.rdbuf());
506 return buf != nullptr && buf->highest_verbosity() >= level;
507}
508
509} // namespace detail
510
512template <typename App>
513inline void define_python_module(py::module_& m, const std::string& app_name)
514{
515 // ConfigException is raised as goby.ConfigError so that all generated modules share one
516 // exception type, which goby.run() catches
517 py::register_exception_translator(
518 [](std::exception_ptr p)
519 {
520 try
521 {
522 if (p)
523 std::rethrow_exception(p);
524 }
525 catch (const middleware::ConfigException& e)
526 {
527 py::object goby_module = py::module_::import("goby");
528 PyErr_SetString(goby_module.attr("ConfigError").ptr(), e.what());
529 }
530 });
531
532 py::class_<ApplicationWrapper<App>, ApplicationWrapperTrampoline<App>>(m, "_ApplicationBase")
533 .def(py::init<double>(), py::arg("loop_frequency_hertz") = 0)
534 .def("loop", &ApplicationWrapper<App>::loop)
535 .def("initialize", &ApplicationWrapper<App>::initialize)
536 .def("finalize", &ApplicationWrapper<App>::finalize)
537 .def("quit", &ApplicationWrapper<App>::quit, py::arg("return_value") = 0)
538 .def("app_name", &ApplicationWrapper<App>::app_name)
539 .def("_run", &ApplicationWrapper<App>::run)
540 .def("_publish", &ApplicationWrapper<App>::publish, py::arg("layer"), py::arg("type_name"),
541 py::arg("scheme"), py::arg("group"), py::arg("data"))
542 .def("_subscribe", &ApplicationWrapper<App>::subscribe, py::arg("layer"),
543 py::arg("type_name"), py::arg("scheme"), py::arg("group"), py::arg("callback"))
544 .def("_cfg_serialized", &ApplicationWrapper<App>::cfg_serialized)
545 .def("_set_loop_frequency_hertz", &ApplicationWrapper<App>::set_loop_frequency_hertz)
546 .def_static("_configure", &ApplicationWrapper<App>::configure, py::arg("argv"))
547 .def_static("_configure_from_text", &ApplicationWrapper<App>::configure_from_text,
548 py::arg("config_text"))
549 .def_static("_configure_from_serialized",
551 .def("_goby_health", &ApplicationWrapper<App>::health, py::arg("serialized"));
552
553 // Simulation time lives in process-wide statics rather than on the application, and is set
554 // when the configuration is read. goby.time reads it from here so that a Python loop is
555 // scheduled on the same warped clock the C++ loop runs on.
556 m.def("_sim_time",
557 []() {
558 return py::make_tuple(
561 static_cast<std::int64_t>(
563 std::chrono::microseconds(1)));
564 });
565
566 m.def("_glog_write", &detail::glog_write, py::arg("verbosity"), py::arg("group"),
567 py::arg("text"));
568 m.def("_glog_is", &detail::glog_is, py::arg("verbosity"));
569 m.def("_glog_add_group",
570 [](const std::string& name, const std::string& description)
571 { goby::glog.add_group(name, goby::util::Colors::nocolor, description); });
572
573 m.attr("_GOBY_APPLICATION_NAME") = app_name;
574}
575
576} // namespace python
577} // namespace middleware
578} // namespace goby
579
580// Macros for use by the autogenerated code to define the C++ side of the pub/sub setup.
581//
582// GROUP_KEY is the interface.yml expression for GROUP as a string, which is the only name
583// Python has for the group; a group's runtime name need not be its C++ variable name.
584#define GOBY_PYTHON_IF_PUBLICATION(SCHEME, LAYER_ENUM, LAYER_FUNCTION, GROUP, GROUP_KEY, TYPE) \
585 if (id == goby::middleware::python::Identifier{ \
586 goby::middleware::python::PubSubLayer::LAYER_ENUM, TYPE::descriptor()->name(), \
587 goby::middleware::MarshallingScheme::SCHEME, GROUP_KEY}) \
588 { \
589 decltype(data.end()) actual_end; \
590 auto msg = goby::middleware::SerializerParserHelper< \
591 TYPE, goby::middleware::MarshallingScheme::SCHEME>::parse(data.begin(), data.end(), \
592 actual_end); \
593 LAYER_FUNCTION().publish<GROUP>(msg); \
594 return; \
595 }
596
597#define GOBY_PYTHON_IF_SUBSCRIPTION(SCHEME, LAYER_ENUM, LAYER_FUNCTION, GROUP, GROUP_KEY, TYPE) \
598 if (id == goby::middleware::python::Identifier{ \
599 goby::middleware::python::PubSubLayer::LAYER_ENUM, TYPE::descriptor()->name(), \
600 goby::middleware::MarshallingScheme::SCHEME, GROUP_KEY}) \
601 { \
602 LAYER_FUNCTION().subscribe<GROUP>( \
603 [callback](const TYPE& pb) \
604 { \
605 std::vector<char> bytes = goby::middleware::SerializerParserHelper< \
606 TYPE, goby::middleware::MarshallingScheme::SCHEME>::serialize(pb); \
607 goby::middleware::python::detail::invoke_callback(callback, bytes); \
608 }); \
609 return; \
610 }
611
612#define GOBY_PYTHON_FAIL(PUBLISH_OR_SUBSCRIBE) \
613 goby::glog.is_die() && \
614 goby::glog << PUBLISH_OR_SUBSCRIBE " not defined for these parameters: [" << id \
615 << "]. Please include in interface.yml and re-generate to include them." \
616 << std::endl;
617
618// used to stringify application name
619#define GOBY_PYTHON_QUOTE(name) #name
620#define GOBY_PYTHON_DEFINE_MODULE(APPLICATION_NAME, MODULE_NAME) \
621 PYBIND11_MODULE(MODULE_NAME, goby_python_module) \
622 { \
623 goby::middleware::python::define_python_module<APPLICATION_NAME>( \
624 goby_python_module, GOBY_PYTHON_QUOTE(APPLICATION_NAME)); \
625 }
626
627#endif
simple exception class for goby applications
Definition exception.h:35
indicates a problem with the runtime command line or .cfg file configuration (or –help was given)
static void check_required_cfg(const google::protobuf::Message &message, const std::string &binary)
Checks that all required fields are set (either via the command line or the configuration file) in th...
Defines the interface to a "configurator", a class that can read command line parameters (argc,...
virtual void validate() const
Override to validate the configuration.
virtual void handle_config_error(middleware::ConfigException &e) const
Override to customize how ConfigException errors are handled.
virtual const protobuf::AppConfig & app_configuration() const
Subset of the configuration used to configure the Application itself.
const Config & cfg() const
The configuration object produced from the command line parameters.
virtual std::string str() const =0
Override to output the configuration object as a string.
Implementation of ConfiguratorInterface for Google Protocol buffers.
Lets Python subclasses override the Goby virtual methods.
void loop() override
Override in Python to do work at loop_frequency Hz.
void initialize() override
Override in Python for work that can't be done in the constructor.
void finalize() override
Override in Python for cleanup just before the application exits.
std::string health(py::bytes serialized) override
Override in Python to answer goby_coroner's health request.
The class exposed to Python; user application classes subclass this.
ApplicationWrapper & operator=(const ApplicationWrapper &)=delete
std::string app_name()
The application name, as set by –app_name or derived from argv[0].
static py::bytes configure_from_text(const std::string &config_text)
Reads the application configuration from a Protobuf TextFormat string.
ApplicationWrapper(const ApplicationWrapper &)=delete
py::bytes cfg_serialized()
The configuration the application was started with, serialized as Protobuf.
static py::bytes configure_from_serialized(const std::string &config_bytes)
Reads the application configuration from a serialized Protobuf message.
virtual void loop()
Override in Python to do work at loop_frequency Hz.
virtual void finalize()
Override in Python for cleanup just before the application exits.
void publish(int layer, const std::string &type_name, int scheme, const std::string &group, const std::string &data)
Publishes already-marshalled bytes; dispatched by the generated glue code.
static py::bytes configure(const std::vector< std::string > &argv)
Reads the command line into the application configuration.
int run()
Runs the Goby event loop until quit() is called or the application is terminated.
virtual void initialize()
Override in Python for work that can't be done in the constructor.
void set_loop_frequency_hertz(double loop_frequency_hertz)
ApplicationWrapper(double loop_frequency_hertz=0)
Constructs the underlying Goby application.
void subscribe(int layer, const std::string &type_name, int scheme, const std::string &group, py::object callback)
Subscribes a Python callable; dispatched by the generated glue code.
void quit(int return_value=0)
Requests a clean exit.
virtual std::string health(py::bytes serialized)
Override in Python to answer goby_coroner's health request.
Base class for the generated C++ glue class, forwarding the Goby virtual methods to Python.
void set_hooks(Hook loop, Hook initialize, Hook finalize, HealthHook health)
std::function< std::string(const std::string &)> HealthHook
Takes the serialized ThreadHealth Goby filled in and returns the one Python wrote.
void health(goby::middleware::protobuf::ThreadHealth &health) override
Hands the report goby_coroner asked for to Python, as serialized Protobuf.
Installs the quit-on-signal handlers for the duration of a run() call.
SignalGuard(void(*quit_fn)(void *), void *quit_ctx)
SignalGuard(const SignalGuard &)=delete
SignalGuard & operator=(const SignalGuard &)=delete
logger::Verbosity highest_verbosity() const
void add_group(const std::string &name, Colors::Color color=Colors::nocolor, const std::string &description="")
Add another group to the logger. A group provides related manipulator for categorizing log messages.
goby::util::logger::GroupSetter group(std::string n)
detail namespace with internal helper functions
Definition json.hpp:263
void configure_simulation_time(const protobuf::AppConfig &app_cfg)
Applies the simulation time settings from an application configuration to goby::time::SimulatorSettin...
PubSubLayer
The publish/subscribe layer used for a given publication or subscription.
Definition interface.h:52
void glog_write(int verbosity, const std::string &group, const std::string &text)
Writes one already-formatted line to goby::glog at the given verbosity.
bool glog_is(int verbosity)
Whether anything is listening at this verbosity.
void invoke_callback(const py::object &callback, const std::vector< char > &bytes)
Hands marshalled bytes to a Python callable, acquiring the GIL first.
Definition application.h:76
void define_python_module(py::module_ &m, const std::string &app_name)
Defines the contents of the generated extension module.
constexpr int scheme()
Placeholder to provide an interface for the scheme() function family.
Definition cstr.h:65
The global namespace for the Goby project.
util::FlexOstream glog
Access the Goby logger through this object.
Fully identifies a publication or subscription crossing the language boundary.
Definition interface.h:76
State used to turn SIGINT/SIGTERM into a clean Application::quit()
Definition application.h:86
static bool using_sim_time
Enables simulation time if true (if false, none of the remaining parameters are used)
Definition simulation.h:38
static std::chrono::system_clock::time_point reference_time
Reference time when calculating SystemClock::now(). If this is unset, the default is 1 January of the...
Definition simulation.h:42
static int warp_factor
Warp factor to speed up (or slow time) the time values returned by SteadyClock::now() and SystemClock...
Definition simulation.h:40