Start
Your first device
In this tutorial, you write two devices and run them in two terminals. The sampler sends the text Hello once per second, and the sink receives it. The code is the code of example 1-1.
You need an installed FairMQ (see Install), CMake 3.15 or later, and a C++17 compiler.
The topology
Section titled “The topology”| Device | Channel | Type | Method | Address |
|---|---|---|---|---|
| sampler | data |
push |
bind |
tcp://*:5555 |
| sink | data |
pull |
connect |
tcp://localhost:5555 |
The sampler binds its channel to the port 5555 on all network interfaces. The sink connects to this port on the same host. Both devices use the default transport zeromq.
Create the project
Section titled “Create the project”Make a folder with three files:
Directoryfirst-device/
- CMakeLists.txt
- sampler.cxx
- sink.cxx
Write the sampler
Section titled “Write the sampler”Write this code into sampler.cxx:
#include <fairmq/Device.h>#include <fairmq/runDevice.h>
#include <string>
namespace bpo = boost::program_options;
struct Sampler : fair::mq::Device{ void InitTask() override { // Get the fText and fMaxIterations values from the command line options (via fConfig) fText = fConfig->GetProperty<std::string>("text"); fMaxIterations = fConfig->GetProperty<uint64_t>("max-iterations"); }
bool ConditionalRun() override { // create a copy of the data with new(), that will be deleted after the transfer is complete std::string* text = new std::string(fText);
// create message object with a pointer to the data buffer, its size, // custom deletion function (called when transfer is done), // and pointer to the object managing the data buffer fair::mq::MessagePtr msg(NewMessage( const_cast<char*>(text->c_str()), text->length(), [](void* /*data*/, void* object) { delete static_cast<std::string*>(object); }, text));
LOG(info) << "Sending \"" << fText << "\"";
// in case of error or transfer interruption, return false to go to the Ready state // successfull transfer will return number of bytes transfered (can be 0 if sending an empty message). if (Send(msg, "data") < 0) { return false; } else if (fMaxIterations > 0 && ++fNumIterations >= fMaxIterations) { LOG(info) << "Configured maximum number of iterations reached. Leaving RUNNING state."; return false; }
return true; }
private: std::string fText; uint64_t fMaxIterations = 0; uint64_t fNumIterations = 0;};
void addCustomOptions(bpo::options_description& options){ options.add_options() ("text", bpo::value<std::string>()->default_value("Hello"), "Text to send out") ("max-iterations", bpo::value<uint64_t>()->default_value(0), "Maximum number of iterations of Run/ConditionalRun/OnData (0 - infinite)");}
std::unique_ptr<fair::mq::Device> getDevice(fair::mq::ProgOptions& /*config*/){ return std::make_unique<Sampler>();}The parts of the sampler:
struct Sampler : fair::mq::Devicemakes the sampler a device.- The device calls
InitTask()in the stateINITIALIZING TASK. The sampler reads its options there withfConfig->GetProperty<T>(). - In the state
RUNNING, the device callsConditionalRun()again and again. Returntrueto get the next call. Returnfalseto go to the stateREADY. NewMessage()makes a message from an existing buffer. FairMQ calls the lambda when it does not need the buffer any more. The lambda deletes the string.Send()sends the message on the channeldata. It returns the number of bytes, or a negative transfer code, for example when a state change interrupts it.addCustomOptions()adds the options--text(defaultHello) and--max-iterations(default0, no limit).getDevice()makes the device object.
Write the sink
Section titled “Write the sink”Write this code into sink.cxx:
#include <fairmq/Device.h>#include <fairmq/runDevice.h>
#include <string>
namespace bpo = boost::program_options;
struct Sink : fair::mq::Device{ Sink() { // register a handler for data arriving on "data" channel OnData("data", &Sink::HandleData); }
void InitTask() override { // Get the fMaxIterations value from the command line options (via fConfig) fMaxIterations = fConfig->GetProperty<uint64_t>("max-iterations"); }
bool HandleData(fair::mq::MessagePtr& msg, int) { LOG(info) << "Received: \"" << std::string(static_cast<char*>(msg->GetData()), msg->GetSize()) << "\"";
if (fMaxIterations > 0 && ++fNumIterations >= fMaxIterations) { LOG(info) << "Configured maximum number of iterations reached. Leaving RUNNING state."; return false; }
// return true if you want the handler to be called again (otherwise return false go to the // Ready state) return true; }
private: uint64_t fMaxIterations = 0; uint64_t fNumIterations = 0;};
void addCustomOptions(bpo::options_description& options){ options.add_options() ("max-iterations", bpo::value<uint64_t>()->default_value(0), "Maximum number of iterations of Run/ConditionalRun/OnData (0 - infinite)");}
std::unique_ptr<fair::mq::Device> getDevice(fair::mq::ProgOptions& /*config*/){ return std::make_unique<Sink>();}The parts of the sink:
- The constructor calls
OnData(). Then the device callsHandleData()for each message that arrives on the channeldata. A device with a data callback does not callConditionalRun()orRun(). HandleData()returnstrueto wait for the next message, orfalseto go to the stateREADY.GetData()andGetSize()give the buffer of the message and its size in bytes.
The main function
Section titled “The main function”Both files include fairmq/runDevice.h. This header defines the function main(). Your code must define the two functions that the header declares:
// to be implemented by the user to return a child class of fair::mq::Devicestd::unique_ptr<fair::mq::Device> getDevice(fair::mq::ProgOptions& config);
// to be implemented by the user to add custom command line options (or just with empty body)void addCustomOptions(boost::program_options::options_description&);The function main() makes a fair::mq::DeviceRunner and calls its Run(). Run() does these steps (see DeviceRunner.cxx):
- It loads the built-in plugins
configandcontrol. - It calls
addCustomOptions(). Your options show in--helpin the groupCustom options. - It parses the command line. For
--help, it prints the options and stops. - It calls
getDevice()to get your device. - It starts the plugins. The plugin
controlrequests the state transitions of the device. - It runs the state machine of the device until the device is in the state
EXITING.
Include fairmq/runDevice.h in only one source file of each program, because it defines main().
Write the CMake file
Section titled “Write the CMake file”Write this code into CMakeLists.txt:
cmake_minimum_required(VERSION 3.15)project(FirstDevice LANGUAGES CXX)
find_package(FairMQ 1.11 REQUIRED)# FairMQConfig.cmake does not find the dependencies of FairMQ. Find them here.foreach(dep IN LISTS FairMQ_PACKAGE_DEPENDENCIES) find_package(${dep} ${FairMQ_${dep}_VERSION} REQUIRED COMPONENTS ${FairMQ_${dep}_COMPONENTS})endforeach()
add_executable(sampler sampler.cxx)target_link_libraries(sampler PRIVATE FairMQ::FairMQ)
add_executable(sink sink.cxx)target_link_libraries(sink PRIVATE FairMQ::FairMQ)The target FairMQ::FairMQ gives your programs the include directories, the C++17 requirement and the libraries. Use FairMQ in a CMake project tells more.
Build the devices
Section titled “Build the devices”cmake -S first-device -B first-device/build -DCMAKE_PREFIX_PATH="$HOME/fairmq_install"cmake --build first-device/buildSet CMAKE_PREFIX_PATH to the installation prefix of FairMQ. If you use spack load fairmq, omit this option, because Spack sets the variable.
Run the devices
Section titled “Run the devices”-
In a terminal, start the sampler:
Terminal window first-device/build/sampler --id sampler1 --rate 1 \--channel-config "name=data,type=push,method=bind,address=tcp://*:5555,rateLogging=0" -
In a second terminal, start the sink:
Terminal window first-device/build/sink --id sink1 \--channel-config "name=data,type=pull,method=connect,address=tcp://localhost:5555,rateLogging=0"
These command lines come from the start script fairmq-start-ex-1-1.sh.in of example 1-1. The options:
| Option | What it does |
|---|---|
--id |
Sets the ID of the device. The plugin config needs the ID to read --channel-config. |
--rate 1 |
Calls ConditionalRun() at most once per second. The unit is Hz. The default 0 sets no limit. |
--channel-config |
Configures one channel with comma-separated key=value pairs. rateLogging=0 switches off the log of the transfer rates, which a channel writes each second by default. |
The output
Section titled “The output”Each log line starts with the time and the severity, for example [14:02:31][STATE]. The output below does not show the time. Before these lines, each device prints the FairMQ logo with the version.
The sampler goes through the states to RUNNING and sends the first message. It waits in Send() until the sink connects:
[STATE] Starting FairMQ state machine --> IDLE[STATE] IDLE ---> INITIALIZING DEVICE[STATE] INITIALIZING DEVICE ---> INITIALIZED[STATE] INITIALIZED ---> BINDING[STATE] BINDING ---> BOUND[STATE] BOUND ---> CONNECTING[STATE] CONNECTING ---> DEVICE READY[STATE] DEVICE READY ---> INITIALIZING TASK[STATE] INITIALIZING TASK ---> READY[STATE] READY ---> RUNNING[INFO] fair::mq::Device running...[INFO] Sending "Hello"[INFO] Sending "Hello"[INFO] Sending "Hello"The sink goes through the same states. Then it logs each message:
[STATE] READY ---> RUNNING[INFO] fair::mq::Device running...[INFO] Received: "Hello"[INFO] Received: "Hello"In the state RUNNING, the plugin control also prints a list of keys. With the default control mode --control dynamic, you can control the device with these keys. For example, h shows the help and q stops the device.
Stop the devices
Section titled “Stop the devices”Press Ctrl+C in each terminal. The plugin control catches the signal SIGINT. It moves the device through the reset states to EXITING (see Control.cxx):
[INFO] Received device shutdown request (signal 2).[INFO] Waiting for graceful device shutdown. Hit Ctrl-C again to abort immediately.[STATE] RUNNING ---> READY[STATE] READY ---> RESETTING TASK[STATE] RESETTING TASK ---> DEVICE READY[STATE] DEVICE READY ---> RESETTING DEVICE[STATE] RESETTING DEVICE ---> IDLE[STATE] IDLE ---> EXITINGA second Ctrl+C stops the process at once with std::abort(). The signal SIGTERM also starts the shutdown, but a second SIGTERM has no effect. The option --catch-signals 0 switches off this signal handling.
Things to try
Section titled “Things to try”- Add
--max-iterations 5 --control staticto both commands. Each device leaves the stateRUNNINGafter 5 messages. In the control modestatic, the device then goes toEXITING, and the program ends. - Add
--text "Hello FairMQ"to the sampler command to send a different text. - Add
--transport shmemto both commands. Then the devices use shared memory for the data. See Transports. - Add
--severity debugto see more log lines, for example the address that each channel uses. - Run
first-device/build/sampler --help. The output shows all options, and your options in the groupCustom options.
Problems that can occur
Section titled “Problems that can occur”- The sink receives nothing. Another program uses the port 5555. Then the sampler binds to a random port from 22000 to 23000, because the channel property
autoBindistrueby default. The log shows this only at the severitydebug. Use a free port in both commands. WithautoBind=0in its--channel-config, the sampler goes to the stateERRORinstead, and the program ends. - The log shows
No device ID or config-key provided for the configuration parser.The command has no--id. Without an ID, the pluginconfigdoes not read--channel-config, and the device has no channels. Add--idto the command. - CMake does not find FairMQ or its dependencies. See Problems that can occur on the CMake page.
Related pages
Section titled “Related pages”- Devices and Device states: the device and its hooks.
- Channels and sockets: types, methods and addresses.
- Receive data with callbacks or loops:
OnData(),ConditionalRun()andRun(). - Example 1-1: the same devices, built in the FairMQ source tree.
- Command-line options: all options of a device.