C++ Plug-in Support Library

The primary API for communication plug-ins leans on the C programming language to ensure binary compatibility across different compiler vendors. It also allows, at least in principle, to write a plug-in in any language that provides a foreign function interface (FFI) to C. However, working directly with the C API can quickly become unwieldy and error-prone. In addition, it is difficult to write tests for such a plug-in.

To avoid these problems, the provided SDK includes a support library which provides a higher level C++ API on top. The support library translates the handle types of the C API (for example, MVValue_t, MVRecipe_t, MVResult_t) either directly into C++ vocabulary types, such as std::variant in case of MVValue_t, or into aggregate types, such as MVPlugin::RecipeData or MVPlugin::Result, which support value semantics. Further, the handling of failures through error codes returned from the C API functions is replaced by exception handling to promote branchless control flow.

The support library is "header-only" and implemented in terms of the underlying C API. Hence, plug-in developers targeting C++17 or above can choose to use it without incurring a dependency on any particular C++ runtime. Consequently, only the version of the support library that is installed as part of the SDK matters when building the plug-in, whereas the runtime compatibility of the resulting plug-in binary with different versions of MERLIC is still exclusively governed by the primary C API.

The support library may undergo API breaks in future versions to incorporate new features or to fix defects. In this case, plug-in developers might need to perform (usually small) changes to their plug-in code when they update to a newer version of the plug-in development SDK.

Building a Plug-in Using the Support Library

The C++ support library headers are installed into the subdirectory "include\mvtec\plugin" within the MERLIC installation, for example, "%PROGRAMFILES%\MVTec\MERLIC-26.03", right next to the headers for the C API. Because the support library is header-only and shares the same include path, no additional setup to the build environment is necessary, and no separate CMake target is provided. The support library requires a compiler supporting at least C++17.

The types and functions provided by the support library use the namespace MVPlugin. Consequently, the "MV" prefix, which can be found on all of the C API's names, has been dropped from their support library counterparts.

Optionally, the support library can utilize the {fmt} library for string formatting. Specifically, the MVPlugin::Logger class conditionally supports overloads excepting format strings and format argument packs. Many of the data types provided by the support library already support formatting out-of-the-box which can make quick work of enriching your log messages with the relevant information. To opt into using the {fmt} library, you need to add the following definition to the CMake target of your plug-in and link against the fmt::fmt target.

target_compile_definitions(MyPlugin PUBLIC MV_USE_FMT)

Both of the example plug-ins, event-logger and action-sender, now use the support library to demonstrate its usage and enhance their readability. They also both make use of the {fmt} library, and its integration into their respective CMake projects can be used as a template.

Plug-in Controller Class and C Interface Functions

When using the support library, the life-cycle of the plug-in instance is tied to the existence of an instance of a user-defined C++ "controller" class which we will simply call Plugin. This class should, however, be placed in an appropriately named namespace. The constructor and destructor of this class correspond to the MVOpen and MVClose functions, respectively, whereas the MVStart and MVStop functions are delegated to Plugin::Start and Plugin::Stop member functions. The state of the plug-in instance can be stored in that class as member variables.

namespace pMVMyPlugin {
struct Plugin {
explicit Plugin(std::unique_ptr<MVPlugin::IActionController>);
~Plugin() noexcept;
MVCode_t Start(MVPlugin::AccessLevel);
MVCode_t Stop();
private:
std::unique_ptr<MVPlugin::IActionController> m_ActionController;
};
}

The support header PluginCInterfaceHelper.h provides ready-made interface function templates which can be associated with the plug-in in MVInit_V2. As an exception to this rule, it is useful to implement MVOpen separately and call MVPlugin::OpenPlugin from within, as this is a useful customization point to inject dependencies into the Plugin class.

An implementation scaffold might look like this:

static MVCode_t MVOpen(MVPlugin_t handle)
{
return MVPlugin::OpenPlugin<pMVMyPlugin::Plugin>(handle, std::make_unique<MVPlugin::ActionController>(handle));
}
MVCode_t MVInit_V2(MVClass_V2_t* MVClass)
{
MVClass->MVOpen = MVOpen;
MVClass->MVStart = MVPlugin::StartPlugin<pMVMyPlugin::Plugin>;
MVClass->MVStop = MVPlugin::StopPlugin<pMVMyPlugin::Plugin>;
MVClass->MVClose = MVPlugin::ClosePlugin<pMVMyPlugin::Plugin>;
return MV_CODE_OK;
}

The Plugin class can also be unit-tested by following these two steps:

  • The static library must be linked into a test executable.
  • The Plugin class has to be injected with "mocked" dependencies.

Sending Actions

The support library features an MVPlugin::ActionController class which wraps the C APIs provided by mv_action.h and mv_action_def.h, mapping each type of action to one member function (overload set). The parameters of the action are passed as function arguments. The wrapper takes care of initializing the underlying MVAction_t object, setting the appropriate action parameters, and queuing the action at the vision system.

MVPlugin::ActionController implements the MVPlugin::IActionController interface. It is recommended to work with this interface from within the Plugin class and to inject the implementation instance from the outside, as demonstrated in the section Plug-in Controller Class and C Interface Functions. This way, unit tests can inject a mock of the interface to allow setting expectations on the actions being taken by the plug-in.

Receiving Events

Receiving events from the vision system directly using the C API defined in mv_event.h and mv_event_def.h entails the following pattern:

  • Spinning up a separate thread in MVStart
  • Polling for new events on a loop
  • Processing the resulting MVEvent_t objects
  • Breaking out of the loop when the Shutdown event is received
  • Finally, joining the thread up again in MVStop

The support library provides an abstraction for this pattern. By deriving from MVPlugin::EventMonitor<Plugin> , the Plugin class can effectively subscribe to certain events from the vision system by defining suitable event callbacks. EventMonitor employs the so called "curiously recurring template pattern (CRTP)" to mix this functionality into the Plugin class, and it will only extract parameters from an event and convert them into their support library counterparts if the Plugin class defines a suitable member function to handle it. Hence, the runtime penalty incurred due to uninteresting events is minimized. The signature of these member functions must match exactly, otherwise the callback is not recognized and the corresponding events are silently ignored.

struct Plugin : MVPlugin::EventMonitor<Plugin> {
// ...
void StateChanged(MVPlugin::EventTrigger, MVPlugin::ActionType cause, MVPlugin::State fromState, MVPlugin::State toState) noexcept;
void Error(MVPlugin::EventTrigger, MVPlugin::Error const&) noexcept;
void RecipePrepared(MVPlugin::EventTrigger, MVPlugin::RecipeId const&) noexcept;
void RecipeUnprepared(MVPlugin::EventTrigger, MVPlugin::RecipeId const&) noexcept;
void JobStarted(MVPlugin::EventTrigger, MVPlugin::JobInfo) noexcept; // (J1)
// or: void JobStarted(MVPlugin::EventTrigger, MVPlugin::JobId, MVPlugin::RecipeId const&, MVPlugin::JobType, std::vector<MVPlugin::Value> const&) noexcept; // (J2)
void JobStarted(MVPlugin::EventTrigger, MVPlugin::JobId) noexcept; // (J3)
void AcquisitionDone(MVPlugin::AcquisitionInfo) noexcept; // (A1)
// or: void AcquisitionDone(MVPlugin::JobId) noexcept; // (A2)
void Ready(MVPlugin::EventTrigger, MVPlugin::JobId) noexcept;
void ResultReady(MVPlugin::Result const&) noexcept;
void VisionSystemAvailable(MVPlugin::SystemStatus const&, std::future<std::vector<MVPlugin::RecipeData>>) noexcept;
// ...
};

These callbacks map closely to the Available Events. The Shutdown event is omitted because it is already handled behind the scenes by breaking out of the event loop.

Both the "JobStarted" and "AcquisitionDone" events admit multiple function signatures which add additional information:

For the "JobStarted" event, the original overload, designated (J3), still exists to remain source-compatible and can also act as a fallback when a plug-in built against the present API version is executed in a previous MERLIC version that still uses the API version 2.2.0. The overload (J2) was added in API version 2.2.1 to provide additional information. In API version 2.2.2, the overload (J1) was introduced providing even more (optional) information which is aggregated in the MVPlugin::JobInfo structure. (J1) will also be invoked if the plug-in is executed by a MERLIC versions with API version 2.2.1. You can only define either (J1) or (J2), with (J1) being the recommended choice for new plug-ins. Additionally, (J3) can also be defined as a compatibility fallback. Only one of the overloads will be invoked per event.

For the "AcquisitionDone" event, either the original overload, (A2), or the extended one, (A1), can be defined. If the extended one is defined, it adds information about the image acquisition in the structure MVPlugin::AcquisitionInfo. Unlike with "JobStarted", the extended overload will be called regardless of the MERLIC RTE version executing the plug-in but the additional data may not be available in older versions.

The VisionSystemAvailable callback is a notable addition and indicates that the vision system has been (re-)started and connected. Thus, its internal state and recipe list may have become inconsistent to what the plug-in might expect from monitoring the event stream. The callback lets it react to that. The std::future provided as the second argument is a "lazy" stand-in for the recipe list. If get() is called on it, the vision system will be queried for the current list of recipes.

To use the EventMonitor, the underlying event loop thread has to be started using MVPlugin::EventMonitor::StartEventLoop() in Plugin::Start() and stopped using MVPlugin::EventMonitor::StopEventLoop() in Plugin::Stop(), respectively.

Configuration Parameters

The support library also significantly simplifies the creation of plug-in configuration options through the C APIs provided by mv_plugin_config.h and mv_plugin_config_def.h. To that end, it caters the most common use case where the available user parameters and the constraints imposed upon them are "static" in the sense that they are independent of the actual configuration values. If you need to dynamically change the available user parameters, you need to fall back on the C API for now.

Two data structures need to be defined in your plug-in's namespace. The first one, which we will call Config, contains all the configuration values needed by your plug-in as member variables. We recommend to initialize the members to the desired default values. This data structure will be handed to the Plugin instance in its Start function, at which point it will hold the actual values which the user has set through the "Communication" tab of the MERLIC RTE Setup.

namespace pMVMyPlugin {
struct Config {
std::uint16_t port = 65432;
};
struct Plugin {
// ...
MVCode_t Start(MVPlugin::AccessLevel, Config config) noexcept;
// ...
};
}

The other data structure, ConfigDescription in this example, should also contain one member per user parameter. These are instantiations of the MVPlugin::Config::ConfigEntry<> template which are created and parametrized using the AddConfigEntry<>() factory function in combination with a "builder pattern".

namespace pMVMyPlugin {
struct ConfigDescription {
Config const default_config;
MVPlugin::Config::AddConfigEntry<std::uint16_t>()
.WithKey("parameter_port")
.WithDisplayName("Port")
.WithDefaultValue(default_config.port)
.WithValueRange({49152, 65535})
.Build();
Config Read(MVPlugin::Config::PluginConfig const&) const;
MVCode_t Validate(Config const&, MVPlugin::Config::Validation&) const;
};
}

The ConfigDescription is created statically only once for each plug-in, and its Read() function is called to produce the actual Config data structure based upon this description. A sample implementation of Read() might look like this:

Config ConfigDescription::Read(MVPlugin::Config::PluginConfig const&
actual_config) const {
return Config{actual_config.GetValueFor(port)};
}

You are free to use Read to perform any conversions from the underlying user parameter types (integers, floating-point numbers, strings) to higher level custom types. Finally, suitable implementations for the MVExpose, MVValidate, and MVStart interface functions need to be provided in MVInit_V2 which consume the ConfigDescription and use it to produce the Config data structure and pass it to Plugin::Start(). The support header PluginCInterfaceHelper.h again provides ready-made interface function templates for this purpose:

MVCode_t MVInit_V2(MVClass_V2_t* MVClass)
{
using namespace pMVMyPlugin;
MVClass->MVOpen = MVOpen;
MVClass->MVStart = MVPlugin::StartPlugin<Plugin, ConfigDescription, Config>;
MVClass->MVStop = MVPlugin::StopPlugin<Plugin>;
MVClass->MVClose = MVPlugin::ClosePlugin<Plugin>;
::ExposeAddAllStaticConfigEntries;
::Validate;
return MV_CODE_OK;
}

The "action-sender" and "save-images" example plug-in demonstrates the use of the configuration feature through the support library.

Getting Image Results

The support library also enables you to get the image results of a MERLIC Vision App, that is, images that have been specified as an "MVApp result" in the MERLIC Creator. The concept of the image API when using the support library is the same as if the standard C API is used. Therefore, you can refer to Getting Image Results to learn more about the general concept of the image API.

In this section, you can find additional information specific for the use of the support library. The support library provides the types MVPlugin::DataContainerDescriptor and MVPlugin::DataComponent analogous to the ones in the C API. To submit the final data container back to the vision system, you can use the function FetchDataContainer() provided in MVPlugin::ISynchronousQueryController.

To see an example how image results can be retrieved via the support library, take a look at the example plug-in "save-images". When the plug-in is running, it automatically saves the image data contained in to a configurable directory on disk. It also offers a rich set of configuration options to customize the image format, output directory structure, and conditions upon which to output an image. The source code of the plug-in is available in the directory "`examples\communication_plugins\save-image`" within the MERLIC installation directory. Thus, you can take a look at the implementation and how the MVPlugin::DataContainerDescriptor and MVPlugin::DataComponent are used in this example plug-in.

The following code block shows an example how to iterate over the data components in a data container descriptor.

// Create a copy of the data container descriptor which comes with the ResultReady event to allow modification
auto scaled_down_data_container_descriptor = result_event.dataContainerDescriptor;
// Iterate over, copy and scale down images to half resolution
for (auto data_component_ref : scaled_down_data_container_descriptor)
{
auto [width, height] = data_component_ref.GetImageDimensions();
data_component_ref.SetImageDimensions(MVPlugin::ImageDimensions{ width / 2, height / 2 });
}
// Submit the data container descriptor to fetch the actual image data
std::vector<MVPlugin::DataComponent> data_container =
m_SynchronousQuery->FetchDataContainer(scaled_down_data_container_descriptor);

For each data component reference, the default image dimension is queried and then scaled to the half of the original size. Then the modified data container descriptor is submitted back to the vision system.

MVPlugin::AcquisitionInfo
Definition: Defines.h:329
MVPlugin_t
struct _MVPlugin_t * MVPlugin_t
Opaque data type which represents a plug-in instance handle.
Definition: mv_plugin_control_def.h:76
MVPlugin::Config::ConfigEntry
Definition: ConfigEntry.h:55
MVPlugin::EventTrigger
Definition: Defines.h:166
MVPlugin::EventMonitor
Definition: EventMonitor.h:186
MVPlugin::Result
Definition: Result.h:53
_MVClass_V2_t
Data structure holding function pointers to plug-in API functions.
Definition: mv_class_v2.h:38
MVPlugin::JobInfo
Definition: Defines.h:288
MVPlugin::Config::PluginConfig
Definition: PluginConfig.h:27
MVPlugin::Error
Definition: Defines.h:194
MVPlugin::ConfigCInterface
Definition: PluginCInterfaceHelper.h:101
MV_CODE_OK
#define MV_CODE_OK
The command was processed successfully.
Definition: mv_error_def.h:79
MVPlugin::Config::Validation
Definition: Validation.h:27
MVCode_t
uint32_t MVCode_t
Type alias used for return codes.
Definition: mv_error_def.h:61
MVPlugin::SystemStatus
Definition: Defines.h:141
MVPlugin::ImageDimensions
Definition: Image.h:29