...one of the most highly
regarded and expertly designed C++ library projects in the
world.
— Herb Sutter and Andrei
Alexandrescu, C++
Coding Standards
To extend the library, the header extend
is provided.
It only provides the explicit style for custom properties, but no implicit style.
What this means is, that a custom initializer can be implemented, a reference
which can be passed to one of the launching functions. If a class inherits
boost::process::extend::handler
it will be regarded as an initializer and thus directly put into the sequence
the executor gets passed.
The executor calls different handlers of the initializers during the process launch. The basic structure consists of three functions, as given below:
Additionally posix provides three more handlers, listed below:
For more information see the reference of posix_executor
.
The simplest extension just takes a single handler, which can be done in a functional style. So let's start with a simple hello-world example, while we use a C++14 generic lambda.
using namespace boost::process; namespace ex = bp::extend;child
c("foo",ex::on_success
=[](auto & exec) {std::cout << "hello world" << std::endl;});
Considering that lambdas can also capture values, data can easily be shared between handlers.
To see which members the executor has, refer to windows_executor
and posix_executor
.
Note | |
---|---|
Combined with |
Caution | |
---|---|
The posix handler symbols are not defined on windows. |
Since the previous example is in a functional style, it is not very reusable.
To solve that problem, the handler
has an alias in the boost::process::extend
namespace, to be inherited. So let's implement the hello world example in
a class.
struct hello_world :handler
{ template<typename Executor> voidex::on_success
(Executor & exec) const { std::cout << "hello world" << std::endl; } }; //in our functionchild
c("foo", hello_world());
Note | |
---|---|
The implementation is done via overloading, not overriding. |
Every handler not implemented defaults to handler
,
where an empty handler is defined for each event.
Since boost.process
provides an interface for boost.asio,
this functionality is also available for extensions. If the class needs the
boost::asio::io_context
for some reason, the following code will do that.
struct async_foo :handler
,ex::require_io_context
{ template<typename Executor> void on_setup(Executor & exec) { boost::asio::io_context & ios =ex::get_io_context
(exec.seq); //gives us a reference and a compiler error if not present. //do something with ios } };
Note | |
---|---|
Inheriting |
Additionally the handler can provide a function that is invoked when the
child process exits. This is done through ex::async_handler
.
Note | |
---|---|
|
struct async_bar : __handler, ex::async_handler
{
template<typename Executor>
std::function<void(int, const std::error_code&)> on_exit_handler(Executor & exec)
{
auto handler_ = this->handler;
return [handler_](int exit_code, const std::error_code & ec)
{
std::cout << "hello world, I exited with " << exit_code << std::endl;
};
}
};
Caution | |
---|---|
|
Caution | |
---|---|
|
If an error occurs in the initializers it shall be told to the executor and
not handled directly. This is because the behaviour can be changed through
arguments passed to the launching function. Hence the executor has the function
set_error
, which takes an
std::error_code
and a string. Depending on the configuration of the executor, this may either
throw, set an internal error_code
,
or do nothing.
So let's take a simple example, where we set a randomly chosen error_code
.
auto set_error = [](auto & exec)
{
std::error_code ec{42, std::system_category()};
exec.set_error(ec, "a fake error");
};
child
c("foo", on_setup=set_error);
Since we do not specify the error-handling mode in this example, this will
throw process_error
.
Now that we have a custom initializer, let's consider how we can handle differences
between different executors. The distinction is between posix and windows
and char
and wchar_t
on windows. One solution is to use the BOOST_WINDOWS_API
and BOOST_POSIX_API macros, which are automatically available as
soon as any process-header is included.
Another variant are the type aliases ex::posix_executor
and ex::windows_executor
,
where the executor, not on the current system is a forward-declaration. This
works fine, because the function will never get invoked. So let's implement
another example, which prints the executable name ex::on_success
.
struct hello_exe :handler
{ template<typename Sequence> voidex::on_success
(ex::posix_executor
<Sequence> & exec) { std::cout << "posix-exe: " << exec.exe << std::endl; } template<typename Sequence> voidex::on_success
(ex::windows_executor
<char, Sequence> & exec) { //note: exe might be a nullptr on windows. if (exec.exe != nullptr) std::cout << "windows-exe: " << exec.exe << std::endl; else std::cout << "windows didn't use exe" << std::endl; } template<typename Sequence> voidex::on_success
(ex::windows_executor
<wchar_t, Sequence> & exec) { //note: exe might be a nullptr on windows. if (exec.exe != nullptr) std::wcout << L"windows-exe: " << exec.exe << std::endl; else std::cout << "windows didn't use exe" << std::endl; } };
So given our example, the definitions with the non-native executor are still a template so that they will not be evaluated if not used. Hence this provides a way to implement system-specific code without using the preprocessor.
Note | |
---|---|
If you only write a partial implementation, e.g. only for |
.