| aas-core3 1.0.0
    Manipulate, verify and de/serialize asset administration shells in C++. | 
Here's a quick intro to get you started with the SDK. See how you can:
Saying that installing C++ [nlohmann/json]: https://github.com/nlohmann/jsonencies is opaque and confusing would be an understatement. There are many compilers, many packagers (vcpkg, conan, etc.) and many build systems (CMake, Make, Bazel etc.). We admit that we are by no means experts in C++ compilation. We give our best to modestly document how to install our SDK and include it in other projects.
At this point, we kindly invite the experts in this area to help us out with packaging and adaptation for different build systems.
We want to keep the dependencies minimal, and optimally the SDK should have none except the standard library. Unfortunately, and unlike some other mainstream languages, the C++ standard library provides no JSON and XML parsing out-of-the-box.
What is more, the union types in form of std::expected and nullables as std::optional were introduced much later in the language, in C++23 and C++17, respectively. We wanted the SDK to stand the test of time, and already use those novel features. We also acknowledge that older settings depending on C++11 must be supported for successful adoption.
Therefore, we rely on four external libraries:
You need to install these dependencies such that CMake can find them using find_package.
To compile your project with our SDK as source code dependency, make sure to first install the dependencies mentioned above.
Then clone the repository, and point to it when you call cmake by:
Our SDK is exported as aas_core3 to CMake. You have to find it first in your CMakeLists.txt:
Then you can then link it to your executables in the CMakeLists.txt of your project with:
We map the meta-model to C++ like follows:
bool.[std::vector]<[std::uint8_t]>.std::optional. Otherwise, we rely on tl::optional. We re-export the one of the two which we use with using directive as aas_core::aas_3_0::common::optional.IClassWe introduce the most general class, aas_core::aas_3_0::types::IClass, to group all the functionality of the model instances. This class does not exist in the original AAS meta-model.
model_type as faster RTTIThe AAS meta-model is given as a deep class hierarchy, with ample multiple inheritance. Therefore, we have to use virtual inheritance in C++ as well.
This makes the dynamic type determination based on [C++ Runtime Type Information (RTTI)] quite slow, see this StackOverflow question about RTTI efficiency. In addition, as C++ does not support type switches. If you have to dynamically act upon the runtime type, you have to write a long sequence of if (...) else if (...) else if (...)'s.
To alleviate that problem, each instance of the model is given an immutable getter of the runtime type as aas_core::aas_3_0::types::IClass::model_type. The model type will hold one of the concrete literals of the enumerator aas_core::aas_3_0::types::ModelType, corresponding to the runtime type of model instance.
Unfortunately, virtual inheritance also makes fast static pointer casts impossible, even when you know upfront the runtime type of pointer. You have to use std::dynamic_pointer_cast instead, which is slower as it relies on RTTI. When you need to cast among different model types, aas_core::aas_3_0::types::IClass::model_type will still spare you a sequence of if-else if-else if's.
In any case, fast upcasting from a concrete class to a more abstract one is still possible with [std::static_pointer_cast].
The SDK also gives you functions IsXxx to check for a single model type in order to avoid RTTI. See, for example, aas_core::aas_3_0::types::IsSubmodel.
Each attribute of a class is represented with a getter, a mutable getter and a setter. The getter returns a constant value of the attribute, usually a constant reference or a primitive value (e.g., for booleans). The mutable getter returns a mutable reference.
The namespace aas_core::aas_3_0::types contains all the classes of the meta-model. You can simply use their constructors to create an AAS model. Usually you start bottom-up, all the way up to the aas_core::aas_3_0::types::IEnvironment.
For optional properties which come with a default value, we provide special getters, {property name}OrDefault. If the property has not been set, this getter will give you the default value. Otherwise, if the model sets the property, the value of the property will be returned.
For example, see aas_core::aas_3_0::types::ISubmodel::KindOrDefault.
Being an old language which predates unicode, C++ is a bit messy when it comes to dealing with unicode strings. At the moment, there is no standard way how to represent, encode and decode unicode strings to and from different formats.
We decided to use std::wstring, which seemed as a solution to fit the widest range of the use cases. However, wide strings do not fit all the use cases. For example, whenever you output something to a console or a text file, it is usually encoded in UTF-8. You serialize then a wide string and encode it as UTF-8 [std::string], and vice versa.
The SDK provides two functions, aas_core::aas_3_0::common::WstringToUtf8 and aas_core::aas_3_0::common::Utf8ToWstring, to convert between UTF-8 and wide strings. Both functions rely on basic functions provided by your operating system. If you are on Windows, they simply wrap MultiByteToWideChar and WideCharToMulitByte. In Linux, they will use std::codecvt.
Here is a very rudimentary example where we show how to create an environment which contains a submodel.
The submodel will contain two elements, a property and a blob.
(We will alias the namespace aas_core::aas_3_0 as aas for readability. You might or might not want to write your code like that; the aliasing is not necessary.)
Enumerators are basically integer numbers in C++. This is efficient when you manipulate them, but makes the enumerator cumbersome to work with when you need to use them in user interfaces, embed them in documents etc. The SDK provides two modules, aas_core::aas_3_0::stringification and aas_core::aas_3_0::wstringification, to convert enumerators to and from strings and wide strings, respectively.
All enumerators are converted using the overloaded functions aas_core::aas_3_0::stringification::to_string and aas_core::aas_3_0::wstringification::to_wstring, respectively.
To parse an enumerator from a string to a literal, use XxxFromString and XxxFromWstring, respectively. For example, see aas_core::aas_3_0::stringification::ModelTypeFromString.
If you want to iterate over all literals of an enumerator, the SDK provides kOverXxx in aas_core::aas_3_0::iteration. For example, aas_core::aas_3_0::iteration::kOverAasSubmodelElements.
The SDK provides various ways how you can loop through the elements of the model. The following sections will look into each one of the approaches.
The iteration code resides in aas_core::aas_3_0::iteration.
Descent and DescentOnceThe SDK provides two iterables, aas_core::aas_3_0::iteration::Descent and aas_core::aas_3_0::iteration::DescentOnce. You give them an instance of the model, and they provide begin() and end() functions. This is particularly practical when you directly pass them into range-based for loops.
As their names suggest, Descent iterates recursively over all the instances referenced by the original instance. In contrast, DescentOnce iterates only over the instanced referenced by the original instance, and does not proceed recursively.
Here is a short example how you can iterate over all the instances of a model, and output their model types:
Descent and DescentOnceFiltering with Descent and DescentOnce is fairly easy, as we can write the filtering code within a for loop.
Here is a short snippet, adapted from the previous example, where we filter for all properties which have int as their value type:
For loops are practical for short iterations. When you need to apply an action for each different model type, your loop body will probably become unbearably long. Such iterations are much better solved by using the visitor pattern. You specify for each model type a separate method in the visitor, and the visitor automatically dispatches the calls based on the type.
IVisitor. The SDK provides an abstract, purely virtual, interface aas_core::aas_3_0::visitation::IVisitor. If you want to steer how the children references from an instance are handled, then implement this interface.
AbstractVisitor. Most of the time, you want to process each instance of a model in isolation, so you do not want to call the visitting methods on the children references. The abstract visitor aas_core::aas_3_0::visitation::AbstractVisitor will take care of the children references. You merely have to specify the operation for each model type.
PassThroughVisitor. If you want to process only some of the model types, use the class aas_core::aas_3_0::visitation::PassThroughVisitor. This class will iterate recursively over all the referenced instances, and simply perform no action for instances of model types for which you did not override the corresponding visiting method.
Here is an example with a aas_core::aas_3_0::visitation::PassThroughVisitor, which has been adapted from the previous filtering example:
Our SDK allows you to verify that a model satisfies the constraints of the meta-model. The verification logic is concentrated in aas_core::aas_3_0::verification.
Similar to descent, the verification is represented as an iterable over verification errors. For a recursive verification over the instance and its references, use aas_core::aas_3_0::verification::RecursiveVerification. If you only want to check the instance and its immediate references, use aas_core::aas_3_0::verification::NonRecursiveVerification.
The reporting of errors is probably easiest in a [range-based for loop]. If you want to report only a pre-determined number of errors, you can simply break out of the loop.
We use long to handle verifying years in xs:Date, xs:DateTime etc. For example, to determine if the year is a leap year. This is important for astronomical applications which might use more than 64-bit year numbers – we could not find any production-ready BigInt implementation which can be easily included. Please let us know if you need astronomical years outside the 64-bit range.
Note that some of the other SDKs suffer from the same issue (TypeScript), while others do not (Python, C# and Golang), which avoid the issue by using BigInt's. 
Here is an example which verifies an environment against the meta-model constraints:
The path to the erroneous value is kept in aas_core::aas_3_0::iteration::Path, based on the enumeration aas_core::aas_3_0::iteration::Property.
We did not implement the reflection at the moment since we did not have a use case for it. If you need reflection, please contact the developers. It should be a small step going from paths to de-referencing to getters and setters.
Our SDK handles the de/serialization of the AAS models from and to JSON format through the aas_core::aas_3_0::jsonization.
Since C++ does not support JSON de/serialization in the standard library, we use nlohmann/json as the underlying representation of JSON values. We thus do not directly de/serialize from and to strings, but rely on nlohmann/json for intermediate handling.
The model instances are converted to nlohmann/json using aas_core::aas_3_0::jsonization::Serialize.
Here is an example code:
To translate nlohmann/json value to a model instance, the SDK offers XxxFrom de-serialization functions. For example, to de-serialize an environment, call aas_core::aas_3_0::jsonization::EnvironmentFrom.
The de-serialization functions return a aas_core::aas_3_0::common::expected, a union type holding either the de-serialized instance or an error.
aas_core::aas_3_0::common::expected ourselves. If your compiler supports C++23, std::expected will be used underneath. Otherwise, a fill-in in form of tl::expected is used. Here is an example:
Here is another example of how to report the errors:
The code that de/serializes AAS models from and to XML documents lives in aas_core::aas_3_0::xmlization.
You serialize a model using the overloaded functions aas_core::aas_3_0::xmlization::Serialize.
Apart from the object, you pass it the writing options (aas_core::aas_3_0::xmlization::WritingOptions) and the stream to write to. The default options should cover the majority of the use cases in the field. The stream is assumed to be encoded in UTF-8, i.e., we write the text encoded in UTF-8.
Here is an example:
De-serialization goes in the opposite direction, with overloaded functions XxxFrom. For example, aas_core::aas_3_0::xmlization::EnvironmentFrom.
The model instances are de-serialized from a text stream encoded in UTF-8. There is an argument to steer the reading, aas_core::aas_3_0::xmlization::ReadingOptions. The defaults are picked such that they cover the majority of the cases.
C++ does not provide any XML parsing in the standard library, so we use Expat, but the user does not interact with it directly.
Similar to JSON, the result of the parsing is returned as aas_core::aas_3_0::common::expected. This is a union type between a model instance and the de-serialization error. Please see the previous note in JSON de-serialization related to how this union type is actually implemented
Here is an example how to de-serialize successfully a model instance from a text string:
Here is another example how you can report the error:
In any complex application, creating, modifying and de/serializing AAS instances is not enough. You have to insert your custom application-specific data to the model in order for the model to be useful.
Take, for example, parent-child relationship. The current library ignores it, and there is no easy way for you to find out to which aas_core::aas_3_0::types::ISubmodel a particular aas_core::aas_3_0::types::ISubmodelElement belongs to.
We did want to keep the types as simple as possible — the parent-child relationships can get tricky very soon if you have multiple environments with shared submodels etc. Instead of overcomplicating the code and making it barely traceable, we decided to keep it simple and frugal in features.
However, that is little solace if you are developing an GUI editor where you know for sure that there will be only one environment, and where parent-child relationships are crucial for so many tasks. What is more, parent-child relationships are not the only data that need to be intertwined — you probably want history, localized caches etc.
There are different ways how application-specific data can be synced with the model. One popular technique is to use Hashtable's and simply map model instances to your custom nuggets of data. This works well if the data is read-only, and you can spare the cycles for the lookups (which is often acceptable as they run on average in time complexity O(1) anyhow).
Otherwise, if you need to modify the data, maintaining the consistency between the Hashtable and your nuggets of information becomes difficult. For example, if you forget to remove the entries from the Hashtable when you remove the instances from the model, you might clog your garbage collector.
Hence, if you modify the data, you need to keep it close to the model instance. In dynamic languages, such as Python and JavaScript, you can simply add your custom fields to the object. This does not work in such a static language like C++.
One solution, usually called Decorator pattern, is to wrap or decorate the instances with your application-specific data. The decorated objects should satisfy both the interface of the original model and provide a way to retrieve your custom nuggets of information.
Writing wrappers for many classes in the AAS meta-model is a tedious task. We therefore pre-generated the most of the boilerplate code in aas_core::aas_3_0::enhancing.
In the context of decoration, we call your specific data enhancements. First, you need to specify how individual instances are enhanced, i.e. how to produce enhancements for each one of them. We call this an enhancement factory. Second, you need to recursively wrap your instances with the given enhancement factory.
The aas_core::aas_3_0::enhancing is generic and can work with any form of enhancement classes. You need to specify your enhancement factory as a std::function which takes an instance of aas_core::aas_3_0::types::IClass as input and returns a std::shared_ptr to an enhancement, or null, if you do not want to enhance the particular instance.
The wrapping and unwrapping is specified in the generic function aas_core::aas_3_0::enhancing::Wrap. This function takes the instance that you want to wrap (i.e., decorate) and the enhancement factory, and recursively wraps it with the produced enhancements. The wrapped instance will be finally returned.
You retrieve the enhancement with aas_core::aas_3_0::enhancing::Unwrap from a model instance. If the given instance has not been wrapped, nullptr is returned.
We also provide a shortcut function, aas_core::aas_3_0::enhancing::MustUnwrap, which always return a pointer to the enhancement, but throws an exception if the instance has not been previously wrapped.
Let us now consider the aforementioned example. We want to keep track of parent-child relationships in a model.
The following code snippets first constructs an environment for illustration. Then we specify the enhancement such that each instance is initialized with the parent set to nullptr. Finally, we modify the enhancements such that they reflect the actual parent-child relationships.
Note that this approach is indeed more maintainable than the one with Hashtable, but you still need to take extra care. If you create new submodels and insert them into the environment, you have to make sure that you wrap them appropriately. If you move a submodel from one environment to another, you have to update the parent link manually etc.
We demonstrate now how you can selectively enhance only some instances in the aas_core::aas_3_0::types::IEnvironment.
For example, let us assign a unique identifier to all instances which are referable, i.e., implement aas_core::aas_3_0::types::IReferable. All the other instances are not enhanced.
We disallow re-wraps of already wrapped instances to avoid costly iterations over the object trees, and throw an exception. Additionally, we want to prevent bugs in many settings where the enhancement factory assigns unique identifiers to instances or performs non-idempotent operations.