General
General API Design, Error Handling
The C++ API follows the following principles:
Objects are constructed directly via their constructors. For example, to load a model, one would use the constructor of the Model class.
Objects themselves have move semantics: they cannot be copied (the copy constructor is implicitly deleted), but they can be moved into other variables of the same type. They behave similar to
std::unique_ptr
in that regard.Functions and methods that perform an action will typically return
void
Functions and methods that query something will typically return a simple value or structure
Errors are reported by throwing exceptions.
Error Handling
API calls that result in an error will throw an exception. The exception will be one of the following:
std::invalid_argument
if an invalid argument was passed to the method in question
std::bad_alloc
if an allocation failed (most likely due to being out of memory)
std::out_of_range
if an index was supplied that is not within the valid range
fluxEngine::Error
or a subclass thereof (which in itself is a subclass ofstd::runtime_error
) if an error occurred that does not fall into the previous categoriesIf a user-supplied callback is provided, and that callbacks throws any exception, that exception is then propagated back out to the user
All exceptions that are either fluxEngine::Error
or a
subclass have additional fields for more information about the error:
Calling
errorCode()
on the exception object will return an error code of typefluxEngine::ErrorCode
that allows the user to further identify the type of error that occurred.Calling
osErrorCode()
on the exception object will return an operating system error code if the operation failed due to a system call failing (for example: a file could not be opened)
See the documentation of fluxEngine::Error
and
fluxEngine::ErrorCode
for further details.
Complex Return Values
If a method returns multiple bits of information, those will be
returned in form of a convenience structure. For example, the method
fluxEngine::Model::groupInfo()
will return a
fluxEngine::Model::GroupInfo
structure that contains
multiple fields describing a given group.
Classes, Constructors, Move-Only Semantics
The major classes, Handle
,
ProcessingQueueSet
,
Model
,
ProcessingContext
, and
DeviceGroup
all work in a
similar manner. If the default constructor is used, for example because
a variable is just being declared, this will not perform any action, but
create an invalid object that may later be filled with a valid object.
Furthermore, it is possible to use any object of the aforementioned
types in an if
clause to check if the variable currently holds a
valid object. For example:
1 fluxEngine::Handle h;
2 // h may not be used at this point, is not valid
3 h = functionThatReturnsAValidHandle();
4 // h is now valid and may be used
5 h = {};
6 // h is now invalid again
7 if (h) {
8 // this code will never be executed
9 }
In order to construct an actual object, one must use any non-default constructor. For example, to create a handle one would typically use the following code:
fluxEngine::Handle handle(licenseData, licenseDataSize);
Objects behave similarly to std::unique_ptr
, in that they can be
moved but cannot be copied:
1 fluxEngine::Handle handle(licenseData, licenseDataSize);
2 // The following works (and now handle is invalid,
3 // and h2 is valid)
4 fluxEngine::Handle h2 = std::move(handle);
5 // The following is a compiler error (no copies allowed)
6 fluxEngine::Handle h3 = h2;
Warning
It is currently only possible to create a single handle due to limitations that may be removed in a later version. If a given handle is to be replaced, the variable containing the handle must be cleared first before constructing the new handle. For example:
1 // (Assuming the variable handle contains an already
2 // valid handle.)
3 // Will not work (because two handles would exist at
4 // the same time)
5 handle = Handle(licenseData, licenseDataSize);
6 // Will work (first the old handle is erased, then
7 // the new handle is created)
8 handle = {};
9 handle = Handle(licenseData, licenseDataSize);
The primary exception to this logic are devices that have been
connected, as they belong to the corresponding
fluxEngine::DeviceGroup
object (and will be destroyed if
that object is destroyed). They can only be accessed via pointers, and
the individual device types inherit from the base class
fluxEngine::Device
.
Initializing the Library
To initialize the library a license file is required. The user must read that license file into memory and supply fluxEngine with it.
The following code demonstrates how to properly initialize fluxLicense and how to tear it down again.
1 // Get the data of the license file from somewhere
2 std::vector<std::byte> myLicenseData = ...;
3 try {
4 fluxEngine::Handle handle(myLicenseData);
5 // handle is now valid, may be used
6 } catch (std::exception& e) {
7 std::cerr << "An error occurred: " << e.what() << std::endl;
8 exit(1);
9 }
10 // Handle has left the current scope, is now no longer valid
Licenses tied to camera serial numbers
If a license is tied to a camera serial number, certain operations will fail unless the camera is currently connected. These operations include (but are not limited to):
Loading a model
Creating a processing context (even for offline processing)
Processing data with an already existing processing context
Loading a HSI cube from disk
For this reason, even if only offline data is to be processed, if a license file is tied to a camera serial number, the user must always first connect to that camera before performing any of these operations. The camera must stay connected while the user wants to perform any of these operations.
It is still possible to save HSI cubes to disk even if no camera is connected. This is to ensure that the camera fails unexpectedly during operation (because e.g. somebody unplugged it) to give the user a chance to save the data they curreently have in memory.
If the license is tied to a dongle or a mainboard serial, this does not apply, and these operations can be performed at any time after a handle has been created. (If a dongle is phyiscally removed after creating a handle, the same restrictions apply though.)
Setting up processing threads
fluxEngine supports parallel processing, but it has to be set up at the
very beginning. This is done via the
createProcessingThreads()
method.
The following example code demonstrates how to perform processing with 4 threads, assuming a handle has already been created:
1 try {
2 handle.createProcessingThreads(4);
3 } catch (std::exception& e) {
4 std::cerr << "An error occurred: " << e.what() << std::endl;
5 exit(1);
6 }
Note
This will only create 3 (not 4!) background threads that will
help with data processing. The thread that calls
fluxEngine::ProcessingContext::processNext()
will
be considered the first thread (with index 0) that
participates in parallel processing.
Note
Modern processors support Hyperthreading (Intel) or SMT (AMD) to provide more logical cores that are phyiscally available. It is generally not recommended to use more threads than are phyiscally available, as workloads such as fluxEngine will typically slow down when using more cores in a system than are physically available.
Note
When running fluxEngine with very small amounts of data, in the extreme case with cubes that have only one pixel, parallelization will not improve performance. In cases where cubes consisting of only one pixel are processed, it is recommended to not parallelize at all and skip this step.
Note
Only one fluxEngine operation may be performed per handle at the same time; executing multiple processing contexts from different threads will cause them to be run sequentially.
Since it is currently possible to only create a single handle for fluxEngine, this means only one operation can be active at the same time; though the limitation of only a single handle will be lifted in a later version of fluxEngine.