How to create a new library¶
This guide describes how to use BlockFactory for creating a new toolbox Toolbox Example. It will contain a single block Signal math with the following specifications:
- Accepts two input signals
- Performs element-wise operations: sum, subtraction, multiplication
- Allows to select the operation with a user friendly GUI (mask)
- Produces an output signal with the result of the operation
Despite it is a very trivial example, it shows how to structure both the C++ and the Matlab components of a toolbox. From this starting point is then possible to build more complex architectures which might need e.g. to be split in many parts or to link against third-party libraries.
Toolbox Example project
You can find the files of this example in the example
folder.
Introduction¶
Before jumping in the example of this tutorial, in this section you will find helpful information useful to grasp the key ideas about the toolbox and a refreshing of common terms and patterns used in programming.
Algorithm specifications¶
The execution of a generic algorithm can be split in the following basic phases:
- Configuration
- Initialization
- Step
- Termination
In the configuration phase the algorithm can, for instance, read parameters and specify details about its inputs and outputs. During the initialization it might need to allocate resources. When everything is ready, the simulation starts and on every cycle of the loop the algorithm computes a step. Before finishing the simulation, in the termination step the resources that are not anymore needed can be deallocated and final operations can be executed.
Terminology¶
There are few key components which are part of the core infrastructure, and it is important they are clear from the very beginning.
Note
This project has strong roots with Simulink. Despite it is not anymore strictly related to it, the structure keeps as inheritance its terminology.
Block¶
The Block is the main component of the toolbox. You may think of it as a wrapper for a generic algorithm. It provides support of all the phases discussed above.
Port¶
Blocks need to interface with the outside for reading input data and writing their results. Ports are attached to the Block and let data flow in and out from it. They have properties like the accepted data type and they are characterized by a size.
Signal¶
A Signal is the generalization of a buffer. It can be plugged to multiple Ports and allows sharing data between them. Similarly to Ports, a Signal has a data type and a width. When a Signal is connected to a Port, their dimension must match.
Engine¶
The engine is the framework that calls and executes the code implementing the Blocks. We currently provide support for Simulink and Simulink Coder. Alternative engines might be Scilab or even standalone C++ code.
BlockInformation¶
BlockInformation is the interface between a Block and the engine that executes its code. Despite blocks are not aware of what engine is executing them, blocks and engine need to exchange information such as user parameters and Port data. BlockInformation provides methods for setting and getting this piece of information.
Simulink Block¶
A Simulink Block is the wrapper of a (C++) Block. It provides a visual representation of it, with input and output ports and gives the possibility to connect it with other Simulink blocks through signals. The terms above come from this representation and map it to C++.
Simulink Model¶
A Simulink Model is a project composed of a set of Simulink Blocks interconnected by Signals.
Simulink Library¶
A Simulink Library is a container of a set of Blocks that populates the Simulink Library Browser. Every toolbox must be shipped with an associated Simulink Library file in order to expose its Blocks.
S-Function¶
There are many types of functions for implementing an algorithm wrapped by a Simulink Block. In Matlab terminology, these functions are referred as S-Functions. You can read more about the supported types in What type of S-Function should you use.
In short S-Functions provide a sort of interface (through C callbacks) where a given number of functions need to be implemented. Simulink knows the name of these functions and calls them on demand.
Block Mask¶
A Simulink Block is just a square with input and output ports. It is possible to "mask" a Simulink Block in order to provide a user-friendly GUI that can be useful e.g. for setting Block parameters. The mask may contain buttons, sliders, check boxes, dropdown menus, etc.
Software library¶
A library is a file containing compiled code (functions, classes, constants, etc.) which cannot be executed standalone. It can be either static or dynamic. Static libraries are meant to be embedded inside the executable that calls their symbols, instead the code of dynamic libraries (also called shared libraries) is only referenced inside the executable and called when needed.
To grasp better this difference, if multiple executables link against the same static library, the same code is embedded inside all of them, resulting in bigger executables and code duplication. A dynamic library object instead can be shared by multiple executables that need only to know its location in the filesystem and which symbols it provide.
Info
You can find more detailed information about software libraries and linkers at this link.
Plugin Library¶
A plugin library is a particular type of a dynamic library. An executable can load dynamic libraries either at load time, i.e. when the executable is launched, or at run time, i.e. when needed during the execution. The libraries that can be loaded during run time are referred as plugins.
Info
On UNIX systems the load of a plugin is executed by a dlopen
call
Overview of Simulink execution¶
BlockFactory provides a framework capable of loading during runtime shared libraries (plugins). When the Simulink simulation starts, its engine assigns a deterministic order to the blocks of the Simulink Model. If one of these blocks is not contained in the default toolboxes provided by Mathworks, it needs to be associated to a S-Function that implements its logic. The toolbox provides a streamlined approach to implement these functions without the need of being an expert of the complex Simulink APIs.
The blocks of our library are implemented in Simulink using the following S-Function block:
For the time being, what you need to know is that the name of the plugin library generated from the C++ code will be stored in the S-function parameters field of the block mask. We provide a generic Level-2 MEX S-Function that reads this parameter and uses the library name name to dynamically load the dynamic library that contains the block's code.
After the library is found in the filesystem and successfully loaded, Simulink allocates an object of the C++ class associated to the block functionality. Again, this information (the class name) is passed in the S-function parameters field of the block mask.
Develop the C++ plugin¶
You already learned that Blocks are no more than regular C++ classes. They are not an exception, in fact all the components discussed until now are mapped to C++ classes or interfaces (abstract classes).
The following resources provide further information about them:
- Blocks are implementations of the
blockfactory::core::Block
interface - BlockInformation is defined in the
blockfactory::core::BlockInformation
interface - Signals are mapped to the
blockfactory::core::Signal
class - Blocks parameters are mapped to the
blockfactory::core::Parameter
class
We need the following folder structure for the C++ project. Create already the empty files so the project can compile from the very first attempt.
.
├── CMakeLists.txt
├── include
│ └── SignalMath.h
└── src
├── Factory.cpp
└── SignalMath.cpp
Info
All the path of this tutorial will be relative to the root folder of the project. This means that if the directory tree above is stored in /home/foo/tutorial
, when you read to go the ./build
directory it means /home/foo/tutorial/build
.
Tip
Bear in mind that this C++ class is independent from Simulink. Potentially, it can be called by a standalone C++ executable.
Tip
It is not mandatory to implement a new class for every Simulink Block. If they share a lot of code, a single class can be referenced by multiple Simulink Blocks and its behavior can be selected using parameters.
Tip
You can use all the external libraries you want to implement the code of this class.
CMake project for compiling the library¶
You are free to use your favourite tool for compiling the project. We recommend CMake. If you are not an expert user of this tool, just follow the steps. The comments in the file should be enough to understand what it is happening.
Fill the file CMakeLists.txt
with the following content:
cmake_minimum_required(VERSION 3.5)
project(ExampleToolbox LANGUAGES CXX VERSION 0.1)
# C++ standard
set(CMAKE_CXX_STANDARD 14)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# Export all symbols in Windows
set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON)
# Utility for defining install folders
include(GNUInstallDirs)
# Tweak linker flags in Linux.
# Matlab is very strict on missing symbols and by default ld do not warn if
# something is missing.
if(UNIX AND NOT APPLE)
get_filename_component(LINKER_BIN ${CMAKE_LINKER} NAME)
if(${LINKER_BIN} STREQUAL "ld")
set(CMAKE_SHARED_LINKER_FLAGS "-Wl,--unresolved-symbols=report-all")
endif()
endif()
# ===========
# C++ LIBRARY
# ===========
# Find the needed BlockFactory components:
# - "Core" contains the core classes such as Block and Signal
# - "Simulink" is required at runtime for loading the library from Simulink
find_package(BlockFactory
REQUIRED COMPONENTS Core
OPTIONAL_COMPONENTS Simulink)
# Create the plugin library. This must be a SHARED library.
add_library(ExampleToolbox SHARED
include/SignalMath.h
src/SignalMath.cpp
src/Factory.cpp)
# Manually set the name of the output library. This is not required and it
# is done only for sake of clarity.
set_target_properties(ExampleToolbox PROPERTIES
OUTPUT_NAME "ExampleToolbox")
# Link the library with the Core component containing the core classes
target_link_libraries(ExampleToolbox PRIVATE
BlockFactory::Core)
# Setup the include directories
target_include_directories(ExampleToolbox PRIVATE
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>)
Note
If your library needs to link against other libraries, use find_package
to load their targets and then add them to the target_link_libraries
directive.
Compiling instructions
From the root folder of the project, execute:
mkdir build
cd build
cmake ..
cmake --build .
mkdir build
cd build
cmake ..
cmake --build . --config Release
You should now find in the ./build
directories a new library file, which depending on your OS is:
libExampleToolbox.so
on LinuxlibExampleToolbox.dylib
on macOSExampleToolbox.dll
on Windows
This is the toolbox's plugin library which is loaded during runtime by the active Engine.
Implement the block logic¶
The only Signal math block of our new toolbox will be implemented in a example::SignalMath
C++ class.
Header¶
Here below the ./include/SignalMath.h
header.
Given the simple logic it should be straightforward to understand. The class inherits from the blockfactory::core::Block
interface and implements some of its methods.
Info
The only mandatory method to implement is the blockfactory::core::Block::output
. By default the other methods are dummy and they always return true
.
#ifndef EXAMPLE_SIGNALMATH_H
#define EXAMPLE_SIGNALMATH_H
#include <BlockFactory/Core/Block.h>
#include <BlockFactory/Core/BlockInformation.h>
#include <memory>
#include <string>
namespace example {
class SignalMath;
} // namespace example
class example::SignalMath : public blockfactory::core::Block
{
private:
enum class Operation
{
ADDITION,
SUBTRACTION,
MULTIPLICATION,
};
Operation m_operation;
public:
static const std::string ClassName;
SignalMath() = default;
~SignalMath() override = default;
unsigned numberOfParameters() override;
bool parseParameters(blockfactory::core::BlockInformation* blockInfo) override;
bool configureSizeAndPorts(blockfactory::core::BlockInformation* blockInfo) override;
bool initialize(blockfactory::core::BlockInformation* blockInfo) override;
bool output(const blockfactory::core::BlockInformation* blockInfo) override;
bool terminate(const blockfactory::core::BlockInformation* blockInfo) override;
};
#endif // EXAMPLE_SIGNALMATH_H
The ./src/SignalMath.cpp
file is more interesting, let's split it in chunks.
Parameters¶
If you recall, BlockInformation is used by the Block for interfacing with the Engine. When this code is executed inside Simulink, the blockfactory::core::SimulinkBlockInformation
implementation will perform the required operations calling Simulink APIs.
#include "SignalMath.h"
#include <BlockFactory/Core/Log.h>
#include <BlockFactory/Core/Parameter.h>
#include <BlockFactory/Core/Signal.h>
using namespace example;
unsigned SignalMath::numberOfParameters()
{
// The base blockfactory::core::Block class needs parameters (e.g. the ClassName).
// You must specify here how many more parameters this class needs.
// Our example needs just one more: the operation to perform.
return Block::numberOfParameters() + 1;
}
// This method should let BlockInformation know the parameters metadata.
// BlockFactory will use this information to gather the parameters from the active engine.
bool SignalMath::parseParameters(blockfactory::core::BlockInformation* blockInfo)
{
// Initialize information for our only parameter
int rows = 1;
int cols = 1;
std::string name = "Operation"; // This label is used later to access the paramemeter
unsigned index = Block::numberOfParameters(); // Indices start from 0
auto type = blockfactory::core::ParameterType::STRING;
// Create the parameter
blockfactory::core::ParameterMetadata parameterMetadata(type, index, rows, cols, name);
// Add the parameter metadata into the BlockInformation
if (!blockInfo->addParameterMetadata(parameterMetadata)) {
bfError << "Failed to store parameter metadata";
return false;
}
// Ask to the BlockInformation interface to parse the parameters and store them into
// the protected m_parameters member of the parent blockfactory::core::Block class.
bool paramParsedOk = blockInfo->parseParameters(m_parameters);
// Return the outcome of the parameter parsing.
// If the parsing fails, the execution stops.
return paramParsedOk;
}
Configuration¶
The configuration of the Block is performed in the following steps:
- The base class needs to be configured. It needs some parameters (e.g. the class name and the library name) and this call asks the Engine to parse them.
- The ports of the Block need to be defined. In this example the size is set as dynamic so that it accepts signals with any width.
- The data is then sent to the Engine through the BlockInformation interface.
Info
If needed, parameters can be accessed from this step. Refer to the initialization phase to understand how to gather them.
Signal size
Simulink has the support of inheriting the port size from the signal size, though use this feature only when strictly needed. In complex Simulink Models the Engine might have difficulties to execute this size propagation, and fixing the Port sizes provides helpful constraints (especially for blocks with only outputs)
Important
Be careful on memory allocations during this step. A temporary object is created only for configuration means, and then destroyed. All the allocated memory will be hereby deleted.
bool SignalMath::configureSizeAndPorts(blockfactory::core::BlockInformation* blockInfo)
{
// The base blockfactory::core::Block class needs to be configured first
if (!blockfactory::core::Block::configureSizeAndPorts(blockInfo)) {
return false;
}
// Create object that store input and output ports information
blockfactory::core::Port::Info input1{/*portIndex=*/0,
std::vector<int>{blockfactory::core::Port::DynamicSize},
blockfactory::core::Port::DataType::DOUBLE};
blockfactory::core::Port::Info input2{/*portIndex=*/1,
std::vector<int>{blockfactory::core::Port::DynamicSize},
blockfactory::core::Port::DataType::DOUBLE};
blockfactory::core::Port::Info output{/*portIndex=*/0,
std::vector<int>{blockfactory::core::Port::DynamicSize},
blockfactory::core::Port::DataType::DOUBLE};
// Store together the port information objects
blockfactory::core::InputPortsInfo inputPortInfo;
blockfactory::core::OutputPortsInfo outputPortInfo;
inputPortInfo.push_back(input1);
inputPortInfo.push_back(input2);
outputPortInfo.push_back(output);
// Store the port information into the BlockInformation
if (!blockInfo->setPortsInfo(inputPortInfo, outputPortInfo)) {
bfError << "Failed to configure input / output ports";
return false;
}
return true;
}
Initialization¶
Info
Starting from this step, memory persistence is guaranteed.
In the initialisation step, the input parameter that defines the operation is parsed. In this example the parameter is passed as a string. In the header a new enum class Operation
was defined and here the related private member is properly initialised. Additional checks can be added, i.e. testing that both ports have the same width given that a dynamic size was previously assigned.
Info
In this case there's no need to allocate memory. If your class has buffers to initialise, this is the right time to do it.
bool SignalMath::initialize(blockfactory::core::BlockInformation* blockInfo)
{
// The base blockfactory::core::Block class need to be initialized first
if (!Block::initialize(blockInfo)) {
return false;
}
// Parse the parameters
if (!SignalMath::parseParameters(blockInfo)) {
bfError << "Failed to parse parameters.";
return false;
}
// Read the Operation parameter and store it as a private member
std::string operation;
if (!m_parameters.getParameter("Operation", operation)) {
bfError << "Failed to parse Operation parameter";
return false;
}
// Check the content of the parameter
if (operation == "Addition") {
m_operation = Operation::ADDITION;
}
else if (operation == "Subtraction") {
m_operation = Operation::SUBTRACTION;
}
else if (operation == "Multiplication") {
m_operation = Operation::MULTIPLICATION;
;
}
else {
bfError << "Operation " << operation << " not recognized";
return false;
}
// Check that the size of the input signals match
if (blockInfo->getInputPortWidth(/*index=*/0) != blockInfo->getInputPortWidth(/*index=*/1)) {
bfError << "Input signals widths do not match";
return false;
}
return true;
}
Output¶
The output
method is where the real algorithm is implemented. The Signals are firstly gathered from the Engine using their index. The classes blockfactory::core::InputSignalPtr
and blockfactory::core::OutputSignalPtr
are particular typedef
of the blockfactory::core::Signal
class and they have the same methods. In the for
loop the configured operation is performed and the result stored in the output signal.
Note
Note that input signals are read-only and accessed in C++ as const
object. You can store data only to the output signals objects.
bool SignalMath::output(const blockfactory::core::BlockInformation* blockInfo)
{
// Get the input signals
blockfactory::core::InputSignalPtr input1 = blockInfo->getInputPortSignal(/*index=*/0);
blockfactory::core::InputSignalPtr input2 = blockInfo->getInputPortSignal(/*index=*/1);
// Get the output signal
blockfactory::core::OutputSignalPtr output = blockInfo->getOutputPortSignal(/*index=*/0);
// Check the signal validity
if (!input1 || !input2 || !output) {
bfError << "Signals not valid";
return false;
}
// Check the width of the output signal.
// This check is recommended for dynamically sized signals since the engine might
// fail to propagate the right dimensions.
if (output->getWidth() != input1->getWidth()) {
bfError << "Output signal has a width of " << output->getWidth()
<< " while input signals have a width of " << input1->getWidth();
return false;
}
// Perform the given operation
for (size_t i = 0; i < output->getWidth(); ++i) {
switch (m_operation) {
case Operation::ADDITION:
output->set(i, input1->get<double>(i) + input2->get<double>(i));
break;
case Operation::SUBTRACTION:
output->set(i, input1->get<double>(i) - input2->get<double>(i));
break;
case Operation::MULTIPLICATION:
output->set(i, input1->get<double>(i) * input2->get<double>(i));
break;
}
}
return true;
}
Terminate¶
Given the simplicity of our Block, the terminate
step is a dummy implementation. This method is reported just for the sake of clarity. It can be omitted since blockfactory::core::Block::terminate
already provides the same dummy implementation.
bool SignalMath::terminate(const blockfactory::core::BlockInformation* /*blockInfo*/)
{
return true;
}
Implement the plugin factory¶
A plugin library usually contains multiple classes used for multiple Blocks. The sharedlibpp
tool for plugins requires declaring what classes are part of the plugin. This operation is done in the ./include/Factory.cpp
file:
#include "SignalMath.h"
// Class factory API
#include <shlibpp/SharedLibraryClassApi.h>
// Add the example::SignalMath class to the plugin factory
SHLIBPP_DEFINE_SHARED_SUBCLASS(SignalMath, example::SignalMath, blockfactory::core::Block);
The SHLIBPP_DEFINE_SHARED_SUBCLASS
macro needs the following three arguments:
- A label used to extract the class from the plugin library
- The class of the block
- The base class of the block
The only interesting part here is the label. Keep this name in mind because we need to know it later in the Simulink section.
If everything was properly done, the library is now ready and can be compiled again:
cd build
cmake --build .
Tip
Simulink will open this library at the beginning of the simulation loop and it needs to be found in the filesystem. The BlockFactory search path can be configured by setting the BLOCKFACTORY_PLUGIN_PATH
environment variable:
export BLOCKFACTORY_PLUGIN_PATH=<build folder absolute path>
$env:BLOCKFACTORY_PLUGIN_PATH = "<build folder absolute path>";
Matlab and Simulink¶
Once the C++ library is ready, the class can be wrapped by a Simulink Block. If, as in this case, there's no existing Simulink Library to which the new block can be added, some extra step to create a new one is necessary.
We're going to store the files discussed in this section in the ./matlab
folder, obtaining at the end the following project structure:
.
├── CMakeLists.txt
├── include
│ └── SignalMath.h
├── matlab
│ ├── ExampleToolbox.slx
│ ├── Model.mdl
│ └── slblocks.m
└── src
├── Factory.cpp
└── SignalMath.cpp
ExampleToolbox.slx
is the Simulink Library containing our new Signal Math Block.slblocks.m
is a m-script necessary for loading external Simulink Libraries into the Simulink Library Browser .Model.mdl
is a Simulink Model for testing the Block.
Create an new Simulink Library¶
The first step is creating a new Simulink Library. Open the Simulink Start Page and create a Blank Library. Save it in ./matlab/ExampleToolbox.slx
.
In order to populate the Simulink Library Browser, you need to create a slblocks.m
file with the following content:
function blkStruct = slblocks
Browser.Library = 'ExampleToolbox';
Browser.Name = 'Example Toolbox';
Browser.IsFlat = 0;
blkStruct.Browser = Browser;
As explained in the official documentation, we also need to modify a property of the library file:
>> set_param('ExampleToolbox','EnableLBRepository','on');
If you followed these steps, the new library should be ready. Be sure that the ./matlab
folder is in the Matlab path
, then open the Simulink Library Browser and press F5. You should now see the empty Example Toolbox entry.
Note
In order to add a folder to the path
, you can either browse it from the Matlab tree view making it the current folder, or executing addpath('/path/of/the/folder')
. Find more details at What Is the MATLAB Search Path?.
Info
Every time you close the library, Matlab locks it. Press the lock icon in the bottom left corner to unlock the library.
Add a block¶
Open the library and add a new S-Function block with the following parameters:
- The S-function name is the name of the generic S-Function that BlockFactory provides
- The S-function parameters are the parameters passed to the S-Function. The first two are required by
blockfactory::core::Block
, and the third one is the parameter of ourexample::SignalMath
class:'SignalMath'
is the label we used inFactory.cpp
when we registered our class.'ExampleToolbox'
is the name of the plugin library. It must match theOUTPUT_NAME
we assigned to the target inCMakeLists.txt
.'Addition'
is the kind of operation we want this block to perform.
Press OK and save the library. If you insert wrong information, like a wrong number of parameters or parameters with a wrong type or content, the bfError
s we added in the C++ class should provide more details (try it, a new popup error message should open).
Create a test Simulink Model¶
Now it's time for finally testing all our work. Create a new Blank Model and populate it with the following blocks:
Then press the Play icon, and the Display connected to the block output should show the result of the addition.
Create a block mask¶
The type of the operation is defined as a parameter of the S-Function block. It is not very intuitive changing it in this way. Beyond the effort of changing the string, we should remember exactly what parameter the block accepts.
This limitation can be overcome by masking the block, that means providing it a GUI. Right-click the block from the Example Toolbox library and press Mask > Create Mask
.
In the Icon & Ports
tab, fill the Icon drawing commands
with:
disp('Signal Math')
port_label('input',1,'Input 1')
port_label('input',2,'Input 2')
port_label('output',1,'Output')
Then, in the Parameters & Dialog
tab, add a Popup
and fill it obtaining the following status:
Note
Note that we unchecked the Evaluate
attribute. When a Popup is not evaluated, its associated variable (operation
in this case) will contain the string of the selected option. Instead, if it is evaluated, it contains a 0-based index.
Save the library and substitute the new Block in the Simulink Model. You can now select the operation double-clicking the block and changing the popup menu entry.
Final comments¶
BlockFactory provides great abstraction capabilities for wrapping C and C++ algorithms to Simulink Blocks. You don't need to be a Simulink expert for this kind of operation, all the machinery is hidden under the hood. Possibilities are endless.
We are eager to hear your feedback! If you find bugs or want to propose enhancements, please fill a new Issue.