Developer Guide

The following sections explain, how to consume the Vector SIL Kit library in your own application and how to use the SIL Kit API to communicate with other participants of a simulation.

Getting the SIL Kit

Precompiled SIL Kit packages are regularly released on GitHub (https://github.com/vectorgrp/sil-kit/releases). If you need to build the library yourself, take a look at the readme file of the repository’s root folder.

Architecture

The SIL Kit implements a layered architecture:

../_images/SilKitArchitecture.svg

Prerequisites for Compilation

  • For Windows:
    • Visual Studio 2017 (toolset v141) and higher

  • For Ubuntu 18.04 LTS:
    • GCC 8 or

    • Clang

Writing your first SIL Kit application

This tutorial assumes that you are familiar with CMake (https://cmake.org) and C++.

Using the SIL Kit package

The SIL Kit distribution contains a self-contained and deployable installation in the SilKit directory. The CMake build configuration required is exported to SilKit/lib/cmake/SilKit and defines the SilKit::SilKit target.

From CMake this can be consumed via the find_package(SilKit CONFIG) mechanism. For example, the following CMakeLists.txt imports the SIL Kit library based on its file system path.

Project(SampleSilKit)
cmake_minimum_required(VERSION 3.0)
set(CMAKE_BUILD_TYPE Debug)
find_package(SilKit 
    REQUIRED CONFIG
    PATHS "${CMAKE_CURRENT_LIST_DIR}/SilKit"
)
find_package(Threads REQUIRED)
add_executable(${PROJECT_NAME} simple.cpp)
target_link_libraries(${PROJECT_NAME} PRIVATE SilKit::SilKit Threads::Threads)

Properties, like include directories and compile flags, are automatically handled by the imported target. If you use another method to build your software you can directly use the SilKit/include and SilKit/lib directories for C++ headers and libraries.

A simple Publish / Subscribe application

We’ll create a simple, self-contained SIL Kit application that uses Publish/Subscribe to exchange user-defined data between two participants. In our C++ file simple.cpp, we include the headers and define namespaces and constants:

#include <iostream>
#include <thread>

#include "silkit/SilKit.hpp"
#include "silkit/services/all.hpp"
#include "silkit/services/orchestration/string_utils.hpp"

using namespace std::chrono_literals;

const auto registryUri = "silkit://localhost:8500";

SIL Kit participants are created with a configuration that is used to change certain aspects of the simulation without recompiling the application.

Note

SIL Kit applications should be able to run without a configuration file. However, applications should allow users to provide one in case they want to reconfigure the behavior of the application (see The Participant Configuration File for details).

This can be done by loading an existing YAML file. Here, we use the configuration file simple.yaml to configure a logger that logs all error messages to a file:

Logging:
  Sinks:
  - Type: File
    Level: Warn
    LogName: SimpleParticipantLog

We load it in the main function of our code:

int main(int argc, char** argv)
{
    auto config = SilKit::Config::ParticipantConfigurationFromFile("simple.yaml");
    // TODO: Use config to create participants
}

The application will run two participants concurrently, each in its own thread. One thread will act as a publisher by sending a test string to its subscribers:

void publisher_main(std::shared_ptr<SilKit::Config::IParticipantConfiguration> config)
{
    auto participant = SilKit::CreateParticipant(config, "PublisherParticipant", registryUri);

    auto* lifecycleService =
        participant->CreateLifecycleService({SilKit::Services::Orchestration::OperationMode::Coordinated});

    SilKit::Services::PubSub::PubSubSpec pubSubSpec{"DataService", "text/plain"};
    auto* publisher = participant->CreateDataPublisher("PublisherController", pubSubSpec);

    auto* timeSyncService = lifecycleService->CreateTimeSyncService();
    timeSyncService->SetSimulationStepHandler(
        [publisher](std::chrono::nanoseconds now, std::chrono::nanoseconds /*duration*/) {
            // Generate some data
            static auto msgIdx = 0;
            std::string message = "DataService Msg" + std::to_string(msgIdx++);
            std::vector<uint8_t> data{message.begin(), message.end()};

            // Publish the raw bytes of the message to all subscribers
            publisher->Publish(std::move(data));

            // Delay the simulation
            std::this_thread::sleep_for(1s);
        },
        1ms);

    try
    {
        // Run the simulation main loop until stopped by the sil-kit-system-controller
        auto result = lifecycleService->StartLifecycle();
        std::cout << "Publisher: result: " << result.get() << std::endl;
    }
    catch (const std::exception& e)
    {
        std::cout << "ERROR: Publisher exception caught: " << e.what() << std::endl;
    }
}

Initially, the simulation is joined by creating the participant called “PublisherParticipant”. This properly initializes the SIL Kit library; enables the instantiation of Services and offers access to the Lifecycle Service, which controls the orchestration of our simulation. Next, we create a publisher for the DataService topic. Later, we subscribe to the same topic name in our subscriber to enable communication between the participants. The actual simulation is performed in the simulation task. This is a callback that is executed by the SIL Kit runtime whenever the simulation time advances. This callback has to be registered with the time synchronization service’s SetSimulationStepHandler(). We hand over the publisher object in the capture list of our simulation task and use it to send data through its Publish() method.

The subscriber runs in its own thread, too:

void subscriber_main(std::shared_ptr<SilKit::Config::IParticipantConfiguration> config)
{
    auto participant = SilKit::CreateParticipant(config, "SubscriberParticipant", registryUri);

    auto* lifecycleService =
        participant->CreateLifecycleService({SilKit::Services::Orchestration::OperationMode::Coordinated});

    SilKit::Services::PubSub::PubSubSpec pubSubSpec{"DataService", "text/plain"};
    auto receptionHandler = [](auto* subscriber, const auto& dataMessageEvent) {
        std::string message{dataMessageEvent.data.begin(), dataMessageEvent.data.end()};
        std::cout << " <- Received data=\"" << message << "\"" << std::endl;
    };
    auto* subscriber = participant->CreateDataSubscriber("SubscriberController", pubSubSpec, receptionHandler);

    auto* timeSyncService = lifecycleService->CreateTimeSyncService();
    timeSyncService->SetSimulationStepHandler(
        [](std::chrono::nanoseconds /*now*/, std::chrono::nanoseconds /*duration*/) {
            // Simulation task must be defined, even an empty one
        },
        1ms);

    try
    {
        // Run the simulation main loop until stopped by the sil-kit-system-controller
        auto result = lifecycleService->StartLifecycle();
        std::cout << "Subscriber: result: " << result.get() << std::endl;
    }
    catch (const std::exception& e)
    {
        std::cout << "ERROR: Subscriber exception caught: " << e.what() << std::endl;
    }
}

The setup is similar to the publisher, except that we instantiate a subscriber interface. This allows us to register a SetDataMessageHandler() callback to receive data value updates. The simulation task has to be defined, even though no simulation work is performed.

We extend our main function to spawn both threads and join them again once finished. Also, we use a try-catch block here to get proper error handling e.g., if the configuration file cannot be loaded.

int main(int argc, char** argv)
{
    try
    {
        // Load the YAML configuration
        auto config = SilKit::Config::ParticipantConfigurationFromFile("simple.yaml");

        // Launch the participant threads
        std::thread publisher{publisher_main, config};
        std::thread subscriber{subscriber_main, config};

        // Once finished, close the threads
        if (subscriber.joinable())
            subscriber.join();
        if (publisher.joinable())
            publisher.join();
    }
    catch (const std::exception& e)
    {
        std::cout << "ERROR: Exception caught: " << e.what() << std::endl;
    }

    return 0;
}

The application is built with CMake on the command line (from a build directory) by calling cmake .. to generate and then build via cmake --build .. A more convenient way is to open the folder in an IDE with CMake support. To run this sample, copy the shared library files (e.g., on Windows the SilKit.dll, SilKitd.dll from SilKit/bin) and the simple.yaml next to the compiled executable.

Running the simulation

Our sample needs the utility processes sil-kit-registry and sil-kit-system-controller to run. The registry is required for participant discovery. The sil-kit-system-controller takes the participant names as command line arguments, initializes the connected participants and starts the simulation until the return key is pressed. For convenience and to reduce code duplication, these utility programs are implemented in separate executables and distributed in binary forms.

The final simulation setup can be run through the following commands:

# Start the Middleware Registry
./sil-kit-registry.exe

# Start the System Controller and tell it to wait for PublisherParticipant and SubscriberParticipant
./sil-kit-system-controller.exe PublisherParticipant SubscriberParticipant

# Start the application running the two participants
# Make sure that the SilKit.dll and simple.yaml are available
./SampleSilKit.exe

The complete source code of this sample can be found here: CMakeLists.txt simple.cpp simple.yaml

Platform support

SIL Kit provides three tiers of platform support

Table 1 Support Tiers

Tier 1

Essential targets. Continuously tested and official binary packages are provided.

Tier 2

Officially supported targets. Continuously tested but NO binary packages are provided

Tier 3

Targets for which we have (limited) build support but which
are not continuously tested and no packages are provided

A platform is hereby defined by the combination of the used operating system (OS), the CPU architecture (eg. x86 or ARM64) and the compiler/toolchain used. For example, Ubuntu 20.04 x86_64 Clang 10.

SIL Kit should compile and run on any POSIX platform. If you have feedback for different targets or platforms not listed here, please report them using GitHub Issues. Thanks!

A target may be upgraded to Tier 1 once we have continuous testing for it in place and we have binary packages available for it.

Tier 1: Official Packages

Essential targets. Automatically tested and provided as official binary packages.

Table 2 Platform Support

OS

Architecture

Notes

Windows

64bit (x86_64)

MSVC 19 with Toolset 14.1

Windows

32bit (x86)

MSVC 19 with Toolset 14.1

Ubuntu 18.04

amd64

GCC 8

Ubuntu 20.04

amd64

Clang 10, .deb

Tier 2: CI Support

Officially supported and automatically tested. NO binary packages provided

Table 3 Platform Support

Platform

Architecture

Notes

Windows

64bit (x86_64)

MSYS2/Mingw: GCC 14

Ubuntu 22.04

amd64

GCC 11/Clang 18
+ Address Sanitizer
+ Undefined Behaviour Sanitizer
+ Thread Sanitizer

Ubuntu 22.04

ARM64

Clang 18

MAC OS

ARM64/M1

AppleClang 15

Tier 3: Known to build

Build and tested by individual contributors or users. Since these are not part of the CI pipeline, compatibility with these platforms can be broken at any time!

Table 4 Platform Support

Platform

Architecture

Notes

Ubuntu 24.04

amd64

GCC 13
Clang 18

QNX 7.1 RTOS

X86 64bit

QNX GCC 8

FreeBSD 14

X86 64bit

FreeBSD Clang 18

Android

ARM64

NDK builds with default compiler, libc++_shared