Let us start this manual with the simplest hello world sample demonstrating the use of an ALib configuration variable:
A simple application needs to include just header alib/config/configuration.hpp
main()
. Background information on this is given with chapter A.4 Collecting Caller Information of the General ALib Manual.The main function then looks as follows:
and the output is of course:
Hello World
The takeaways from this quick sample is the following:
"S"
specifies that the variable is of string type. Other built-in types exist and users of the library can register their own (complex) types to be stored in variables.OK, so far so good. But you might wonder: What is this good for? C++ already has variables in its language. Why should now run-time variables be used?
The answer becomes obvious when you compile and run this sample program passing the following argument:
--MY_VAR=Joe
Then the output will be:
Hello Joe
Obviously, the command line parameter was recognized as an external definition of the variable. And: This external definition is "stronger" than the "hard-coded" definition.
Does this make sense? Of course it does: Hard-coded values placed in the source code of an application are considered default values. If the user specifies some other value explicitly, quite naturally this value is the one to be used!
With this litte introduction sample, a reader might already guess what the whole use and effort of providing module ALib Configuration is about. The rest of this manual explains the details.
This module provides tools to read and write configuration data using different mechanisms. In short, the features are:
Possible provision of variable declarations, including
as resourced C++ enumeration values.
With that in place, the source code exclusively refers to variables using an C++ enumeration element. This allows duly separating all external configuration data from the source code and for example to translateall variable names and description to a different locale.
As a result from this feature list, it may become obvious that this ALib Camp can used in other use-cases - apart from "just" configuration data.
Variables are stored in and managed by class Configuration. This type inherits its core feature from class StringTree provided by module ALib Containers.
If you are not familiar with class StringTree, you might read its reference documentation now. In short, this type is a hash table which organizes the entries in a linked tree structure (in addition to the key/value hashing). The path through the tree is implemented by strings - just as the path of a file system is. While in a file system, the nodes of the tree are files, in case of class Configuration, the nodes are variables.
Same as with filesystems, the StringTree defined with class Configuration uses a slash '/'
as the path separator. The default interface into configuration variables treats all variable names given with declarations as absolute paths (!) in the string tree. Hence to declare a variable named
MYAPP/MYVAR
is the same as declaring it
/MYAPP/MYVAR
For simple use cases, the fact that variables are organized in a tree structure can just be ignored! If things grow and become more complex, organizing variables along a well thought hierarchy will bring some tremendous benefits.
Variable names are case-sensitive. This has advantages and disadvantages. One advantage is performance: When declaring variables (which is searching variables in the StringTree and creating the path if not existent), no upper/lower case conversion has to be performed.
A problem arises if external configuration systems are case-insensitive. Then, two ALib variables that only differ in letter case, are not externally distinguishable. This may already be true for environment variables (depending on the operating system).
The most natural way out of this problem - and this is what we recommend to do - is to just use capital letters and underscores '_'
with ALib variables, as seen with the sampled "MY_VAR"
and as implemented with all built-in variables of ALib.
Built-in plug-ins EnvironmentVariablesPlugin and CLIVariablesPlugin, as well as class IniFileFeeder (all types are discussed in detail in later chapters) operate in a case insensitive fashion.
As just learned, if variable names use the path separator, the variables become organized hierarchically. For example, two ALib Camps are using variables today:
"/ALIB/"
"/ALOX/"
Let us look at variable ALIB/LOCALE which is read by camp class BaseCamp during bootstrap and allows setting a different locale than defaulted by the environment that an application is launched in. If an INI-file was used, naturally this variable would reside in INI-file section "ALIB" and the entry would be named "LOCALE=...". Here you see, that some sort of "translation" already has to take place.
When this variable is read from the command-line, it has to be addressed as:
--ALIB_LOCALE=...
The path separator here is replaced from slash to an underscore '_'
character.
The decision for such translations is not really a concept of this module. It is rather defined by the configuration plug-ins (which are discussed later) and by user code that imports or exports configuration data from custom sources as needed. It is just important to keep this in mind, because using slashes is often not wanted for external representations of variables.
This ALib Camp actively supports to use one declaration singleton to define a set of variables. This feature is used in the case that a certain set of configuration data is applied to more than one instance of a user type. As a sample, let us look at ALox configuration variable ALOX/LOGGERNAME/FORMAT. While the documentation here says "/LOGGERNAME/" the resourced name of the declaration of the variable reads "ALOX/%1/FORMAT"
.
Classes Variable and Declaration both provide overloaded methods that accept an additional parameter called replacements. With that, replacement strings for placeholders %1, %2, ...N
can be passed. In the above sample, camp ALox passes the name of a logger that a user of the camp can freely choose and where multiple instances may exist. With that in place, each logger disposes about individual settings.
In addition to replacing placeholders in the variable name, those may also occur in a declaration's default value and comment strings.
If placeholders are used, a new singleton of class declaration is allocated in the MonoAllocator of class Configuration
. This means, that it is not allowed to perform an indefinite number of replacement-declarations. In the above sample, this is of course given: An application usually has two or three different ALox loggers in place, but does not create and delete new loggers with different (run-time generated) names.
Parameter replacements of the various overloaded declaration methods is of type Box. As explained with the Programmer's Manual of module Boxing, a box may also contain arrays and even arrays of boxes. Because of the multiple overloads, it was technically the easiest solution to offer the replacement values with such single parameter. Internally, type-guessing is performed and the following types are discovered:
The box or boxes found are converted to string values, by appending them to an internal AString instance.
Given this information, it becomes obvious, that in the simple case of one replacement string, such string can just be passed to parameter replacements. The following sample, taken from module ALox, shows how two replacements strings are passed. The resourced variable declaration is:
The declaration of the individual variable for a certain Logger attached to a certain Lox is as simple as this:
This camp differentiates between variable declarations and their definition. While these terms were chosen along the lines of the C++ language, of course with this camp the effective meaning of the terms are quite different.
This chapter is about declaring variables.
In the hello-world-sample above, three parameters had been given to the constructor of a variable:
This constructor is a shortcut to the following two lines of code:
This demonstrates the minimum information that is needed to declare a variable: The variable's name and type. While we have talked about names already, variable types will be discussed later. For now, it has to be enough to say that types are given as strings and that type "S"
stands for string variables.
With the declaration, the run-time memory allocation of the variable is in fact performed. A subsequent repeated declaration has no effect, of course no matter if this is done in the same code entity or in a different module or thread.
Internally the following things happen:
A variable's declaration can contain two more values. Together four declaration values can be given:
These four elements can be specified by using an instance of class Declaration:
The first advantage of using a declaration object lies in the fact that in case a variable should be used from different places in the code, the declaration might be created only once and shared between these places. This way, to change a variable's name, type, default value or description, this has to be done only at one point in the code. All variable declarations should be kept centralized somewhere.
Variable declaration values can be defined using "externalized" resource strings. For this, the concept of C++ Enum Records, introduced with module ALib Enums, comes into play. With the extension of this concept by the possibility to retrieve such enum records from resources, every aspect of a variable can be externally defined. The using code exclusively uses a C++ enum element to refer to variables, not "knowing" even what their name is.
As a sample, to declare aforementioned variable "ALIB/LOCALE"
, the following single line of code is needed:
This works, because:
This is all that is needed to fully externalize variable declarations.
If during the development of a software, a new variable is needed, all that is to be done is:
We chose the terms "declaration" and "definition" of variables, because they are so familiar to C++ users. The one tells what name and type a variable has, the other tells where the memory for a variable is found.
This implementation does not really go along these lines (and even technically could not). Instead, with the declaration of an ALib configuration variable, the memory for its value is already allocated and accessible namely in the nodes of the underlying StringTree.
Let's again look at the code of the introductory hello-word sample. It read:
So why do we need to also "define" a variable? And even: does such a definition can go wrong or why do we have this if-statement?
The truth is: We could leave the if-statement out, and even we do not need to call Define() at all and could still write the string "World" into the variable, like this:
What would not work then, was to overwrite the value by passing a command line argument. If passing
--MY_VAR=Joe
Then the output would remain:
Hello World
Conclusion: "defining" variables obviously means something different here.
A challenge that is inherent to working with "externalized configuration variables", is that contradicting values for a variable can exist. If such contradiction is resolved by ordering all possible input sources along a priority value, the challenge can be resolved and in fact turned into a feature that nicely resolves a variable value to what a programmer or an end-user expects.
Let's first look at the priority values. Those are given with C++ enum type Priority. Please read this enum's reference documentation now.
As you learn, a default value, that is usually residing in externalized resources along with the variable name and type, is superseded by any value that is set with priority Standard. Priority Standard is used (as default!) when common source code assigns a value to a variable.
In turn, Standard is superseded by any of the external configuration data sources like ConfigFile, Environment or CLI. This make a lot of sense, as we saw in the introductory hello-word-sample. The hard-code value "World" was overwritten by "Joe" if explicitly given as a command line argument.
Among the external sources superseding DefaultValues and Standard, priority CLI is the highest, configuration files (like INI-files) are the lowest and between both are environment variables and temporary session files. If you think about this for a minute, this order becomes quite intuitive.
Enumeration type Priority is an ALib arithmetical enum. Thus values can be easily compared and constants can be added or subtracted. Imagine two configuration files:
/etc
on GNU/Linux,~/.config
on GNU/Linux.Obviously, entries in the user-specific configuration file should supersede those in the general configuration file. Hence, values read from the general configuration file would use Priority::ConfigFile and values read from the user-specific file would use Priority::ConfigFile + 1.
With this background information, method Variable::Define can be explained. It has a parameter that was not visible in the hello-world-sample, namely parameter priority, which is defaulted to Standard. Now, all that this method does is to compare the given priority with the one that the variable already was defined with before. If a previous definition was higher, false
is returned! This indicates to the caller that no value should be written. While a C++ compiler can forbid you to write to a const
value, the ALib configuration system cannot forbid a programmer to still write a value into the variable.
Therefor, the whole mechanism of "variable definition" has to be seen as a contract between the code entities! If all sources fulfill the contract, a well organized management of external and internal configuration data is achieved.
And as every rule has their exception, it can still make sense sometimes to
What is described here, is the principal concept. Nothing is graved in stone. One example is found in the next quick section.
[[nodiscard]]
. Thus a compiler warning might be raised if the value returned is not used. If such ignorance is intentional, the call has to be preceded by (void)
to mitigate the warning.Enumeration type Priority defines element Protected. It has the maximum possible priority value. If a piece of code uses this priority to assign a value, no other entity will overwrite this value. At least no other code that obeys the contract, which is true for all default plug-ins provided with ALib.
This way, it is possible to protect values against external modification.
You might wonder: "If I do not want to allow external modification, I rather do not use an external
variable at all!" This of course is true for code under control. However, for 3rd party code using ALib, this offers an easy way to disallow users of your software (which incorporates that 3rd party code) to configure things that you do not want to be configurable.
An obvious sample is already found when a software just uses ALib: Imagine you do not want to allow ALib set a locale during bootstrap different from the one you designed your application for. All you have to do is to modify bootstrapping to stop before the final phase, define variable ALIB_LOCALE with priority Protected and assign the needed locale.
This way, an end user cannot "hack" into your application and switch the locale.
Another sample comes with module ALox. Here, the verbosity of log statements can be very quite easily fine-tuned with external configuration data, like setting an environment variable. Consider you use release logging which counts some transactions to an external server. Now you want to forbid the default behavior which allows disabling such logging. Well, setting the verbosity variable to Protected, stops any 3rd-party attempt to jump in.
In general, software security aspects must not be underestimated when using external configuration data, which this module enables so conveniently. Always consider what someone with bad intentions could do with your variables.
Protecting values can be performed in various ways, especially it is possible to set protected values without declaring or defining a variable. This is important to understand, because declaration and definition is only possible if the concrete variable type is known - and accessible! Now, if a variable is defined in a non-accessible library code, still its value can be protected. This will be shown in later chapters 6.1 Im- and Export and 6.2 Preset Values.
To recapture quickly what was said:
Now, a code might want to just test whether some other code entity has defined a variable or not, without creating an entry in the underlying StringTree. This can be achieved with methods Variable::IsDeclared and Variable::Try.
So far, we have just seen variables of type "S"
which are string types. With external configuration, human-readable strings are a very common approach of exporting and importing data and for example with command line parameters or environment variables, everything is received as string types.
Nevertheless, ALib configuration variables support arbitrary variable values. This means, not only integers and floating points are supported, but complex custom structs and classes can likewise be stored in variables.
The following types are built-in and thus available for use without further efforts:
Declaration Type Name | C++ Variable type | Description |
---|---|---|
B | bool | A boolean value, that is parsed with method alib::config::Configuration,ParseBooleanToken "config::Configuration,ParseBooleanToken" and optionally written back with alib::config::Configuration,WriteBooleanToken "config::Configuration,WriteBooleanToken". |
I | alib::integer | An integral value which will be parsed when imported from external string values. |
F | double | A floating-point value which will be parsed when imported from external string values. |
S | AStringPA | A string value. This is probably the most often used type. |
SV, | StringVectorPA | A list of strings, which is imported by parsing a comma-separated list. |
SV; | StringVectorPA | A list of strings, which is imported by parsing a list of strings separated by semicolons. |
BOX | Box | A generic variable type using the power of ALib Boxing. |
The value of a variable can be accessed with templated method Get<T>. The line of code of the hello-world sample:
can therefore be generalized with:
Method Variable::GetString is just an inlined shortcut to Get<AStringPA>(). Similar methods for the other built-in types exists with GetBool, GetInt, GetFloat, GetDouble, GetString, GetBox, GetStrings(), Size and GetString(int).
By the same token, corresponding assignment operators and implicit conversion operators have been defined. This allows for example to write:
which uses the variable declared as type "B"
just like a native boolean value. But due to the various overloads, the compiler has to be fed with the precise type. For example, stating
varSwitch= 0;
in the sample above would not compile because 0
might fit to bool
or other integral types. Therefore, a precise cast hast to be given. In this case of type "I" it would be:
varInt= alib::integer(0);
Having the GetXYZ methods and the assign- and conversion operators in place, the built-in variable types have a very convenient and natural interface. However, it has to be kept in mind, that no internal conversion takes place. Instead, accessing a variable using a wrong type, raises an ALib run-time assertion with debug-compilations. With release-compilations, such wrong access is undefined behavior.
A variable of type "BOX"
may be used to have different parts of a software store different sorts of data. Of course all limitations and precautions in respect to type-guessing, life-cycle considerations, etc. that are explained with the Programmer's Manual of module ALib Boxing have to be respected. The VMeta implementation (explained only in the next chapter) for this type imports the given string data as follows:
double
is boxed.For exporting, class VMeta just appends the Box to an AString. Consequently, all mechanics of serializing boxes as described with that module are in place.
Enabling the ALib configuration system to create and manage variables that contain custom structs and classes, is considered an advanced use of the API. While the effort is not very high, some basic understanding on how this camp internally manages variable types and values is helpful.
There are no technical restrictions in respect to the custom type that is to be stored in variables, apart from one: The type's memory alignment must not be greater than 'alignof(uint64_t)
'.
With this pack of theory in place, let's look at a step-by-step sample. Camp ALox introduces various own variable types, one of them defines the output meta-information and format of log statements. Here are the steps that have been taken:
The convenience methods GetXYZ(), assignment operators and conversion operators that are existing for the built-in types (see previous chapter 5.1 Built-In Variable Types, are of course not existing for custom types. Hence, variable value read/write access is always to be made using templated method Variable::Get<T>. In the sample of the previous chapter, the final step 5. added a convenience method to type TextLogger which hides the whole fact that the meta-information struct resides in a configuration variable.
Should you use variables very intensively, it can be a good idea to derive an own custom variable type from class Variable. In this case besides adding the convenience functions for the different content types, a tip would be to then also override the constructors of this custom variable type to omit the parameter that specifies the configuration. This way, your custom type would always automatically bind to the configuration instance that these variables should reside in.
The reason why we did not do this with camp ALox is that we have the access methods in place as just explained, and if we had an own variable type in addition, this would have added a next type to the reference documentation, and thus just added complexity to the user of the API, which is not necessary the code internally accesses the variables in the not so convenient fashion.
In the previous chapters, it was seen already that variables can be declared by providing a default value. For this, either Declaration::DefaultValue is used or parameter defaultValue of overloads of constructor of type Variable or its declaration methods. In all cases, independent of the variable type, this is a string value.
With method Variable::Import, string values can be used to set a variable value even after its declaration and a variable can likewise be serialized into a string value with method Variable::Export.
These interface methods can be used without "knowing" a variable's type. For example, the following code does not use a variable's type:
Instead, this code only tests if the variable was declared and if it was, it passes a string value to import.
From this, it becomes obvious that serializing and de-serializing variables from string values is an inherent concept of this ALib Camp. And as indicated with the abstract interface methods VMeta::imPort and VMeta::exPort, this camp talks about importing and exporting variable values (instead of using the word serialization).
This concept is very important to understand: Any value of any built-in or custom variable type can be noted in a human-readable string. There are different rationals for this:
Before the concepts of Presets and Substitution are detailed, a challenge arising by working with strings has to be discussed. This challenge is that there is a difference between strings that are human-readable (and for example editable in a text file), and those used by a C++ program internally. The difference is that, all non-printable characters like line-feeds, tabulators, leading and trailing spaces, quotation marks, etc have to be "escaped", when externally stored, just like these characters are escaped in a sting definition in the C++ source code.
Unfortunately, it is not sufficient to just unescape a given string and then store this C++ string for later imports, because the escape mechanics depend on the final variable type. Take the following two import strings:
Greets, 100 "Greets ", 100 "Hello, my name is \"Joe\"", 200
The corresponding custom variable type's import/export format definition, obviously expects a string type and an integral value. The import (deserialization) code obviously uses a comma to separate the values. Now, when de-escaping the import string, it has to be ensured, that the comma in the third sample is not seen as a separation character. Therefore, parsing the different tokens has to take into account that this comma is enclosed in quotation marks. If the import string as a whole was just converted to a C++ string representation, this information was lost.
For escaping strings, virtual utility type StringEscaper and its derivate StringEscaperStandard, provided by module ALib Strings, is used. Whenever values are im- or exported, an instance of one of both types (or a derived custom type) has to be available and with the just given samples, it becomes clear that only with the declaration of a variable, when the type and import mechanics are known, the escaping can take place.
Let's recap the prerequisites made in the previous chapter:
With this in place, the concept of Preset Values was implemented with this camp. Its use is very simple through overloaded methods Configuration::PresetImportString.
The methods store the given string along with an escaper and a priority in a hidden section of the StringTree that the configuration instance is based upon. If the given string value is nulled, a previously set value is removed. Once the variable is declared, the preset value will be imported using the given priority. (This is of course only performed if no different preset value was given with a higher priority and if no configuration plug-in with a higher priority defines the variable.)
While this looks like "just another feature" of the camp, in fact presetting import values is a very fundamental concept. Remember that otherwise, setting variable values is only possible once a variable is declared. And for this, the using code needs to know the variable's type.
More on using this concept is described in later chapter 7. Attaching External Configuration Systems.
When values are imported, class Configuration by default substitutes references to other configuration variables found in the import value of the requested variable.
For example, if two variables are defined as follows:
MY_RESULT= 42 MY_VARIABLE= The result is ${MY_RESULT}
then, with the declaration of variable MY_VARIABLE
, variable MY_RESULT
is read and the substring "${MY_RESULT}" is substituted by "42".
Substitutions are performed repeatedly until all variables are resolved. Therefore, nested substitutions may be defined as well.
If a variable that is referenced as a substitution value is not declared yet, then it is checked if a preset value exists. If so, this preset value is imported (using the right escaper) to a string variable and exported back using the escaper of the substituted variable.
true
for the case that circular substitution occurs. No warning or error message is given. If it is needed to detect substitution errors, the resulting value of a variable has to be checked, according to the expected value or pattern, which is depending on the using codeBy default, enclosing "${}"
is used to recognize variables. This can be altered using members SubstitutionVariableStart and SubstitutionVariableEnd of class Configuration. With both, It is also possible to specify a prefix and a suffix for the identification of substitutable variables in other variable's values. For example, the syntax can be adjusted to
MY_VARIABLE= The result is %MY_RESULT
A third member is SubstitutionVariableDelimiters. Please refer to all three member's reference documentation for further details.
Variable substitution can be a powerful concept and should be explained to end-users of an application. Consider an application that exposes a variable called "OUTPUT_FORMAT". A user may alter the format in a configuration file or with command line parameters. Whenever the user changes it, the former value has to be overwritten. With the concept of variable substitution, a user could create own variables for example in an INI-file, which are not even specified by the application, for example
LONG= ...a long format... SHORT= ...a short format...
and then just alter the original variable's value between:
OUTPUT_FORMAT= ${LONG}
and
OUTPUT_FORMAT= ${SHORT}
The user could even start its software by passing a command line parameter
--OUTPUT_FORMAT=${SHORT}
or whatever else is defined in the INI-file.
Alternatively, an application could create a set of presets for a variable and depending on a command line parameter like "--verbose" could set a variable to substitute one of the presets.
In general, external configuration data can be made available to the ALib configuration system in two possible ways:
The decision about which one is the right way to go mostly depends on the type of the external data. Here is one example for each implementation possibility:
The following chapters explain both approaches and a little more.
As just explained, some data sources may be read only on request, instead of feeding all configuration data to the ALib configuration system on bootstrap. For such sources, class Configuration provides a plug-in mechanism. Simple virtual base class ConfigurationPlugin describes the interface that is to be implemented. When done, the plugin can be attached with method PluginContainer::InsertPlugin.
For two of such sources, built-in solutions exist with types
Both plug-ins are created and attached with construction of a configuration instance (respectively its method Reset) in case parameter createDefaults is not specified to suppress such creation.
As explained in the introduction of this chapter, configuration data that is specific to an application is recommended to be read and fed into the configuration system in one shot with application bootstrap.
A challenge arises from the fact, that external configuration systems usually do not dispose about a variable's declaration information, especially about the type information. All that for example an INI-file stores is externalized string data, that cannot be imported (un-escaped and parsed) to a variable value, without this information.
There are three ways to cope with this problem:
Now, with all this knowledge in place, a step-by step recipe for using built-in type IniFileFeeder can be given and this should be easily understood:
With the start of a software:
Here is a sample for this approach, taken from the Programmer's Manual of module ALox:
With termination of a software:
Following the above sample, this code could look like this:
In case a custom configuration system is to be attached, all information given in the previous chapters should be duly read and understood.
Next, the source code of class IniFileFeeder should be analysed. The good news here is that most INI-file related code resides in separate class IniFile which IniFileFeeder uses.
With that, the code is easily understandable and rationally short to be used as a jump start. In addition, information given in chapter 8. The StringTree in Class Configuration might be helpful.
Another detail to be discussed is that export methods of variable types are allowed to insert NEW_LINE code into the export string. An external configuration system probably has to check if such codes exist and properly handle them. While the easiest way to handle them is of course to simply remove them, this feature allows external configuration systems to smoothly format longer lists of values. Built-in class IniFileFeeder, respectively the internally used helper-type IniFile, creates duly formatted lines and adds a continuation backslash '\'
to the previous line. Duly formatted here means that the values are written in a tabular way. See for example, the following INI-file entry, generated for an ALox variable:
# Meta info format of logger "DEBUG_LOGGER", including signatures for verbosity strings and # astring added to the end of each log statement. # Format: MetaInfo,Error,Warning,Info,Verbose,MsgSuffix FORMAT= "%SF:%SL:%A3%SM %A3[%TC +%TL][%tN][%D]%A1#%#: %V ", \ \ec0, \ \ec3, \ , \ \ec8, \ \e[0m
The built-in string vector types SV,
and SV;
insert a NEW_LINE code after each character.
Session information data is a special sort of external configuration data. Its character is quite volatile: Nothing bad happens if no data is given and storing the information might be a matter of a background task that does this only in certain time intervals. A good example is the recent position of an application window:
Two ways to write session information are proposed in the next subchapters. Both approaches have advantages and disadvantages which are rather obvious and thus are not further elaborated here.
The right and preferred way to manage session information is to create a dedicated session file. Often such data is stored in a temporary folder or in folder ~/.cache
, /tmp
or similar. Using the ALib configuration system provides all necessary tools needed to quickly implement a session file. Here are the steps to do so:
Then use method Export to export the subtree of session variables. For example:
myIniPlugin.Export( "/MYAPP/SESSION" )
and then write the INI-file with ExportEnd
A different approach might be taken if a software uses a write-enabled configuration data source. Of course, session information can then be stored along with the read-only data.
This module provides for example built-in type IniFileFeeder which provides an explicit interface to cope with session data. Here are the steps to implement mixed configuration and session information using this type:
With the above in place, those entries that have the writeback-flag set, are overwritten by future application runs, in contrast to those entries that are only written once when an empty INI-file is populated for the first time.
As of today, only one built-in variable that have a notion of session data exist with ALOX/LOGGERNAME/AUTO_SIZES. This is exposed by module ALox.
It has been mentioned already several times in this manual, that class Configuration inherits class containers::StringTree. A bigger portion of its logic builds on this base type. Consequently, class Variable encapsulates a StringTree::TCursor and with that it is just a lightweight pair of pointers (which even fits into a Box).
In some special situations, for example if a custom plug-in or "feeder" of external configuration data is to be written, it is necessary to use the two underlying interfaces.
This chapter of the manual should give some hints for doing that.
Class Configuration inherits class StringTree
directly and publicly. Hence, all functionality is directly accessible. When using the StringTree interface, often values of type StringTree::Cursor are involved. This type provides method StringTree::TCursor::Tree, which, for convenience, allows providing an optional template type to statically cast to.
Thus, when casting a StringTree instance back to class Configuration, instead of using the rather unhandy syntax:
static_cast<Configuration*>( myCursor.Tree() )
the better way to phrase this, is:
myCursor.Tree<Configuration>()
Class Variable inherits class StringTree::TCursor in a protected
fashion. The rationale for this is not to completely hide the cursor-interface. Instead, this is meant to duly hide the interface for the 99% of use-cases where a variable is used in code.
Because class Variable
does not add any field-members, a variable can be "converted" into a cursor and vice versa. Conversion to a cursor is provided with method Variable::AsCursor, which is just a static cast! The cursor can be manipulated freely. Once done, it can be used as a constructor argument of class Variable.
Note that not any position in the tree that a cursor may represent, in fact is a variable. Instead, a cursor can point just be node in the path to a variable! For an example, let us look at some ALib variables:
/ALIB/LOCALE /ALIB/WAIT_FOR_KEY_PRESS /ALIB/HAS_CONSOLE_WINDOW
If a cursor was generated from one of these variables and next method TCursor::GoToParent. was invoked, and then this cursor was passed to the constructor of class Variable, then this variable was illegal.
This simple sample already demonstrates why the interfaces of class StringTree and Cursor are hidden: It needs some core understanding of the internal mechanics to safely use them.
To probe a cursor for either being a variable or just a node of a path in the tree, a temporary variable can be constructed from the cursor and method IsDeclared invoked. If false
is returned, the cursor represents a path node.
When traversing the internal StringTree of variables, a first-level node named "$PRESETS"
will be found. Beyond this node, class Configuration
stores information provided with method PresetImportString. This node and its children have to be skipped explicitly when traversing the tree.
The ALib variable system provides a mechanism that enables a software to monitor changes that occur in the tree of data. For this, a custom implementation of abstract interface type ConfigurationListener is to be created and registered with a configuration instance.
There are three types of events that can be monitored. They are defined with enumeration ConfigurationListener::Event:
true
. With that, this event is used to monitor changes of a variable's value.While there is only one interface type ConfigurationListener, which only has one simple virtual notification callback function, five different ways of registering such listener for monitoring one or more variables exist. Those are:
Each method accepts four parameters:
To de-register a listener, parameters two to four have to be specified exactly as with registration, along with the first parameter being ContainerOp::Remove. If this is not done, de-registration fails and the listener remains set which may cause undefined behavior. Therefor, in debug-compilations, an ALib warning is raised if the exact combination of the parameter-set was not found internally.
An alternative, more convenient and in most cases sufficient method to de-register a listener is offered with method MonitorStop, which removes all registrations for a given listener.
Class Configuration, acts as the container type for all variable data. With its inheritance of class StringTree of camp ALib Containers, it fully implements this camp's concept of weak monotonic allocations. The challenge of extending the recycling capabilities of class StringTree to the user-defined variable content types, was resolved by adding a PoolAllocator which uses the MonoAllocator of the underlying StringTree.
As a result, infinite insertions and removals of similar data will not cause a "memory drain". Instead, all allocated memory is re-used and hence the processes' heap memory remains completely un-fragmented, because all that class Configuration allocates are several bigger blocks of memory, until the "weighted limit" of the necessary space is reached.
Because this introduces a very nice showcase for the mentioned concepts of module ALib Monomem, its Programmer's Manual provides chapter 6.3 Using Meta-Info Singletons to Reclaim Allocation Information, which discusses implementation details of class Configuration.
This ALib Camp does not provide built-in facilities to avoid racing conditions in multithreaded processes. It is up to the user of the library to make sure that no conflicting operations are made. (Note that this is the common approach. For example, the container classes in namespace std
do not provide such mechanics).
With compiler symbol ALIB_DEBUG_CRITICAL_SECTIONS set, the underlying StringTree receives assertion mechanics to detect forbidden parallel access. More information on how this works is provided with chapter 1.4.2 Asserting Critical Sections' Entry And Exit of the Programmer's Manual of module ALib Threads.
To implement protection against race conditions efficiently, some understanding of the internal implementation is necessary.
The general rule is:
This is again very similar to container classes in namespace std. The good news is that accessing a distinct existing variable is fully independent of parallel insertions or deletions of other variables in the tree. Precisely, the following rules are guaranteed:
true
the variable can also be accessed.As learned in the previous section, only insertions and deletions of variables interfere with each other and with cursor operations in the same subtree of a configuration.
As described chapter 4.6 Customizing The Bootstrap Process of the general manual of ALib, different ALib Camps and custom camps may share a single configuration object and it is allowed and recommended to use this same configuration object to store custom configuration data. The variables that are stored by ALib Camps are listed in the corresponding reference manual.
The only occurrence of insertions and deletions of variables built-in with any ALib Camp is when instances of class TextLogger are inserted to or removed from a Lox. Now, in common applications, loggers are inserted during bootstrapping and removed with the shutdown phase of a software. With that, such process can rely on the fact that ALib will not perform insertions or deletions of variables while multi-threading is in place. Consequently, only custom insertions and deletions have to be protected.
The other way round: If a multithreaded software inserts or removes TextLogger instances during its lifetime, such operations have to be protected against each other and against any other insertion and removal operation of custom variables.
This camp ALib Configuration can be very well used for other purposes than storing "just" configuration data. If so, it is recomendet to not rely on the built-in instance shared with the ALib Camps, but rather created a dedicated Configuration object. Such object is then under complete control of the application, independent of other camps and thus not subject to race conditions introduced by such.
While the motivation to introduce this ALib Camp results from the need of general configuration data of almost any software that is a little more complex than a "Hello World" application, and thus perfectly solves the problem of prioritizing different external data sources, this camp may be very well suited for other use cases.
Generally spoken, this camp offers named run-time variables, which in contrast to using simple flat hash tables, are organized hierarchically and furthermore support different levels of write-access rights.
With that, many structured data models (e.g., all that is represented in widespread JSon or XML files) can be easily implemented using this library.
For example, a CAD software could store all model data, along with "materials", "colors", etc. in a configuration tree. From a performance perspective, repeated read-access to such data can be implemented in a way that is just as performant as accessing a simple C++ pointer. This is due to the fact that class Variable is a simple pair of pointers and when accessing its value, only the second pointer is even used. Furthermore, values of variables can be stored by a software and as explained in chapter 11. Multi-Threading / Racing Conditions, it is ensured that these pointers remain valid, even if parallel insertions or deletions of other variables occur.
Next, by accessing the underlying StringTree cursor (with method Variable::AsCursor) the library allows traversing and inspect the data model without "knowing" about which variable (names) are existing.
This is all that should be said in this final chaper. An experienced software engineer can easily judge about the fields of application of the ALib Camp.