Robot Raconteur Core C++ Library
|
Robot Raconteur is an object-oriented client-service RPC framework. Services expose objects, which are made available to clients using "object references", sometimes referred to as "proxies". See Introduction for an overview of the Robot Raconteur framework.
The object types and their members are defined in "service definition" files. See Service Definitions for more information on how service objects are defined. C++ uses RobotRaconteurGen
to generate "thunk" source that implements the object and value types defined in service definitions. See CMake and Robot Raconteur Thunk Source Generation, RobotRaconteurGen
Utility, and CMake ROBOTRACONTEUR_GENERATE_THUNK
Macro for more information on thunk source and thunk source generation.
For C++, the thunk source generates an abstract class with pure virtual functions for accessing the members. Services implement these abstract interfaces by creating objects that implement the defined members. Robot Raconteur then creates object references and member proxies to allow clients to interact with the members from the remote node.
Developing C++ services is not a trivial task. Because of the design of C++, the creation of security flaws or other terminal bugs is difficult to avoid for novice programmers. The use of Python, C#, or Java is recommended unless C++ is absolutely necessary.
Services are registered with a node using RobotRaconteur::RobotRaconteurNode::RegisterService(). This function takes the name of the service, the name of the service definition of the root object, and the root object instance. The root object must implement an abstract interface generated from a service definition. See Implementing Service Objects. Once registered, the root object will be available to services.
The node must be configured to accept incoming connections. See C++ Node Setup.
The service may have security configured. See Service Security.
Each service is managed by an instance of RobotRaconteur::ServerContext. This object interacts with the node to receive and send messages. It manages the service objects and client connections. See RobotRaconteur::ServerContext for the public API. It can be accessed using the RobotRaconteur::RRServiceObject interface, the return from RobotRaconteur::RobotRaconteurNode::RegisterService(), or using the RobotRaconteur::ServerContext::GetCurrentServerContext() function.
Each client has a RobotRaconteur::ServerEndpoint associated with it. It can be accessed during member calls using RobotRaconteur::ServerEndpoint::GetCurrentEndpoint(). The server endpoint is used by members to identify a client. The function RobotRaconteur::ServerEndpoint::GetCurrentAuthenticatedUser() can be used to retrieve the authentication for the current server endpoint. To retrieve the uint32_t
endpoint ID, use RobotRaconteur::ServerEndpoint::GetCurrentEndpoint()->GetLocalEndpoint().
Each object in a service has a "service path" that uniquely identifies the object. See Service Paths for more information.
Services expose implementations of object types for use by clients. These implementations contain the real functionaly that the service provides. For instance, a robot driver service object would have members that command the robot. This command can be done through a proprietary API provided by the robot vendor, directly using hardware registers, or one of the other variety of ways that software can interact directly with hardware. The implementation may also be a software component such as a robot planner. The members are used to command the planner to generate robot plans based on parameters passed through the members. Robot Raconteur is not specific about what the implementation of each service object does, rather Robot Raconteur is designed to provide access to the object members from a remote node through a client connection. It is up to the developer of the service to decide what the implemented object actually does. It is also up to the service developer to decide what object, member, and value types need to be used to implement a service. The use of standardized or commonly used service definitions is recommended to increase compatibility between services. Custom service definitions can be created if necessary, but should be avoided if possible.
The thunk source generated by RobotRaconteurGen
or the CMake macro contains an "abstract interface" and a "default implementation" for each object type defined in the service definition. The "abstract interface" is a C++ class with pure virtual functions for each member. On the client, these pure virtual functions are implemented to proxy the interactions to the service. On the service side, a class implementing these virtual functions is expected. The implementation of the interface should extend the interface. A "default implementation" is also generated by the thunk source alongside the abstract interface. This is a bare-bones implementation extending the abstract interface. The service developer can extend this "default implementation" instead of extending the abstract interface directly. The developer can use the behavior of the "default implementation", overriding member virtual functions when necessary to change the behavior of the object. The rest of this section assumes that the developer chooses to use the "default implementation".
Note that virtual inheritance must always be used with Robot Raconteur service objects!
Consider the following example service definition:
service experimental.service_example1 object MyObject property double my_property function void my_function() end
The generated C++ "abstract interface" thunk source results in the following:
// In namespace experimental::service_example1 // Modified for brevity class MyObject : public virtual RobotRaconteur::RRObject { public: virtual double get_my_property()=0; virtual void set_my_property(double value)=0; virtual void my_function()=0; }; using MyObjectPtr = boost::shared_ptr<MyObject>;
The service developer is responsible for developing a subclass of MyObject
that implements all of the pure virtual functions. The exact form of pure virtual function function for each member is discussed later in this section.
The "default implementation" for MyObject
results in the following declaration:
// In namespace experimental::service_example1 // Modified for brevity class MyObject_default_impl : public virtual MyObject, public virtual RobotRaconteur::RRObject_default_impl { protected: double rrvar_my_property; public: MyObject_default_impl(); virtual double get_my_property(); virtual void set_my_property(double value); virtual void my_function(); };
and implementation:
// In namespace experimental::service_example1 // Modified for brevity MyObject_default_impl::MyObject_default_impl() { rrvar_my_property=0.0; } double MyObject_default_impl::get_my_property() { boost::mutex::scoped_lock lock(this_lock); return rrvar_my_property; } void MyObject_default_impl::set_my_property(double value) { boost::mutex::scoped_lock lock(this_lock); rrvar_my_property = value; } void MyObject_default_impl::my_function() { throw RobotRaconteur::NotImplementedException(""); }
The "default implementation" is a class named with the object name with _default_impl
appended. In the above example, the property my_property
and function my_function
are implemented by the default implementation. The property implementation stores the value of the property in a class field named with the property named prefixd with rrvar_
. The accessors use this_lock
for thread locking. this_lock
is defined in RobotRaconteur::RRObject_default_impl, which is a virtual base class of all default implementations. Thu function my_function
will throw RobotRaconteur::NotImplementedException when invoked. The service developer must implement this virtual function in the subclass of the default implementation.
The following is an example implementation of MyObject
that uses the default implementation:
using namespace RobotRaconteur; using namespace experimental::service_example1 class MyObjectImpl : public virtual MyObject_default_impl { public: virtual void set_my_property(double value) { if (value < 0.0) { throw RobotRaconteur::InvalidArgumentException("my_property must not be negative"); } boost::mutex::scoped_lock lock(this_lock); rrvar_my_property = value; } virtual void my_function() { double p; { boost::mutex::scoped_lock lock(this_lock); p = rrvar_my_property; } std::cout << "my_property is currently: " << p << std::endl; } };
This implementation does a bounds check on my_property
, and implements my_function
to print the current value to the terminal on the service side. Note the use of this_lock
to protect rrvar_my_property
. This is necessary because member calls on service objects come from the thread pool. Multiple threads may call members concurrently, which can lead to data corruption. Use this_lock
to protect data that can be corrupted by concurrent calls. See C++ Multithreading and Asynchronous Functions for more information on the thread pool.
The generated thunk source also provides an "abstract default implementation". The "abstract default implementation" ends with _abstract_default_impl
and is intended to be used if a class extends another _default_impl
class.
Object members such as pipe
, wire
, and callback
need to be initialized by the service before they can be used. Service objects may inherit RobotRaconteur::IRRServiceObjectIntereface to be notified when the object has been initialized. The function RobotRaconteur::IRRServiceObject::RRServiceObjectInit() is called when the initialization is complete.
An example that improves MyObjectImpl
to implement IRRServiceObject
:
using namespace RobotRaconteur; using namespace experimental::service_example1 class MyObjectImpl : public virtual MyObject_default_impl, public virtual IRRServiceObject { protected: boost::weak_ptr<ServerContext> ctx; std::string this_service_path; public: virtual void RRServiceObjectInit(boost::weak_ptr<ServerContext> ctx, const std::string& service_path) { // Save ctx and service_path for later use this->ctx = ctx; this_service_path = service_path; // Do other initialization tasks here } virtual void set_my_property(double value) { if (value < 0.0) { throw RobotRaconteur::InvalidArgumentException("my_property must not be negative"); } boost::mutex::scoped_lock lock(this_lock); rrvar_my_property = value; } virtual void my_function() { double p; { boost::mutex::scoped_lock lock(this_lock); p = rrvar_my_property; } std::cout << "my_property is currently: " << p << std::endl; } };
Property members allow clients to "get" and "set" a property value on the service object. Properties may use any valid Robot Raconteur value type.
Property members are implemented as two access functions in the object, a "get" and "set" function. The "get" function is the name of member prepended with get_
. It takes no arguments, and returns the current value. The "set" function is the name of the member prepended with set_
. It takes the new property value, and returns void. Service objects must implement the accesor functions, or use the _default_impl
defaul implementation of the property.
For example, the property definition:
property double my_property
An example implementation of the property accessors:
virtual double get_my_property() { boost::mutex::scoped_lock lock(this_lock); // Return the current property value here } virtual void set_my_property(double val) { boost::mutex::scoped_lock lock(this_lock); // Set the property value here }
Properties can be declared readonly
or writeonly
using member modifiers. If a property is readonly
, the set_
accessor function is not used. If a property is writeonly
, the get_
accessor function is not used.
Properties can be implemented as asynchronous functions instead of the synchronous accessors above. The thunk source generates an asynchronous version of the abstract interface class that has asynchronous versions of the property member accessors and function members. The asynchronous interface starts with async_
followed by the object name. The asynchronous accessor functions start with async_get_
and async_set_
. If the service object extends the asynchronous interface, the property and function members will always call the asynchronous versions. The async_set_
accessor will not be generated if the property is declared readonly
. The async_get_
accessor will not be generated if the property is declared writeonly
.
Function members allow clients to invoke a function on the service object. Functions may have zero or more value type parameters, and return a value or be declared void
for no return. Functions may be "normal", not using a generator, or be "generator functions" which return a generator.
Normal functions accept zero or more value type parameters, invoke the remote function with these parameters, and return the result, or void
. They are implemented in the abstract interface as a C++ function with the same name as the member.
For example, the function definition:
function double addTwoNumbers(int32 a, double b)
An example implementation of the function:
virtual double addTwoNumbers(int32_t a, double b) { return (double)a + b; }
An example function definition with no parameters and void return:
functon void do_something()
An example implementation of the function:
virtual void do_something() { // Do the operation }
Functions can be implemented as asynchronous functions instead of synchronous functions. The thunk source generates an asynchronous version of the abstract interface class that has asynchronous versions of function members. The asynchronous interface starts with async_
followed by the object name. The asynchronous function implementation start with async_
followed by the member name. If the service object extends the asynchronous interface, the asynchronous versions of the functions will always be called.
Generator functions are similar to normal functions, but instead of returning a value or void, they return a generator. A generator is similar to an iterator, or can implement a coroutine. See RobotRaconteur::Generator and Functions for more discussion on generators.
Generators may be Type 1, 2, or 3 depending on the argument and return value configuration for Next()
. See Generator Functions for a discussion of these different generator types.
The service implementation of generator functions must return a generator object. This generator object must extend RobotRaconteur::Generator. The client will then be able to call the generator using the generator reference provided to the client. RobotRaconteur::Generator has pure virtual functions for Next()
, Abort()
, Close()
, and the asynchronous versions AsyncNext()
, AsyncAbort()
, and AsyncClose()
. By default, the service will always call the asynchronous versions of these functions. If the service prefers to use synchronous versions of these function, the generator implementation can extend RobotRaconteur::SyncGenerator.
An example service definition containing a generator function to count to 100 with a given start:
service experimental.service_example2 object MyObject function int32{generator} count_to_100(int32 start) end
An implementation of MyObject
and the generator returned by count_to_100()
:
using namespace RobotRaconteur; using namespace experimental::service_example2; class CountTo100Generator : public virtual SyncGenerator<int32_t,void> { protected: int32_t count = 0; bool aborted = false; bool closed = false; boost::mutex this_lock; public: CountTo100Generator(int32_t start) { count = start; } virtual int32_t Next() { boost::mutex::scoped_lock lock(this_lock); if (aborted) { // Throw OperationAbortedException if the generator was aborted throw OperationAbortedException(""); } if (closed || count > 100) { // Throw StopIterationException if generator is complete or closed closed = true; throw StopIterationException(""); } return count++; } virtual void Close() { boost::mutex::scoped_lock lock(this_lock); // Close the generator closed = true; } virtual void Abort() { boost::mutex::scoped_lock lock(this_lock); // Abort the generator aborted = true; } }; class MyObjectImpl : public virtual MyObject_default_impl { public: GeneratorPtr<int32_t,void> count_to_100(int32_t start) { boost::mutex::scoped_lock lock(this_lock); if (start < 1 || start > 100) { throw InvalidArgumentException("start must be between 1 and 100"); } return boost::make_shared<CountTo100Generator>(start); } };
As shown in the above example, the generator should return values until complete. If the generator runs out of values or is closed, it should throw RobotRaconteur::StopIterationException. If the generator is aborted, it should throw RobotRaconteur::OperationAbortedException. Abort should be used in situations where any modifications the generator may have made should not be commited. It can also be used if the generator represents a physical motion, like executing a robot trajectory, that requires the motion to be rapidly aborted.
Events are used by the service to notify all connected clients an event has occurred. Events may have zero or more value type parameters. Events are sent to all connected clients. In C++, events are implemented using boost::signals2::signal
. See the documentation for boost::signals2::signal
for more information on using Boost.Signals2. An example event definition:
event somethingHappened(string what, double when)
The abstract interface generates an accessor for a boost::signals2::signal
reference to be returned by the service. The service needs to create an instance of the signal as a field in the class, and return a reference to that instance in the accessor function. The _default_impl
class does this automatically, creating a field rrvar_
followed by the event member name.
The following example shows the event being fired from a class extending _default_impl
.
// In the same C++ class as the event, extending from the "_default_impl" class void fire_event(const std::string& what, double when) { boost::mutex::scoped_lock lock(this_lock); rrvar_somethingHappened(what, when); }
The fire_event()
function will trigger the signal. The service listens to this signal, and will forward the event to all connected clients.
ObjRef members are used to access other objects within a service. See Service Paths for more information on objrefs and service paths. An example objref definition:
objref MyOtherObject other_object
This objref definition results in an accessor function in the abstract interface that needs to be implemented:
class MyOtherObjectImpl : public virtual MyOtherObject { // TODO: Implement the other object }; // In the object owning the other_object member: virtual MyOtherObjectPtr get_other_object() { // Return a new object, or a pointer to an existing object return boost::make_shared<MyOtherObject>(); };
ObjRefs may also be indexed with an int32_t
or string
. See ObjRef Members for a discussion of the different forms of the accessor function. The index is passed to the service as a parameter to the accessor function. It is up to the service to decide what object to return based on the index. If the index is invalid, RobotRaconteur::InvalidArgumentException should be thrown.
ObjRefs also use the async_
asynchronous abstract interface if the service object extends it. The asynchronous accessor function is prefixed with async_get_
.
Pipe members provide reliable (or optionally unreliable) data streams between clients and service, in either direction. See RobotRaconteur::Pipe for a discussion of pipes.
An example pipe definition:
pipe double[] sensordata
Results in the following pure virtual functions being generated in the abstract interface that need to be implemented by the service object:
virtual PipePtr<RRArrayPtr<double>> get_sensordata(); virtual void set_sensordata(PipePtr<RRArrayPtr<double>> pipe);
Pipes are initialized by the service when the root service object is registered, or after service objects are returned from objref members. The service will call the set_
accessor function with a pipe server for use by the service. The service object is expected to store a reference to this pipe server, and accept incoming connection requests from clients. The service uses RobotRaconteur::Pipe::SetConnectCallback() to specify a callback function to invoke when clients request pipe connections. Each time a client connects a pipe, the callback is invoked with a RobotRaconteur::PipeEndpointPtr that the service can use to send and/or receive packets with the client. If the service needs to reject the incoming pipe connection, the callback can throw an exception. This exception will abort the connection, and pass the exception back to the client. The service is responsible for maintaining a reference to the RobotRaconteur::PipeEndpointPtr, but the reference is still owned by the pipe. It is recommended that boost::weak_ptr<RobotRaconteur::PipeEndpoint<T>>
be used to store the endpoint pointer to avoid memory leaks.
Pipes declared readonly
may only send packets on the service side. Pipes declared writeonly
may only receive packets on the service side.
There is a helper class RobotRaconteur::PipeBroadcaster that can be used when the service needs to send the same packets to every connected client. It should only be used with pipes declared readonly
. The pipe broadcaster has optional flow control, that tracks how many packets are "in flight", and compares it to the "maximum backlog". If the number of packets in flight exceeds the maximum backlog, sending packets is paused. (This flow control method only works with reliable pipes.)
The _default_impl
will automatically create a RobotRaconteur::PipeBroadcaster for readonly
pipes. The pipe broadcaster is stored in a field rrvar_
followed by the name of the pipe member.
An example, assuming that the sensordata
pipe is in MyObject
, and using IRRServiceObject
to set the maximum backlog:
class MyObjectImpl : public virtual MyObject_default_impl, public virtual IRRServiceObject { public: virtual void RRServiceObjectInit(boost::weak_ptr<ServerContext> ctx, const std::string& service_path) { // Set the maximum backlog to prevent overloading the transport rrvar_sensordata->SetMaxBacklog(3); } protected: void SendData(RRArrayPtr<double> data) { boost::mutex::scoped_lock lock(this_lock); // Test to make sure that rrvar_sensordata has been initialized if (rrvar_sensordata) { // Send the packet, don't wait for send completion rrvar_sensordata->AsyncSendPacket(data,[]()); } } };
AsyncSendPacket()
is used to prevent the SendData()
function from blocking the thread.
If the pipe member is not marked readonly
, the _default_impl
will store RobotRaconteur::PipePtr in rrvar_
. Use RobotRaconteur::Pipe::SetConnectCallback() in RRServiceObjectInit
to set the connect callback function.
Callbacks allow the service to invoke a function on a specific client. The definition is nearly identical to a function
member, except the keyword is callback
and generators are not supported. An example callback definition:
callback double addTwoNumbersOnClient(int32 a, double b)
The callback is managed by an instance of RobotRaconteur::Callback. The example results pure virtual functions being generated in the abstract interface that need to be implemented by the service object:
virtual CallbackPtr<boost::function<double (int32_t, double)> > get_addTwoNumbersOnClient(); virtual void set_addTwoNumbersOnClient(CallbackPtr<boost::function<double (int32_t, double)> > callback);
The service object needs to store the RobotRaconteur::CallbackPtr in a field so it can retrieve proxies to client callbacks. The _default_impl
automatically handles this, and stores the RobotRaconteru::CallbackPtr in a field rrvar_
followed by the member name.
Service objects retrieve proxies to the client callbacks using RobotRaconteur::Callback::GetClientFunction(). This function takes a uint32_t
client endpoint ID to select which client to call. This client endpoint ID can be determined using RobotRaconteur::ServerEndpoint::GetCurrentEndpoint()->GetLocalEndpoint() called during a function member or property member request.
An example service definition demonstrating using a callback:
service experimental.service_example3 object MyObject function void other_function() callback double addTwoNumbersOnClient(int32 a, double b) end
An implementation of addTwoNumbersOnClient()
that will call the last client to invoke other_function()
:
using namespace ::experimental::service_example3; class MyObjectImpl : public virtual MyObject_default_impl { uint32_t client_id = 0; public: virtual void other_function() { // Store the endpoint ID boost::mutex::scoped_lock lock(this_lock); client_id = ServerEndpoint::GetCurrentEndpoint()->GetLocalEndpoint(); } protected: double invoke_add(int32_t a, double b) { boost::function<double(int32_t,double)> cb; { boost::mutex::scoped_lock lock(this_lock); try { // Get a proxy to the client callback cb = rrvar_addTwoNumbersOnClient->GetClientFunction(client_id); } catch (std::exception&) { // If getting the callback failed, set client_id to zero, or invalid client_id = 0; throw InvalidOperationException("Client ID is not valid"); } } return cb(a,b); } };
The above example will invoke the callback on the last function to call other_function()
. The choice of client which client to use depends completely on the purpose of the callback. The callback can be invoked on any connected client. If the client has not specified a callback for use, a RobotRaconteur::InvalidOperationException is thrown.
Wire members provide a "most recent" values. They are typically used to communicate a real-time signal, such as a robot joint angle. See RobotRaconteur::Wire for a discussion of wires.
An example wire definition:
wire double[2] currentposition
Results in the following functions pure virtual being generated in the abstract interface:
virtual WirePtr<RRArrayPtr<double>> get_currentposition(); virtual void set_currentposition(WirePtr<RRArrayPtr<double>> wire);
These functions must be implemented by the service object.
Implementing wires can be somewhat complicated. The use of the helper classes RobotRaconteur::WireBroadcaster and RobotRaconteur::WireUnicastReceiver are recommended. Using these two helper classes will be discussed first, followed by a discussion of using the wire without helper classes.
Wires can be marked as readonly
or writeonly
. If neither is specified, the wire can send values in both directions. Wire memers are usually set to readonly
or writeonly
for most service designs. While using a full-duplex wire is possible, the need for full-duplex wires is not typical. readonly
wires can only send values from service to client. writeonly
wires can only send values from client to service.
For readonly
wires, the service can use the RobotRaconteur::WireBroadcaster to send the same values to all connected wires. The OutValue
is set on the wire broadcaster, and this value is sent to all connected wires. For writeonly
wires, the service can use the RobotRaconteur::WireUnicastReceiver. The unicast receiver is desigen to provide the InValue
of the most recent wire connection to connect. This InValue
is set by the client, providing values from the client to the service. If a client wire connection is already established, it is closed in favor of the more recent connection. Clients locking should be used to prevent other clients from connecting. See Object Locking. The RobotRaconteur::WireBroadcaster and RobotRaconteur::WireUnicastReceiver automate most of the functionality of the wire. In most cases the user simply needs to set the OutValue
of the broadcaster and query the InValue
of the receiver.
The generated _default_impl
class implements the accessor functions for the wires, and stores the wire in class fields named rrvar_
appended with the name of the member. The _default_impl
has special behavior for readonly
and writeonly
members. For readonly
wire members, the rrvar_
field will automatically be initialized with a RobotRaconteur::WireBroadcaster. For writeonly
wire members, the rrvar_
field will automatically be initialized with a RobotRaconteur::WireUnicastReceiver. (The service can override the get_
and set_
accessor functions to override this behavior.)
Wires are typically used with a (soft) real-time control loop, such as a robot feedback loop. The following is a simple example of a first-order discrete-time system y[n] = -a*y[n-1] + b*u[n] loop implemented using wires for input and output.
Discrete time loop service definition:
service experimental.service_example4 object MyObject wire double y [readonly] wire double u [writeonly] end
The service object implementation of the discrete time system:
class MyObjectImpl : public virtual MyObject_default_impl, public virtual IRRServiceObject { double y_n1 = 0; double a = 0.1; double b = 0.1; public: virtual void RRServiceObjectInit(boost::weak_ptr<ServerContext> ctx, const std::string& service_path) { rrvar_u->SetInValueLifespan(250); } // Run one step of the discrete time system void Step() { boost::mutex::scoped_lock lock(this_lock); // If either wire has not been initialized, return if (!rrvar_y || !rrvar_u) { return; } // Get "u", if not available, use zero double u = 0; TimeSpec ts; uint32_t ep; // rrvar_u is initialized to WireUnicastReceiver<double> by MyObject_default_impl if (!rrvar_u->TryGetInValue(u,ts,ep)) { // Set u to zero if no input available u = 0; } double y = - a * y_n1 + b * u; // rrvar_y is initialized to WireBroadcaster<double> by MyObject_default_impl // Set the out value wire rrvar_y->SetOutValue(y); // Save y for next iteration y_n1 = y; } };
The RobotRaconteur::WireUnicastReceiver::SetInValueLifespan() is used to give the received input value a finite lifespan. If the client disconnects or stops sending data, the in value will expire, preventing stale data from being received. The Step()
function must be called periodically by the program. This is typically done in a loop in main()
or from a thread.
For a device like a robot or a senser, the wires would be used to send feedback and receive commands instead of being used with a software discrete time system.
The discussion and examples so far have used the helper classes WireBroadcaster
and WireUnicastReceiver
. These classes automatically manage the incoming wire connections, peek requests, and poke requests. If the service does not want to use a helper class, it must implement the get_
and set_
accessors in the object, and it must set callbacks for incoming wire connections, peek in value requests, peek out value requests, and poke out value requests. The relevant functions to set the callbacks are RobotRaconteur::Wire::SetWireConnectCallback(), RobotRaconteur::Wire::SetPeekInValueCallback(), RobotRaconteur::Wire::SetPeekOutValueCallback(), and RobotRaconteur::Wire::SetPokeOutValueCallback(). These callbacks are usually configured in the set_
accessor. The set_
accessor will only be called once by the service. Manually managing wire connections and peek/poke callbacks is not normally necessary, since the helper classes can be used directly or subclassed to implement wire functionality.
Memories are used to read and write a memory segment on the service. Memories may be numeric arrays, numeric multidimarrays, pod arrays, pod multidimarrays, namedarray arrays, or namedarray multidimarrays. The different types of memories and their corresponding C++ classes are discussed here: Memory Members.
A numeric array memory client and a numeric multidimarray memory client will be used as examples. Pod and namedarray memories are identical, except for the memory class and the value types being utilized.
Example array memory definition:
memory double[] datahistory
Results in a single pure virtual accessor function being generated in the abstract interface that must be implemented:
virtual ArrayMemoryPtr<double> get_datahistory() { // Assume that there is a field storing the data RRArrayPtr<double> my_data = self->my_data_; // Return an array memory return boost::make_shared<ArrayMemory>(my_data); }
The service will proxy the read and write requests to the returned memory. The service can return the existing memory C++ classes, however these do not provide any locking or data protection. Is is recommended that the provided C++ classes be extended for the specific needs of the application.
Memory members are a seldomnly used feature. They should only be used when a device provides a true shared memory region, such as a ring buffer or a set of registers that must be exposed, or when there is a very large data set that is randomnly accessed by clients.
Services can be secured using a RobotRaconteur::ServiceSecurityPolicy instance. The security policy is passed to RobotRaconteur::RobotRaconteurNode::RegisterService() function when the service is registered. The constructor takes a map of policies, and a pointer to a user authenticator. Currently, the only authenticator available is RobotRaconteur::PasswordFileUserAuthenticator. This authenticator takes a file or string of usernames, passwords, and privileges to authenticate against. See RobotRaconteur::PaswordFileUserAuthenticator for information on the file format. See Security for a discussion of Robot Raconteur security.
The following is a typical example of a secured service being initialized:
// Username, password, and privileges data, one user per line. Passwords md5 hashed std::string password_auth_data = "user1 79e262a81dd19d40ae008f74eb59edce objectlock" "\n" "user2 309825a0951b3cf1f25e27b61cee8243 objectlock" "\n" "superuser1 11e5dfc68422e697563a4253ba360615 objectlock,objectlockoverride" "\n"; // Create the service object MyObjectPtr obj = boost::make_shared<MyObjectImpl>(); std::map<std::string,std::string> policies = { {"requirevaliduser", "true"}, {"allowobjectlock", "true"} }; // Create the password authenticator PasswordFileUserAuthenticatorPtr auth = boost::make_shared<PasswordFileUserAuthenticator>(password_auth_data); // Create the security policy ServiceSecurityPolicyPtr s = boost::make_shared<ServiceSecurityPolicy>(auth,policies); // Register the service RobotRaconteurNode::s()->RegisterService("my_service", "experimental.service_example5", obj);
The above example has two normal users, "user1" and "user2", and one superuser, "superuser1". The superuser has the "objectlockoverride" privilege, allowing the superuser to unlock any object regardless of which user created the lock. The passwords are stored as md5 hashes. These md5 hashes can be generated using RobotRaconteurGen
. See RobotRaconteurGen
Utility. The password data should be stored in a file so it can be modified as users are added and removed.
Clients can request object locks to gain exclusive access. There are three types of object locking: user locks, client locks, and monitor locks. See object_locking and Object Locking for more information on object lock types.
User locks and client locks only require that the "allowobjectlock" policy is set, that the current client is authenticated, and the authenticated client has the "objectlocking" privilege. Object locking at this point is handled automatically by the service. The service can create and release locks on behalf of clients using RobotRaconteur::ServerContext::RequestObjectLock(), RobotRaconteur::ServerContext::RequestClientObjectLock(), and RobotRaconteur::ServerContext::ReleaseObjectLock(). See each function for more information.
Monitor locks are used to request a thread-exclusive lock. Service objects that wish to be monitor-lockable must extend and implement RobotRaconteur::IRobotRaconteurMonitorObject. This interface contains function to enter the lock, and exit the lock. The simplest implementation will use a boost::mutex
to implement the lock. The following example uses this method:
using namespace RobotRaconteur; class MyObjectImpl : public virtual MyObjectDefaultImpl, public virtual IRobotRaconteurMonitorObject { public: virtual void RobotRaconteurMonitorEnter() { monitor_lock.lock(); } virtual void RobotRaconteurMonitorEnter(int32_t timeout) { if (timeout==-1) { RobotRaconteurMonitorEnter(); } else { monitor_lock.timed_lock(boost::posix_time::milliseconds(timeout)); } } virtual void RobotRaconteurMonitorExit() { monitor_lock.unlock(); } protected: boost::mutex monitor_lock; };
The service can also use monitor_lock
directly, when it needs to prevent clients from accessing a memory region.
Not that unlike client and user locks, monitor locks are not enforced. The client must voluntarily request monitor locks.
Service attributes are provided by services to help with discovery. The attributes are made available to clients during the discovery process. In C++, the attributes have the type std::map<std::string,RRValuePtr>
. The attributes follow the same type rules as varvalue{string}
. The attributes map must not contain any types defined in service definitions, since the client won't be able to unpack these types.
An example of using attributes for a service with root object type MyRobot
:
MyRobotPtr robot = boost::make_shared<MyRobot>(); std::map<std::string,RRValuePtr> attributes = { { "description", stringToRRArray("My awesome robot!") }, { "location", stringToRRArray("Robotics lab") } }; ServerContextPtr ctx = RobotRaconteurNode::s()->RegisterService("my_robot", "experimental.my_robot", robot); ctx->SetAttributes(attributes);
When service objects are returned from objref members, the service takes ownership of the object. If the service needs to release the object, it must be done explicitly using the RobotRaconteur::RobotRaconteurNode::ReleaseServicePath() function. This function takes the "service path" of the object to be released. See Service Paths for more information on service paths. The service path of an object can be determined using RobotRaconteur::IRRServiceObject, or using the function RobotRaconteur::ServerContext::GetCurrentServicePath(). When the service path is released, all connected clients are notified using an event. If the service path contains sensitive data such as a session token, the RobotRaconteur::ServerContext::ReleaseServicePath(boost::string_ref path, const std::vector<uint32_t>& endpoints) overload should be used. This version will only notify the clients specified in the endpoints
parameter.