Write a Server and Client Communicating via RPC (C++)

Excursion (click to expand): Basics of object construction, value/reference semantics and related best practices in C++ (recommended for C++ beginners)

Objective: Learn how to create a server component with an ice interface and a client component communicating via remote procedure calls (RPC) in ArmarX.

Previous Tutorials: Create a "Hello World!" Component in ArmarX (C++), Understand Distributed Systems

Next Tutorials: Write a Publisher and Subscriber Communicating via Topics (C++)

Reference Code: component_tutorials

In this tutorial, you will implement two components communicating in the server-client paradigm (i.e. using remote procedure calls). For the sake of simplicity, the server will provide a service to generate random numbers.

We will use the following names:

  • Server: random_number_server
  • Client: random_number_client

Choose or Create an ArmarX Package

To create a component, you need an ArmarX package. If you followed Create a "Hello World!" Component in ArmarX (C++), you can use the package created there:

cd $ARMARX_WORKSPACE/tutorials/component_tutorials

If you want to create a new package, refer to Create a "Hello World!" Component in ArmarX (C++) or the armarx-package.html for how to do that.

For this tutorial text, we will assume the ArmarX package is called component_tutorials.

Implement the Server

Create the Server Component

First, we create and implement the server component. In the root directory of the ArmarXPackage, use armarx-package to create a new component called random_number_server:

cd .../component_tutorials
armarx-package add component random_number_server

Try to build the fresh component: Navigate to the build directory of the package and run cmake and make.

cd build/
cmake ..
make

Define an Ice Interface for the Server Component

Navigate to the directory where the component was created: component_tutorials/source/components/random_number_server/. Look what is inside:

random_number_server/
CMakeLists.txt
Component.cpp
Component.h
ComponentInterface.ice

You may recognize the files from the "Hello World!" tutorial, but let us recap here:

  • CMakeLists.txt: The "manifest" file required by CMake to configure the project.
  • Component.cpp: The C++ source file containing definitions of functions and variables.
  • Component.h: The C++ header file containing declarations of classes, variables and functions.
  • ComponentInterface.ice: A strange file we know nothing about.

Now, the last point may not be up-to-date anymore! In the previous tutorial (../102_distributed_systems/README.md "Understand Distributed Systems"), you learned about Ice, the communication system used by ArmarX, and Slice, the Specification Language of Ice. The file ComponentInterface.ice is a file written in Slice: It defines the Ice interface of the component random_number_server. When we want to customize interface, we need to edit that file.

Examine the Generated <tt>ComponentInterface.ice</tt>

So, boot up QtCreator and open the project components_tutorial (if it is not open already from the previous tutorials). Open the file ComponentInterface.ice. It should look like this:

#pragma once
module component_tutorials { module components { module random_number_server
{
interface ComponentInterface
{
// Define your interface here.
};
};};};

Let us think about its current content:

#pragma once

We know this line from C++: It is an include guard every header file should have. So Slice may be somehow similar to a c++ Header file?

module component_tutorials { module components { module random_number_server
{

In Slice, modules are a way group names.

  • In C++, they are translated to namespaces (e.g. component_tutorials::components::random_number_server).
  • In Python, they are translated to python modules/subpackages (e.g. component_tutorials.components.random_number_server).
  • The syntax is very similar to C++ namespaces, although the shorthand form module component_tutorials::module components { is not supported.
interface ComponentInterface
{
// Define your interface here.
};

This is our component's interface and carries the beautiful name ComponentInterface. As you see, it is declared with the Slice keyword interface and generally looks like a class. This makes sense, because it is translated to an adequate class in the target languages.

Currently, it does not have any methods, but this will change soon.

};};};

Finally, all opened modules are closed.

Edit the Generated <tt>ComponentInterface.ice</tt>

Now that we understand the basic structure of a Slice/.ice file, it is time to adapt it to our needs. Let us briefly think about what we want: We want clients to be able to call the server and obtain a random number.

Let us start simple: We will add a function generateRandomNumber() to the interface which takes no arguments and returns an int:

interface ComponentInterface
{
int generateRandomNumber();
};

Just as regular functions, Ice interface functions can take any number of arguments, including none. The Request / Response scheme that you saw in ../102_distributed_systems/README.md "Understand Distributed Systems" is just a pattern - but one we strongly suggest to use. We will see how and why later on when we add arguments to the function.

Implement the Interface in the Component

Build the Project and ... Get an Error?

Before we continue, try to build the project now.

You should get an error like this:

[ 83%] Building CXX object source/component_tutorials/components/random_number_server/CMakeFiles/random_number_server_cmp.dir/Component.cpp.o
In file included from /.../component_tutorials/source/component_tutorials/components/random_number_server/Component.h:29,
from /.../component_tutorials/source/component_tutorials/components/random_number_server/Component.cpp:24:
/.../armarx/ArmarXCore/source/ArmarXCore/core/Component.h: In instantiation of ‘static TPtr armarx::Component::create(Ice::PropertiesPtr, const string&, const string&) [with T = component_tutorials::components::random_number_server::Component; TPtr = IceInternal::Handle<armarx::Component>; Ice::PropertiesPtr = IceInternal::Handle<Ice::Properties>; std::string = std::__cxx11::basic_string<char>]’:
/.../armarx/ArmarXCore/source/ArmarXCore/libraries/DecoupledSingleComponent/Decoupled.h:21:46: required from ‘static bool armarx::Decoupled::registerComponent(const string&) [with ComponentT = component_tutorials::components::random_number_server::Component; std::string = std::__cxx11::basic_string<char>]’
/.../component_tutorials/source/component_tutorials/components/random_number_server/Component.cpp:218:5: required from here
/.../armarx/ArmarXCore/source/ArmarXCore/core/Component.h:126:24: error: invalid new-expression of abstract class type ‘component_tutorials::components::random_number_server::Component’
126 | TPtr ptr = new T();
| ^~~~~~~
In file included from /.../component_tutorials/source/component_tutorials/components/random_number_server/Component.cpp:24:
/.../component_tutorials/source/component_tutorials/components/random_number_server/Component.h:43:11: note: because the following virtual functions are pure within ‘component_tutorials::components::random_number_server::Component’:
43 | class Component :
| ^~~~~~~~~
In file included from /.../component_tutorials/source/component_tutorials/components/random_number_server/Component.h:37,
from /.../component_tutorials/source/component_tutorials/components/random_number_server/Component.cpp:24:
/.../component_tutorials/build/source/component_tutorials/components/random_number_server/ComponentInterface.h:429:24: note: ‘virtual Ice::Int component_tutorials::components::random_number_server::ComponentInterface::generateRandomNumber(const Ice::Current&)’
429 | virtual ::Ice::Int generateRandomNumber(const ::Ice::Current& current = ::Ice::emptyCurrent) = 0;
| ^~~~~~~~~~~~~~~~~~~~
make[2]: *** [source/component_tutorials/components/random_number_server/CMakeFiles/random_number_server_cmp.dir/build.make:76: source/component_tutorials/components/random_number_server/CMakeFiles/random_number_server_cmp.dir/Component.cpp.o] Error 1
make[1]: *** [CMakeFiles/Makefile2:718: source/component_tutorials/components/random_number_server/CMakeFiles/random_number_server_cmp.dir/all] Error 2
make: *** [Makefile:146: all] Error 2

Wow, that is a lot of error message ... Well, that is just C++, so it cannot be helped. Anyway, what this basically says is:

error: invalid new-expression of abstract class type ‘component_tutorials::components::random_number_server::Component’

Our Component class is abstract, so it cannot be instantiated. But why?

because the following virtual functions are pure within ‘component_tutorials::components::random_number_server::Component’

Apparently, the class has at least one "pure virtual" function. Now what does that mean?

  • In C++ terminology, a virtual function is a function that can be overridden in a deriving (aka. inheriting / extending / sub-) class. A virtual function's declaration looks like this:
    virtual int foo();

In contrast to Java and Python where all functions can be overridden in deriving classes, in C++ such functions must be marked as virtual in the base (aka. super-) class.

  • Pure virtual is C++'s term for an abstract function, i.e. a function without implementation in this class. A class with at least one pure virtual function is called abstract (yes, also in C++). A pure virtual function is marked by a trailing = 0:

    virtual int foo() = 0;
    Note
    If a function that is not pure virtual has no implementation (i.e. definition in the source file), it is usually an error and leads to "undefined reference" errors during linking.

    Okay so which pure virtual function causes Component to be abstract?

/.../component_tutorials/build/source/component_tutorials/components/random_number_server/ComponentInterface.h:429:24: note: ‘virtual Ice::Int component_tutorials::components::random_number_server::ComponentInterface::generateRandomNumber(const Ice::Current&)’
429 | virtual ::Ice::Int generateRandomNumber(const ::Ice::Current& current = ::Ice::emptyCurrent) = 0;

Aha! The culprit is virtual ::Ice::Int generateRandomNumber(const ::Ice::Current& current = ::Ice::emptyCurrent) = 0;, which is part of the class component_tutorials::components::random_number_server::ComponentInterface! But wait - this is the function we just defined in the ComponentInterface.ice file.

Let us gather the information we have and digest that ...

  • We added a function to an interface.
  • Functions in an interface are, by nature, abstract because they do not have implementations but need to be overridden.
  • In C++, such interface functions are naturally modelled as pure virtual functions.
  • Interfaces are implemented by deriving from them, and then overriding the abstract/pure virtual functions.
  • ... That's it!

We added a function to an interface, but did not implement it yet! Of course the component class is abstract, and we get an error when the framework tries to instantiate it. And that is good: It shows us that we are not finished with our work, but that there is something left to do: Implement the interface function. Okay, so let us do that.

Add the Ice Interface Function to the <tt>Component</tt> Class

First, open the header file Component.h. The important lines are:

#include <component_tutorials/components/random_number_server/ComponentInterface.h>

This line imports the C++ declarations of the Ice interface. This includes the generated interface class component_tutorials::components::random_number_server::ComponentInterface.

class Component :
virtual public armarx::Component,
virtual public component_tutorials::components::random_number_server::ComponentInterface

Apart from the base component class armarx::Component, our class Component derives from the Ice interface class public component_tutorials::components::random_number_server::ComponentInterface that was generated from the ComponentInterface.ice file.

As said, because ComponentInterface contains the pure virtual function generateRandomNumber() and Component does not override and implement it, Component is virtual as well. We can change that by adding a declaration and definition of generateRandomNumber() in Component.

We can use QtCreator to help use here:

  • Position the cursor on class Component :
  • Press Alt+Enter. A menu should pop up.
  • Navigate to the option Insert Virtual Functions of Base Classes and press Enter.
  • In the following dialog, you can check which methods you want to insert and how:
    • When you scroll down, you should see the functiongenerateRandomNumber() from ComponentsInterface. Make sure that it is checked (it may already be checked by default).
    • At the bottom of the dialog, disable adding "virtual" to the function declaration, as we do not intend to have it overridden in a subclass of Component.
    • Enable adding "override" to the function declaration, which tells the compiler that we intend to override a function from a base class (ComponentInterface). This way, we will get an error if the function actually does not override one (e.g. when the signatures do not match).
    • The dialog should look like this:
      The Insert Virtual Functions Dialog
  • Finally, press OK.

QtCreator should take your focus back to Component.h, where it added the function declaration of generateRandomNumber() at the bottom of the class:

// ComponentInterface interface
public:
Ice::Int generateRandomNumber(const Ice::Current& current) override;

First, move it up a bit to where functions are declared in Component: Move it below the protected: section containing the onInitComponent() and related functions and format it like this:

public:
// ComponentInterface interface
Ice::Int generateRandomNumber(const Ice::Current& current) override;

This is just a convention, but the idea is that we go from general to specific inside the Component class. Moreover, we could gather more interface functions in this section here.

If you do want to allow another classes deriving from Component to override the function again,

add the virtual keyword in front of the declaration:

virtual Ice::Int generateRandomNumber(const Ice::Current &) override;

However, there is no reason to do that here.

Okay, trying to build now. You should receive an error like this:

[ 91%] Linking CXX executable ../../../../bin/random_number_server_run
/usr/bin/ld: CMakeFiles/random_number_server_cmp.dir/Component.cpp.o:(.data.rel.ro._ZTVN19component_tutorials10components20random_number_server9ComponentE[_ZTVN19component_tutorials10components20random_number_server9ComponentE]+0x198): undefined reference to `component_tutorials::components::random_number_server::Component::generateRandomNumber(Ice::Current const&)'
collect2: error: ld returned 1 exit status
make[2]: *** [source/component_tutorials/components/random_number_server/CMakeFiles/random_number_server_run.dir/build.make:120: bin/random_number_server_run] Error 1
make[1]: *** [CMakeFiles/Makefile2:666: source/component_tutorials/components/random_number_server/CMakeFiles/random_number_server_run.dir/all] Error 2
make: *** [Makefile:146: all] Error 2
11:46:10: The process "/usr/bin/cmake" exited with code 2.
Error while building/deploying project component_tutorials (kit: ArmarX)
When executing step "Build"

The important part is:

undefined reference to `component_tutorials::components::random_number_server::Component::generateRandomNumber(Ice::Current const&)'

Undefined reference is a classical C++ error. It means that the compiler knows how the function looks like (the function is declared in a header), but the linker does not know where the implementation is (i.e. the definition, usually in a source file, is missing). This usually indicates one of these issues:

  • You declared a function (in a header), but did not implement it (in the source file).
  • You use a library function, but do not link against it.

Can you guess which case we have here?

Solution: We declared the function (non-pure, without the = 0), so our class is not abstract anymore. But we did not implement the function yet, so the "reference" to it is undefined. So let us add this definition.

Again, we can use QtCreator to help us:

  • Place your cursor on the function declaration of generateRandomNumber().
  • Press Alt+Enter.
  • In the pop-up menu, choose Add Definition in Component.cpp.
  • QtCreator should then generate method stub in the source file, waiting for you to be implemented.

If this does not work, add the definition manually in the source file (just underneath all the other method definitions).

The empty function definition looks like this:

Ice::Int Component::generateRandomNumber(const Ice::Current& current)
{
}

It is structured like this:

  • Ice::Int is the return type. If you want to see what it actually is, place your cursor on the word Int part and press F2. This gets you to a symbol's declaration or definition. Doing that will take you to a header file of Ice (Ice/Config.h) and the following code:

    /** The mapping for the Slice int type. */
    typedef int Int;

    This is a type definition: A way in C++ to define an alias, i.e. another name, for an existing type. So an Ice::Int is just another name for the primitive type int.

    By the way, the modern way to do a type definition is to use the using keyword:

    using Int = int;

    Which is a bit more readable because it looks like an assignment.

  • Component:: is the "namespace" of the function. It is Component:: because the function is defined in the class Component.
  • generateRandomNumber(...) is the name of the function and the braces surrounding the argument list.
  • const Ice::Current& current is the last argument in every Ice interface function (Ice Documentation). Ice uses it to provide some context information. However, it is not used most of the time, so you usually can ignore it.
    • To avoid an "unused argument" warning/error, you can remove the argument name from the signature: Ice::Int Component::generateRandomNumber(const Ice::Current&)
    • If you want to provide a default value, use the constant Ice::emptyCurrent.
  • { } is the function body containing the executed code, which is empty at the moment.

If you build now, you get the following error:

[ 83%] Built target hello_world_run
/.../component_tutorials/source/component_tutorials/components/random_number_server/Component.cpp: In member function ‘virtual Ice::Int component_tutorials::components::random_number_server::Component::generateRandomNumber(const Ice::Current&)’:
/.../component_tutorials/source/component_tutorials/components/random_number_server/Component.cpp:126:5: error: no return statement in function returning non-void [-Werror=return-type]
126 | }
| ^
cc1plus: some warnings being treated as errors

As the error says, we are not returning anything yet. It is good to get this error, as the function is not complete yet. For the moment, you can add a

return 0;

to the function stub. Now your component (or rather, your ArmarX package) should finally build without error. However, we need to come back to add the actual implementation that generates a random number.

Implement the Random Number Generation

In C++, we can generate numbers in the following way:

  • We use a random engine to generate randomness.
  • We use a distribution to, based on this randomness, sample numbers according to a given probability distributing.

See cppreference for more information on pseudo-random number generation in C++.

The random engine is an object that we should keep over multiple number generations, so it will generate another randomness each time. However, the distribution is not state-full, so we can instantiate it each time a new. Therefore, we keep the random engine as an attribute of our class, while the distribution will be a local variable in a function.

Here, will use the std::default_random_engine as randomness generator and std::uniform_int_distribution as distribution. (std stands for "standard" is the namespace of the Standard Template Library, aka STL.) On cppreference.com, you can look up which header(s) we must include in order to use them:

cppreference tells us that we need the header <random> to use std::uniform_int_distribution.

Apparently, in order to do random things, we need to include <random>, so add the following line to the top of Component.h:

#include <random>

Hint: In QtCreator, you can use F4 to switch between a pair of header and source.

In addition, add the following member variable to the private section of Component (also in Component.h):

private:
std::default_random_engine engine;

Take a look at the code below to make sure your code for the random_number_server::Component class looks the same (some non-important things removed):

#pragma once
#include <random>
#include <component_tutorials/components/random_number_server/ComponentInterface.h>
namespace component_tutorials::components::random_number_server
{
class Component :
virtual public armarx::Component,
virtual public component_tutorials::components::random_number_server::ComponentInterface
{
public:
/// @see armarx::ManagedIceObject::getDefaultName()
std::string getDefaultName() const override;
/// Get the component's default name.
static std::string GetDefaultName();
protected:
/// @see PropertyUser::createPropertyDefinitions()
armarx::PropertyDefinitionsPtr createPropertyDefinitions() override;
/// @see armarx::ManagedIceObject::onInitComponent()
void onInitComponent() override;
/// @see armarx::ManagedIceObject::onConnectComponent()
void onConnectComponent() override;
/// @see armarx::ManagedIceObject::onDisconnectComponent()
void onDisconnectComponent() override;
/// @see armarx::ManagedIceObject::onExitComponent()
void onExitComponent() override;
public:
// ComponentInterface interface
Ice::Int generateRandomNumber(const Ice::Current& current = Ice::emptyCurrent) override;
private:
static const std::string defaultName;
std::default_random_engine engine;
};
} // namespace component_tutorials::components::random_number_server

Next, switch to Component.cpp. Before we can use engine, we should initialize it with a seed. We can do this in onInitComponent():

void
Component::onInitComponent()
{
std::random_device randomDevice;
this->engine = std::default_random_engine { randomDevice() };
}
  • std::random_device can use the OS and hardware to generate non-deterministic randomness, but it is also more expensive. Therefore, we only use it to initialize our pseudo-random number generator engine.
  • The this-> is optional here, but it makes clear and sure that we are dealing with a member variable here.

<details>

In C++, we do **not need to use new to create any objects.** For example, the line std::random_device randomDevice; does the following:

  • Reserve enough space on the stack to hold an instance of std:random_device.
  • Call the constructor of std::random_device to construct randomDevice at that location.

The variable randomDevice is kept per value in the function. When the scope of the function is left, the destructor of std::random_device is called and the space is freed. No need to involve the heap here, and if so, it is done by std::random_device internally.

In C++, it is perfectly common to create a local variable as value in a function, perform some operations on it, then either destroy it or return it per value (not per reference or pointer!). For example:

std::vector<int> squareNumbersUpTo(int max)
{
std::vector<int> squares;
for (int i = 1; i * i <= max; ++i)
{
squares.push_back(i * i);
}
return squares;
}

Here, an empty std::vector (the standard sequence container in C++) containing ints is constructed as a local variable, filled with square numbers up to a maximum, then returned per value to the caller.

This is a bit of a deeper topic, but for C++ beginners it is good to notice the following things:

  • In C++, the standard behaviour is per value for everything, i.e. both primitive and complex types. This is different to Java and Python, where everything except primitives is kept and passed per reference.
  • As a consequence, it is very simple to deep copy objects in C++. Consider the following example:

    Foo a;
    Foo b = a;

    In the second statement, b = a performs a full copy of a into b, as long as Foo has complete value semantics internally (which should be the default in C++).

  • In order to avoid unnecessary copies of function arguments, it is perfectly common to pass complex arguments as const reference, which is written as const Foo&. (A & after a type means "reference to that type".) You saw an example of that in the Ice interface function generateRandomNumber(const Ice::Current& current).
  • C++ does not have a garbage collector. If you need pointer semantics, use smart pointers, i.e. std::unique_ptr or std::shared_ptr, which take care of memory management using a technique called RAII.
  • (Smart) pointers are necessary to hold polymorphic objects, for example. Copying an instance of a derived class to a variable of a base class can lead to slicing.
  • Do not use new/delete or smart pointers to temporarily construct an object - a value is enough most of the time.
  • Containers like std::vector and std::map do hold their data on the heap, but take care of it themselves. From an outside perspective, they follow value semantics.
  • Non-owning raw-pointers (e.g. Foo* foo) are okay if they are used as optional references (references cannot be null, but pointers can).
  • Never use new and delete to do manual memory management, except when you know what you are doing and that it is the best solution.
  • Some libraries have their own semantics, though. For example, when using the GUI framework Qt (yes, the same "Qt" as in "QtCreator"), you usually create an object using new, then pass it to the Widget class, which takes ownership of the object (and thus takes care of deleting it when necessary):
    HBoyLayout* layout = new HBoxLayout(); // Create an owning pointer to an HBoxLayout.
    widget->setLayout(layout); // Pass the ownership to widget.

Okay, back to the topic.

</details>

Finally, we implement the generateRandomNumber() method by

  • constructing a uniform distribution from 1 to 10,
  • pass engine to the distribution to sample from it,
  • returning the sampled random number:
Ice::Int Component::generateRandomNumber(const Ice::Current&)
{
std::uniform_int_distribution<int> distribution(1, 10);
int randomNumber = distribution(engine);
return randomNumber;
}

To test whether our function is doing the correct thing, we can add the following lines to the onInitComponent() method (after initializing the engine):

// Test the RNG implementation.
std::stringstream ss;
for (int i = 0; i < 10; ++i)
{
ss << "[" << (i + 1) << "] Randomly generated number: " << generateRandomNumber() << "\n";
}
ARMARX_IMPORTANT << ss.str();

A std::stringstream can be used to accumulate string output over multiple instructions, then pass it to an ARMARX_* logging macro as one string.

ARMARX_IMPORTANT should be used with some consideration: It is meant to stand out and should only be used for, well, important messages. It is okay to use it here for testing purposes, but it should be removed or reduced to a lower level once the component is "released".

Now, build the package, and we are ready to test the server!

Test the Server

Now let's have a look at what our implementation does so far. Make sure the Ice service is running on your local system or in your network and that you can connect to it. This can be done via the following command:

armarx status

If you get a message like the following,

Exception while initializing Ice:
Ice.ConnectionRefusedException:
Connection refused
IceGrid is not running
Try starting it with 'armarx start'

you first need to start Ice using armarx start.

Next, start an ArmarX GUI via

armarx gui

In the ArmarX GUI, open the log viewer (LogViewer) if it is not already open. With the log viewer opened, run the server by executing the random_number_server_run executable in a terminal window from the component_tutorials/build/bin directory:

cd .../component_tutorials/build/bin/
./random_number_server_run

An ArmarX application is still just a regular application: You can also start it from the terminal instead of creating a scenario. However, when you want to configure the component in a persistent way, a scenario is the way to go.

The log should show an entry like this:

Log of the random number server.

Or, in text form:

[1] Randomly generated number: 7
[2] Randomly generated number: 2
[3] Randomly generated number: 7
[4] Randomly generated number: 7
[5] Randomly generated number: 3
[6] Randomly generated number: 6
[7] Randomly generated number: 2
[8] Randomly generated number: 2
[9] Randomly generated number: 6
[10] Randomly generated number: 1

You might ask yourself what all this effort about the Ice interface was about if we are not using it anyway. Well, if you made it this far in the tutorial, you're about to find out!

Implement the Client

Create the Client Component

To call our random number generator via Ice, we need to create another component: the client. This is done in the same way as the server: Navigate to the root folder of the component_tutorials package, then use the armarx-package tool as follows:

cd .../component_tutorials/
armarx-package add component random_number_client

To have QtCreator update the project hierarchy:

  • Right-click the project name, a directory or a CMake target (hammer icon!)
  • Press Run CMake

The client does not offer any functionality through Ice. Therefore, we don't need to define an Ice interface for it.

Implement the Client

Connect to the Server for Remote Procedure Calls

First, the client needs to learn about the server's interface (random_number_server::ComponentInterface). For that, we need to include the interface header in the include section of the random_number_client/Component.h:

#include <component_tutorials/components/random_number_server/ComponentInterface.h>

Next, the client needs a way to talk to the server. The client-side representation of another component is a proxy. Using a proxy of the server component, the client can call interface functions of the server, and Ice will take care of the actual communication to the server behind the scenes.

Slice generates type definitions for proxy types alongside the interfaces. If the interface is called MyInterface, the name of the corresponding proxy type is MyInterfacePrx. As we need a proxy to the interface random_number_server::ComponentInterface, the corresponding proxy type is random_number_server::ComponentInterfacePrx (note the trailing Prx). So we just add a private member to the random_number_client::Component class in the header file of random_number_client such as this:

private:
random_number_server::ComponentInterfacePrx server;

The fully qualified name of the server interface is component_tutorials::components::random_number_server::ComponentInterface. Because the client is also in the component_tutorials::components namespace, we can start qualifying the server type only as random_number_server::ComponentInterface.

Try building now. You will probably get errors like this:

[ 94%] Linking CXX executable ../../../../bin/random_number_client_run
/usr/bin/ld: CMakeFiles/random_number_client_cmp.dir/Component.cpp.o: in function `IceInternal::ProxyHandle<IceProxy::component_tutorials::components::random_number_server::ComponentInterface>::~ProxyHandle()':
/usr/include/Ice/ProxyHandle.h:172: undefined reference to `IceProxy::component_tutorials::components::random_number_server::upCast(IceProxy::component_tutorials::components::random_number_server::ComponentInterface*)'
/usr/bin/ld: CMakeFiles/random_number_client_cmp.dir/Component.cpp.o: in function `virtual thunk to component_tutorials::components::random_number_client::Component::~Component()':
/usr/include/c++/9/bits/stl_construct.h:107: undefined reference to `IceProxy::component_tutorials::components::random_number_server::upCast(IceProxy::component_tutorials::components::random_number_server::ComponentInterface*)'
/usr/bin/ld: CMakeFiles/random_number_client_cmp.dir/Component.cpp.o: in function `virtual thunk to component_tutorials::components::random_number_client::Component::~Component()':
/usr/include/c++/9/bits/stl_construct.h:107: undefined reference to `IceProxy::component_tutorials::components::random_number_server::upCast(IceProxy::component_tutorials::components::random_number_server::ComponentInterface*)'
/usr/bin/ld: CMakeFiles/random_number_client_cmp.dir/Component.cpp.o: in function `virtual thunk to component_tutorials::components::random_number_client::Component::~Component()':
/usr/include/c++/9/bits/stl_construct.h:107: undefined reference to `IceProxy::component_tutorials::components::random_number_server::upCast(IceProxy::component_tutorials::components::random_number_server::ComponentInterface*)'
/usr/bin/ld: CMakeFiles/random_number_client_cmp.dir/Component.cpp.o: in function `virtual thunk to component_tutorials::components::random_number_client::Component::~Component()':
Component.cpp:(.text._ZN19component_tutorials10components20random_number_client9ComponentD0Ev[_ZN19component_tutorials10components20random_number_client9ComponentD0Ev]+0x7c): undefined reference to `IceProxy::component_tutorials::components::random_number_server::upCast(IceProxy::component_tutorials::components::random_number_server::ComponentInterface*)'
/usr/bin/ld: CMakeFiles/random_number_client_cmp.dir/Component.cpp.o:Component.cpp:(.text._ZN19component_tutorials10components20random_number_client9ComponentD0Ev[_ZN19component_tutorials10components20random_number_client9ComponentD0Ev]+0x259): more undefined references to `IceProxy::component_tutorials::components::random_number_server::upCast(IceProxy::component_tutorials::components::random_number_server::ComponentInterface*)' follow
collect2: error: ld returned 1 exit status

Extracting the most important part yields:

undefined reference to `IceProxy::component_tutorials::components::random_number_server::upCast(IceProxy::component_tutorials::components::random_number_server::ComponentInterface*)'

So we have an undefined reference to a function in something related to the random_number_server namespace. This is not something from the random_number_client, then, which is a strong hint that this is the other case mentioned above: We are using something from another library, but we do not link it.

Using a function (or any definition in a source file) from another library B in a library (or executable) A requires that A links B. Think about linking as "importing" the definitions from one library (B) into another library (A). In this case, we need to import the definitions of functions related to the Ice interface of random_number_server into random_number_client.

Orchestrating the linker is the job of CMake. Therefore, we need to modify the CMakeLists.txt of random_number_client. When you open it, it should look like this (some out-commented parts removed):

armarx_add_component(random_number_client
ICE_FILES
ComponentInterface.ice
ICE_DEPENDENCIES
ArmarXCoreInterfaces
SOURCES
Component.cpp
HEADERS
Component.h
DEPENDENCIES
# ArmarXCore
ArmarXCore
)

In ArmarX, libraries to link are specified as DEPENDENCIES, because if A links B, A depends on B, and thus B is a dependency of A.

Now we just have to find out the name of what we need to link. You can try to find out by looking at the project hierarchy in QtCreator:

The project hierarchy in QtCreator helps you to find the names of libraries and other targets.

The entries with the hammer icon

Hammer Icon

are targets, which is a CMake term encapsulating things like libraries, executables, and others. Find the file whose contents you want to use, and then check under which target it is listed. This is a good hint at what you need to specify as dependency in CMake.

In this case, we want to use the random_number_server::ComponentInterface, which is defined in the random_number_server/ComponentInterface.ice. It is listed under the random_number_server_ice target. So this is probably what we need.

Add random_number_server_ice to the DEPENDENCIES of random_number_client. Above the line, add a comment # component_tutorials, so the dependencies are grouped by the packages they are contained in.

DEPENDENCIES
# ArmarXCore
ArmarXCore
# component_tutorials
random_number_server_ice

When you build now, the undefined reference error should disappear.

One thing is still missing: We have created the proxy to the server, but we did not initialize it yet. In order to actually connect to the server, we need to inform ArmarX about our intentions.

Head over to the source file of the random_number_client component. Change the body of the function createPropertyDefinitions() to:

Component::createPropertyDefinitions()
{
defs->component(server, "random_number_server",
"Ice object name of the random number server component.");
return def;
}

The line defs->component(server, ...); does multiple things:

  1. Create a property for the name of the random number server.
    • A property is basically a parameter of a component or application. Its value can be specified via the command line or a scenario.
    • The property's default value is set to "random_number_server"
    • The property's human-readable description is set as well.
    • By creating a property for the component's name, users are allowed to change it via configuration without re-compiling code.
  2. Ask ArmarX to set up the communication with the server via remote procedure calls.
    • Declare that the client uses a component as a server:
      • The component's name is specified by the property ("random_number_server" by default).
      • The component's interface is specified by the type of server
        (random_number_server::ComponentInterfacePrx).
      • This implies that the client is supposed to wait until the server is available. Remember: When using remote procedure calls, the client depends on the server. Once all dependencies are available, the client's onConnectComponent() is called.
    • Before the client's onConnectComponent() is called, the variable server will be initialized with a valid proxy to the server.
      • Therefore, the earliest point where we are allowed to use proxies such as server is during the onConnectComponent().
      • Or, in other words, we are not allowed to use proxies before onConnectComponent() (e.g. in the onInitComponent()).
      • You can find more details about the component lifecycle [in the documentation](armarx::ManagedIceObject).

In summary, the line defs->component(server, ...) makes sure that the variable server is initialized and ready to use once the client reaches its onConnectComponent(), and allows the user to configure the server's name.

You can also do these things "by hand", i.e. by writing the code yourself. You will see that a lot in older components which were written when the wrapper defs->component() was not available yet. For reference, the old pattern went like this:

  • Define the property in createPropertyDefinitions():
    Component::createPropertyDefinitions()
    {
    ...
    defs->defineOptionalProperty("cmp.random_number_server", "random_number_server",
    "Ice object name of the random number server.");
    ...
    }
  • Declare that this component is using another component in the onInitComponent():
    void
    Component::onInitComponent()
    {
    usingProxy(getProperty<std::string>("cmp.random_number_server").getValue());
    }
  • Obtain the proxy to the other component in the onConnectComponent():
    void
    Component::onConnectComponent()
    {
    server = getProxy<random_number_server::ComponentInterfacePrx>(
    getProperty<std::string>("cmp.random_number_server").getValue());
    }

As you see, that is a lot of boilerplate code, i.e. code that is almost always the same, for a very common use case. Therefore, we introduced the defs->component() pattern to support the standard use case, which should get you going in most cases with a single line.

If you have more special requirements, though, you can always fall back to the "manual" way of doing things.

Actually, we can make the line even shorter. Have another look at our current code:

defs->component(server, "random_number_server",
"Ice object name of the random number server component.");

This still feels a bit repetitive, does it not? In fact, in a standard setup like this, defs->component() is able to infer the default name and generate a suitable description just from the server argument (or, more precisely, its type). Thus, we can reduce the call to just:

defs->component(server);

It does not get much more minimal than that. However, remember that you can always override the defaults if they do not fit your specific use case.

Call the Server Interface

Finally, we are ready to perform the remote procedure call to the server. At this point, that only takes a single line (and one line to print the result):

void
Component::onConnectComponent()
{
// NOTE: Common mistake: Don't put this in onInitComponent
int randomNumber = server->generateRandomNumber();
ARMARX_IMPORTANT << "Random number received by client: " << randomNumber;
}

This performs the remote procedure call to the server (server->generateRandomNumber()), stores the result in a variable (int randomNumber =) and logs the result using ARMARX_IMPORTANT.

Build your package. Afterwards, you will have two applications executing the server and client separately in two processes.

Before we continue, add some more code to illustrate what happens:

  • Change the client-side code of onConnectComponent() to:
    Component::onConnectComponent()
    {
    ARMARX_INFO << "[Client] Send request to server.";
    int randomNumber = server->generateRandomNumber();
    ARMARX_INFO << "[Client] Received response: " << randomNumber;
    }
  • Change the server-side code of generateRandomNumber() to:
    Ice::Int Component::generateRandomNumber(const Ice::Current&)
    {
    ARMARX_INFO << "[Server] Received request.";
    std::uniform_int_distribution<int> distribution(1, 10);
    int randomNumber = distribution(engine);
    ARMARX_INFO << "[Server] Send response: " << randomNumber;
    return randomNumber;
    }
  • Also, remove or disable the testing code from the server-side onInitComponent() (i.e. the for-loop calling generateRandomNumber() from the server).

You are almost done. Woo! The (almost) only thing remaining now is having a look at what our code does.

Test Server and Client

Start the Components from the Terminals

As before, start an ArmarX GUI and open the log viewer (if not done already). Now, run the applications random_number_server_run and random_number_client_run located in component_tutorials/build/bin/ in two terminals (make sure that the correct ArmarX workspace is active in each terminal):

  • Terminal 1:
    cd .../component_tutorials/build/bin/
    ./random_number_server_run
  • Terminal 2:
    cd .../component_tutorials/build/bin/
    ./random_number_client_run

Watch the log viewer while you are doing that. You should see the following:

Log of Server and Client

or on the terminals:

$ ./random_number_server_run
[34913][20:03:11.181][random_number_server][random_number_server]: [Server] Received request.
[34913][20:03:11.181][random_number_server][random_number_server]: [Server] Send response: 2

and

$ ./random_number_client_run
[34901][20:03:04.958][random_number_client][ObjectScheduler]: ManagedIceObject 'random_number_client' still waiting for:
random_number_server
[34901][20:03:11.179][random_number_client][ObjectScheduler]: All dependencies of 'random_number_client' resolved!
[34901][20:03:11.181][random_number_client][random_number_client]: [Client] Send request to server.
[34901][20:03:11.181][random_number_client][random_number_client]: [Client] Received response: 2

You can see how both the server and the client log their respective messages. But wait: How can the client receive the response before the server has sent it? What magic is this?

Actually, the order in your case may be different. This is a nice difference of how unexpected distributed systems can be: Even the log itself is sent via Ice (a topic, to be precise). It may just have happened that the log messages by the client arrived at the GUI before those by the server did. Because the server operation does not take long, the client almost instantly continues execution. Actually, you can see at the timestamps that all the four log messages were sent in the same millisecond.

Let us change the server-side code to simulate an operation that takes a bit longer to complete:

Note
Includes that are only required by code in the source file should also be included only there. In the header, only add includes that are required in the header.

Now the server will sleep for 1 second before it sends the response. Compile, and run both applications again. The log should now look like this:

The log after adding the 1 second sleep.

Now this looks much more like we expected:

  1. The client sends the request.
  2. The server receives the request (and processes it).
  3. The server sends the response.
  4. The client receives the response.

You can also see from the time stamps that the server operation took 1 second.

Try playing around with this a bit and see how the components behave. For example:

  • Start the components in different orders
  • While one component is running, restart the other

Start the Components in a Scenario

Starting applications from the terminal is nice for quick testing. But would you not say that starting both in a scenario in the GUI is much more comfortable? Luckily, you already know how to do that from the "Hello World!" Tutorial, so we will leave this as a small exercise for you. Do not skip it!

If you need a refreshment, go back to the "Hello World!" Tutorial and see how the scenario was created there. The only difference here is that you need to add two components, the random_number_server and the random_number_client. The rest works the same way.

The resulting scenario could look like this:

A scenario with server and client.

Good job! You now know how to ...

  • create a server component,
  • extend its Ice interface,
  • implement its Ice interface, and
  • create a client component that performs a remote procedure call to the server.

That is really great!

But There's More ...

However, there are still some things we did not cover:

  • The Ice interface function generateRandomNumber() does not take any arguments yet. How would the client specify the range it would like the random number to be in?
  • What if the server needs to return more than just a single integer? We could want it to generate multiple numbers in bulk, or return additional information.
  • What if we need to add more data to the input and output of an Ice interface function in general? How much work is it to update all servers implementing and clients using the interface? Can we prepare our interface to reduce the required maintenance?
  • How can we add more parameters (aka properties) to a component that we can configure in the scenario?

So many questions! We will attend to them in future tutorials.

Next Up

In the next tutorial, you will learn how to Write a Publisher and Subscriber Communicating via Topics (C++).

ARMARX_IMPORTANT
#define ARMARX_IMPORTANT
Definition: Logging.h:183
armarx::core::time::Clock::WaitFor
static void WaitFor(const Duration &duration)
Wait for a certain duration on the virtual clock.
Definition: Clock.cpp:104
Clock.h
armarx::core::time::Duration::Seconds
static Duration Seconds(std::int64_t seconds)
Constructs a duration in seconds.
Definition: Duration.cpp:83
armarx::ctrlutil::a
double a(double t, double a0, double j)
Definition: CtrlUtil.h:45
max
T max(T t1, T t2)
Definition: gdiam.h:48
Component.h
armarx::Component
Baseclass for all ArmarX ManagedIceObjects requiring properties.
Definition: Component.h:95
armarx::control::common::getValue
T getValue(nlohmann::json &userConfig, nlohmann::json &defaultConfig, const std::string &entryName)
Definition: utils.h:55
armarx::ComponentPropertyDefinitions
Default component property definition container.
Definition: Component.h:70
IceUtil::Handle< class PropertyDefinitionContainer >
armarx::VariantType::Int
const VariantTypeId Int
Definition: Variant.h:916