Skip to content
This repository has been archived by the owner on Oct 15, 2024. It is now read-only.

Latest commit

 

History

History
384 lines (274 loc) · 16.8 KB

highlevel.md

File metadata and controls

384 lines (274 loc) · 16.8 KB

High-level API (with code-generation)

Most applications don't need the flexibility of the low-level API (with kdbGet, kdbSet, etc.). In most cases an easy way 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:

  1. directly
  2. 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.

Overview

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.

Writing a specification

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:

  1. the parent key (@)
  2. @/mydouble
  3. @/myfloatarray/# (The # at the end of myfloatarray/# 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 a 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 defaults 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 required key is missing, the initialization of the Elektra handle will fail.

Invoking the code-generator

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 setup 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.

Using the generated code

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 of 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, 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 (...) (...).

Obtaining an Elektra handle

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 ** argv) {
    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, &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 '-h' or '--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.

Reading config values

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, while it shouldn't happen, if you setup 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 exit the application.

Writing config values

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 not 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.

Command-line options

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.

Advanced concepts

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).

Compiling your application

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

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.

Mounting the specification

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 the noresolver resolver. This also means that the 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 which kdb or your application was executed. The file should not exist when calling kdb mount. specload works different to other plugins. The given config file is only used, if the user makes changes to the specification via kdb 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.

Configuring your application

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 -N system:

kdb set -N 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.