Most applications don't need the flexibility of the low-level API (with kdbGet
, kdbSet
, etc.). In most cases an easy and safe way to
access the configuration values is preferred. This is why we created the high-level API.
There are two different ways of using the high-level API:
- directly
- via code-generation
The recommended way is via code-generation (which will be explained below). If you want to use the high-level API directly take a look at its documentation. Please note, however, that certain features are only available through code-generation.
The code-generation API builds on Elektra's specifications. Instead of just using specifications at runtime, the code-generator parses them when invoked (ideally right before compiling) and utilizes the specification when generating configuration accessor functions.
The process of writing a full specification for your application is beyond the scope of this guide. We will just focus on the parts that are necessary for using the code-generation API.
We will use this specification in the format of the ni
plugin:
[]
mountpoint = myapp.ini
[mydouble]
type = double
default = 0.0
[myfloatarray/#]
type = float
default = 1.1
In Elektra a specification is defined through the metadata of keys in the spec
namespace. The specification above contains metadata for three keys:
- the parent key (
@
) @/mydouble
@/myfloatarray/#
(The#
at the end ofmyfloatarray/#
indicates that it is an array)
The mountpoint
metadata on the parent key sets the name of our application's config file (the location is defined by Elektra), it should
be unique.
The type
metadata specifies the type of key. The available types can be found in the high-level API
Readme under "Data Types". It is important to set the type
, because the code-generator will ignore all
keys that don't have a type
.
Because we want our getters to be unable to fail (makes error handling trivial), we need to provide a default
value as well. Note that
default
s for array keys like myfloatarray/#
only work via the spec
plugin. If you didn't mount everything correctly, you will get an
error.
That's it. The code-generator just requires that each key (that you want to access) has a type
and a default
value.
Note: You can also mark keys with the require
metadata, if there is no reasonable default value. This is only recommended as a last
resort, but still preserves the guarantee that elektraGet*
calls won't fail. If a require
d key is missing, the initialization of the
Elektra
handle will fail.
The code-generator is a very powerful and flexible tool and has many options to tweak its output. If you want to know more about how to set up
everything just the way you want to, take a look at the man-pages kdb-gen(1)
and
kdb-gen-highlevel(1)
.
To get started the basic invocation of the code-generator should be enough:
kdb gen -F ni=spec.ini highlevel "/sw/example/myapp/#0/current" conf
This tells the code-generator that your application uses the parent key /sw/example/myapp/#0/current
and that the output files should be
called conf.*
. The argument highlevel
just specifies which template to use and the option -F ni=spec.ini
indicates that the file
spec.ini
(in the ni
plugin's format) contains the specification. While the code-generator can read a specification from the KDB, we
recommend you use the -F
option. It keeps the KDB clean and can avoid troubles later on, when installing your application.
You can now take a look at conf.h
and conf.c
(the files generated by the compiler). Depending on the specification you used, these files
may be very long. They might also be formatted strangely, because of limitations in the code-generator. Feel free to reformat them with your
tool of choice, before inspecting them.
To explain how to use the generated code, lets take a look at some of conf.h
. For brevity's sake some parts of the file have been replaced
by placeholder comments.
/* file header ... */
#ifndef CONF_H
#define CONF_H
#ifdef __cplusplus
extern "C" {
#endif
/* includes ... */
/* helper macros ... */
First there is some boilerplate, including the copyright header, include statements and some helper macros.
/**
* Tag name for 'mydouble'
*/
#define ELEKTRA_TAG_MYDOUBLE Mydouble
/**
* Tag name for 'myfloatarray/#'
*
* Required arguments:
* - kdb_long_long_t index1: Replaces occurrence no. 1 of # in the keyname.
*/
#define ELEKTRA_TAG_MYFLOATARRAY Myfloatarray
Next we see all the 'tag macros' used to refer to config values. These are essentially just aliases, but they allow for some flexibility
in how we generate the names of the static inline
functions further down. You should always refer to your config values via these macros,
even if they are just aliases. This is because we might have to change the naming scheme for the functions, but we will try to keep the tag
macros unchanged.
Additionally, the comments for these macros contain the documentation on what arguments are needed for accessing the tag in question. For
example to access the elements of the array myfloatarray/#
, we obviously need to provide an index.
#define elektra_len19(x) ((x) < 10000000000000000000ULL ? 19 : 20)
/* local macros ... */
#define elektra_len(x) elektra_len00 (x)
Then we see some local helper macros only used in this file.
static inline kdb_double_t ELEKTRA_GET (Mydouble) (Elektra * elektra) { /* ... */ }
static inline void ELEKTRA_SET (Mydouble) (Elektra * elektra, kdb_double_t value, ElektraError ** error) { /* ... */ }
static inline kdb_float_t ELEKTRA_GET (Myfloatarray) (Elektra * elektra, kdb_long_long_t index1) { /* ... */ }
static inline void ELEKTRA_SET (Myfloatarray) (Elektra * elektra, kdb_float_t value, kdb_long_long_t index1, ElektraError ** error) { /* ... */ }
This is the most important part of the header. It is what makes the API work.
For each config value we generate an ELEKTRA_GET(*)
and an ELEKTRA_SET(*)
accessor function. All these functions are static inline
,
because they just call other getter/setter functions with partially fixed arguments. In fact many of these functions will only be a single
line.
#undef elektra_len19
/* local macros ... */
#undef elektra_len
int loadConfiguration (Elektra ** elektra, int argc, const char * const * argv, const char * const * envp, ElektraError ** error);
void printHelpMessage (Elektra * elektra, const char * usage, const char * prefix);
void exitForSpecload (int argc, const char ** argv);
Then we undefine the local macros we defined before and declare the three initialization functions loadConfiguration
, printHelpMessage
and exitForSpecload
.
/* elektra* macros ... */
#ifdef __cplusplus
}
#endif
#endif // CONF_H
At the end of the file you will find the elektra*
convenience macros. These macros can be used to make accessing config values look more
like normal function calls and avoid the ugly double parentheses in e.g. ELEKTRA_GET (...) (...)
.
We start at the bottom of our conf.h
excerpt. exitForSpecload
is used to initiate specload mode, if needed. This mode makes your application
provide its specification to Elektra. How this works exactly is not so important (see specload plugin).
You only need to know, that exitForSpecload
should be called immediately at the start of your main
function and that it only returns, when
your application is not in specload mode.
int main (int argc, const char * const * argv, const char * const * envp) {
exitForSpecload (argc, argv);
// ...
}
To access your configuration, you first need to call loadConfiguration
to get an Elektra
handle for your application. This is done via
a snippet that is more or less the same for all applications:
ElektraError * error = NULL;
Elektra * elektra = NULL;
int rc = loadConfiguration (&elektra, argc, argv, envp, &error);
if (rc == -1)
{
fprintf (stderr, "An error occurred while opening Elektra: %s", elektraErrorDescription (error));
elektraErrorReset (&error);
exit (EXIT_FAILURE);
}
if (rc == 1)
{
// help mode - application was called with '--help'
// for more information see "Command line options" below
printHelpMessage (elektra, NULL, NULL);
elektraClose (elektra);
exit (EXIT_SUCCESS);
}
Next it is recommended, you change the default handler for fatal errors. By default, we just call exit (EXIT_FAILURE)
, since we don't know
how you log your errors and what cleanup may be needed.
elektraFatalErrorHandler (elektra, onFatalError);
onFatalError
will receive the fatal ElektraError *
. It must at least call elektraErrorReset
on the error and then call exit()
.
If you want to try out the application immediately, skip down to the section about compiling. You may also have to follow the section on running your application to get everything up and running.
Once you have your Elektra
instance, reading config values is easy. You just call one of the getter functions.
kdb_double_t mydouble = elektraGet (elektra, ELEKTRA_TAG_MYDOUBLE);
Here we used the convenience macro elektraGet
. You could also invoke the static inline
accessor function directly:
kdb_double_t mydouble = ELEKTRA_GET (ELEKTRA_TAG_MYDOUBLE) (elektra);
No error handling is required, because getter functions are designed to not fail. In a correct setup, either the initialization fails and getters are never called, or getter calls always succeed.
To access config values that don't have a static key name, like arrays, you have to supply additional arguments (and use elektraGetV
):
kdb_float_t myfloat0 = elektraGetV (elektra, ELEKTRA_TAG_MYFLOATARRAY, 0);
kdb_float_t myfloat1 = elektraGetV (elektra, ELEKTRA_TAG_MYFLOATARRAY, 1);
// or
kdb_float_t myfloat0 = ELEKTRA_GET (ELEKTRA_TAG_MYFLOATARRAY) (elektra, 0);
float myfloat1 = ELEKTRA_GET (ELEKTRA_TAG_MYFLOATARRAY) (elektra, 1);
Of course, we also need to know how big the myfloatarray/#
array actually is. To that end we can use ELEKTRA_SIZE
or elektraSize
:
kdb_long_long_t myfloat_size = elektraSize (elektra, ELEKTRA_TAG_MYFLOATARRAY);
// or
kdb_long_long_t myfloat_size = ELEKTRA_SIZE (ELEKTRA_TAG_MYFLOATARRAY) (elektra);
ELEKTRA_SIZE
functions like their ELEKTRA_GET
counterparts are designed to not fail.
Please note that even if you set up everything correctly, calling a getter on a non-existent, wrongly typed or otherwise inconvertible key is a fatal error. All fatal errors result in a call to the fatal error handler and therefore will exit the application.
Writing config values is not quite as easy as reading, but it is still quite simple:
ElektraError * error = NULL;
ELEKTRA_SET (ELEKTRA_TAG_MYDOUBLE) (elektra, 3.141593, &error);
if (error == NULL) {
// handle error
elektraErrorReset (&error);
}
As you can see the complexity stems from the necessary error handling. Because setting values involves IO and other uncontrollable factors,
setter calls cannot be designed to not fail. This is why they accept an additional ElektraError **
argument. It is important to call
elektraErrorReset
, if an error was set. Calling a setter with a non-null ElektraError **
parameter is a fatal error.
Of course, you can also use elektraSet
(error handling omitted):
elektraSet (elektra, ELEKTRA_TAG_MYDOUBLE, 3.141593, &error);
elektraSetV (elektra, ELEKTRA_TAG_MYFLOATARRAY, 2.718282f, &error, 2);
Note that elektraSetV
takes the ElektraError
argument before the variable arguments, while in ELEKTA_SET
the error is always the last
argument. This is because of limitations in the C macro system.
There is no setter for array sizes. Since Elektra's low-level part supports discontinuous arrays, we simply change the array size whenever necessary, if an array element setter is called. However, the high-level API has no support for discontinuous arrays, so take care not to create holes in your arrays, if you want to iterate over them. Remember, accessing non-existent keys (and this includes array elements) is a fatal error.
The generated loadConfiguration
function automatically mounts the gopts
plugin. This means that command-line options (as described
here) are parsed and their values are set on the corresponding keys. You don't have to do anything, apart from
setting the opt
metadata. The only exception to that is the help mode.
When your application is called with -h
or --help
, we enter help mode. This is indicated by the return value 1
of loadConfiguration
.
As you can see in the example above, you should call printHelpMessage
to print an appropriate help message to stdout
and then close the
allocated Elektra
instance and exit
. Beware an Elektra
instance created in help mode may not be fully functional, which is why you
should immediately close it once you called printHelpMessage
.
The code-generator has some more advanced features that are supported out of the box. For example, you can read multiple config values at once by utilizing structs. The use of structs also allows for recursive configurations like menus (a menu can have submenus).
For more information take a look at the man-page kdb-gen-highlevel(1)
.
Once you've written your application, you will want to compile it. This requires linking some libraries and adding to your include path. The easiest way is to use CMake or pkg-config to find the needed compiler options. Examples on how set this up can be found in here and here.
The compiler invocation should look something like this:
cc myapp.c conf.c `pkg-config --cflags --libs elektra-codegen` -I. -o myapp -Wl,-rpath `pkg-config --variable=libdir elektra-codegen`
Note: At least C99 is required, so if your compiler defaults to an older version you'll need to add -std=c99
.
Running your application is easy, just run the executable (e.g. myapp
). While this might work out of the box, you will just get the default
configuration. To change the configuration you need to use kdb
, which doesn't know about your specification yet. This means you would need
to set the type
metadata and all the other stuff that your application expects by hand. For every single key. Obviously this is not the
right solution.
A better solution is to inform Elektra (and kdb
) about our specification. Then Elektra automatically copies metadata to where it should be.
First you need to mount your specification itself into the KDB. Mounting is basically the process of informing Elektra about a new part of the KDB, similar to how mounting an external hard drive informs the OS about a new part of the file system.
sudo kdb mount -R noresolver /etc/myapp_spec.eqd "spec:/sw/example/myapp/#0/current" specload app="$PWD/myapp"
The command above assumes that you also used the kdb gen
command from above and that the myapp
executable
is located in $PWD
.
Note: Because of a limitation in
specload
, we have to use thenoresolver
resolver. This also means that the path to the config file (here/etc/myapp_spec.eqd
) has to be absolute. Otherwise, it will always be relative to the current working directory in whichkdb
or your application was executed. The file should not exist when callingkdb mount
.specload
works different to other plugins. The given config file is only used, if the user makes changes to the specification viakdb set
.
Now that Elektra knows about your specification, calling your application might work better since metadata should now be copied when you
set a config value via kdb set
. However, there won't be any type checking. For that we need to enable the type
plugin. While this could
be done manually, we can just let Elektra figure out which plugins we need and activate all of them.
This can be done with the spec-mount
command:
sudo kdb spec-mount "/sw/example/myapp/#0/current"
Now finally your application is all setup.
To configure your application you can use kdb
:
kdb set "/sw/example/myapp/#0/current/mydouble" 15.4
If you want to set a value system-wide (not just for your user) you can use the system namespace:
kdb set "system:/sw/example/myapp/#0/current/mydouble" 15.4
Always use the cascading version of kdb set
(i.e. the keyname begins with a slash /
), otherwise type checking and other plugins might not
be called correctly.