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:
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