User Libraries

Integration Service defines three types of libraries, Transformation Library, Bridge Library, and Types Library. The users can implement all of them. The same library can mix all these libraries, including adding several libraries of the same kind (FIROS2’s TIS_NGSIv2 example mixes several transformation libraries in the same file).

See Configuration format for more information about how to indicate Integration Service which libraries to use.

Transformation Library

Integration Service allows defining custom transformation functions that a Connector will apply. Transformation functions are static functions that receive the input data, apply some transformation and store the result in the output data. The connector will be configured with the library and the function to call in each case. There is a static data prototype in resource/templatelib.cpp:

These functions must have one of the following interfaces:

Static Data

extern "C" USER_LIB_EXPORT void HelloWorldToKey(SerializedPayload_t* inputData, SerializedPayload_t* outputData)
{
    // Input HelloWorld Data
    HelloWorldPubSubType hwPubSub;
    HelloWorld hwData;
    hwPubSub.deserialize(inputData, &hwData);

    // Input key Data
    samplePubSubType keyPubSub;
    sample keyData;

    // Custom transformation
    keyData.index() = hwData.index() % 256;
    keyData.key_value() = hwData.index() % 256;

    // Serialize keys
    outputData->reserve(static_cast<uint32_t>(keyPubSub.getSerializedSizeProvider(&keyData)()));
    keyPubSub.serialize(&keyData, outputData);
}

Dynamic Data

extern "C" USER_LIB_EXPORT void HelloWorldToKey(DynamicData* inputData, DynamicData* outputData)
{
    // Custom transformation
    uint32_t temp = inputData->GetUint32Value(0);
    outputData->SetByteValue(temp % 256, 0);
    outputData->SetByteValue(temp % 256, 1);
}

In both cases, the transformation function parses the inputData, modifies it at will and stores the result into outputData. See the Dynamic Types example for some already working implementations.

Bridge Library

Bridge libraries are used to integrate new communication protocols. They must offer the following function declarations:

  • create_bridge:
extern "C" USER_LIB_EXPORT ISBridge* create_bridge(const char* name,
    const std::vector<std::pair<std::string, std::string>> *config)
{
    CustomBridge* bridge = new CustomBridge(name, config);
    return bridge;
}

As shown, the instantiated bridge must implement ISBridge. ISBridges are in charge of communicating readers with writers and apply transformation functions as defined in the Connector.

  • create_reader:
extern "C" USER_LIB_EXPORT ISReader* create_reader(ISBridge *bridge, const char* name,
    const std::vector<std::pair<std::string, std::string>> *config)
{
    CustomReader* reader = new CustomReader(name, config);
    return reader;
}

The reader returned must implement ISReader. ISReaders must be able to receive data from the input protocol.

  • create_writer:
extern "C" USER_LIB_EXPORT ISWriter* create_writer(ISBridge *bridge, const char* name,
    const std::vector<std::pair<std::string, std::string>> *config)
{
    CustomWriter* writer = new CustomWriter(name, config);
    return writer;
}

The writer returned must implement ISWriter. ISWriters must be able to send data to the destination protocol.

When the bridge stops, Integration Service will deallocate these objects from memory. If a node in the XML configuration file has at least one property, a vector of pairs of strings will be provided to the corresponding function (see Bridge configuration for more information).

If some functions want to use the default implementation (RTPS-Bridge), they must return nullptr. See Integration Service architecture section for more information about the interfaces that any Bridge Library must implement.

The responsibility of how to instantiate the bridge, writer, and reader is on the Bridge Library, but it’s important to remark that “RTPS” publishers and subscribers will be filled automatically by ISManager with the configuration from the <participant> node of the Fast-RTPS profiles. See the Adding new Bridges section for some already working implementations.

ISBridge

This component must communicate ISReaders with ISWriters, applying the transformation functions if any and its default implementation must be enough for the majority of cases. It can be seen as a connector manager as it is responsible for applying the data flow and the logic of each connector. A bridge can manage several connectors, and it should reuse readers, transformation functions, and writers if it’s possible. In complex configurations, like in this Example, several connectors can share the same readers, transformation functions, and writers.

Custom ISBridges must inherit from it:

class ISBridge
{
public:
    virtual void onTerminate();
    virtual void addReader(ISReader *sub);
    virtual void addFunction(const std::string &sub, const std::string &fname, userf_t func);
    virtual void addFunction(const std::string &sub, const std::string &fname, userdynf_t func);
    virtual void addWriter(const std::string &sub, const std::string &funcName, ISWriter* pub);
    virtual ISWriter* removeWriter(ISWriter* pub);
    virtual void on_received_data(const ISReader *sub, SerializedPayload_t *data);
    virtual void on_received_data(const ISReader *sub, DynamicData *data);
};

ISBridge.h and ISBridge.cpp implement the default behavior. There is no need to implement any function from any subclass but, if needed all these methods can be implemented. In that case, the implementation should be done carefully with the full functionality. It’s recommended to copy the standard implementation and modify it with your needs. After that, remove the unmodified methods. addFunction and on_received_data methods have two flavors, with static and dynamic data.

RTPS-Bridge

Integration Service has a default built-in RTPS-Bridge, but it allows creating custom bridges to connect new protocols implementing bridge libraries.

It implements a full ISBridge using Fast-RTPS publisher and subscriber. It allows communicating several subscribers with several publishers, establishing routes and applying transformation functions depending on each connector configuration.

The connector RTPS Connector uses this kind of bridge.

ISWriter

This component must be able to write data to the destination protocol. The default implementation uses a Fast-RTPS publisher.

class ISWriter
{
public:
    virtual bool write(eprosima::fastrtps::rtps::SerializedPayload_t* /*data*/) = 0;
    virtual bool write(eprosima::fastrtps::types::DynamicData* /*data*/) = 0;
    virtual ISBridge* setBridge(ISBridge *);
};

ISWriter doesn’t have a default implementation, so the built-in RTPS-Bridge provides the default behavior. Any custom bridge that needs to define its writer must implement at least both write methods. If one of them isn’t needed, implement it as follows:

bool write([...]) override { return false; }

The user must be sure that this method’s version never will be called.

ISReader

This component is in charge of receive data from the input protocol. Its default implementation uses a Fast-RTPS subscriber.

class ISReader
{
public:
    virtual void addBridge(ISBridge* bridge);
    virtual void on_received_data(eprosima::fastrtps::rtps::SerializedPayload_t* payload);
    virtual void on_received_data(eprosima::fastrtps::types::DynamicData* data);
};

ISReader doesn’t have a default implementation, so the built-in RTPS-Bridge provides the default behavior. Any custom bridge that needs to define its reader must implement their reading method that must call at least one of the on_received_data methods.

void custom_read(void *data) // Suppose our source protocol call us directly, so custom_read is a callback.
{
	CustomData receivedData = parse(data); // The received data may need to be parsed.
	CustomDataPubSubType datapst; // If our CustomData was generated by FastRTPSGen, we can use its PubSubType too.
	SerializedPayload_t payload;
	datapst.serialize(&payload, &receivedData); // And retrieve a SerializedPayload_t from it.
	// Once we have the serialized payload, we can call our on_received_data method with its default implementation.
	on_received_data(&payload);
}

If one of them isn’t needed, implement it as follows:

void on_received_data([...]) override { }

Types Library

Integration Service allows defining types libraries to create custom data types. These libraries must offer a function with the following declaration:

extern "C" USER_LIB_EXPORT TopicDataType* GetTopicType(const char *name);

It receives the name of the TopicType and must return an instance of it (subclass of Fast RTPS’s TopicDataType). If the provided type is unknown, the function must return nullptr.

extern "C" USER_LIB_EXPORT TopicDataType* GetTopicType(const char *name)
{
    if (strncmp(name, "HelloWorld", 11) == 0)
    {
        return new HelloWorldPubSubType();
    }
    return nullptr;
}

The returned type can be built using Fast-RTPS dynamic types, using an already generated IDL statically or implementing it directly as a TopicDataType subclass.

extern "C" USER_LIB_EXPORT TopicDataType* GetTopicType(const char *name)
{
    if (strncmp(name, "HelloWorld", 11) == 0)
    {
        // Create basic types
        DynamicTypeBuilder_ptr created_type_ulong = DynamicTypeBuilderFactory::GetInstance()->CreateUint32Builder();
        DynamicTypeBuilder_ptr created_type_string = DynamicTypeBuilderFactory::GetInstance()->CreateStringBuilder();
        DynamicTypeBuilder_ptr struct_type_builder = DynamicTypeBuilderFactory::GetInstance()->CreateStructBuilder();

        // Add members to the struct.
        struct_type_builder->AddMember(0, "index", created_type_ulong.get());
        struct_type_builder->AddMember(1, "message", created_type_string.get());
        struct_type_builder->SetName("HelloWorld");

        DynamicType_ptr dynType = struct_type_builder->Build();
        DynamicPubSubType *psType = new DynamicPubSubType(dynType);
        return psType;
    }
    return nullptr;
}

In section Dynamic Data Integration you can find an already working example.