sil2100//vx developer log

Welcome to my personal web page

UniConf: part I

In this post, I will try to write about how to use the basic UniConf API. This is an unofficial guide to UniConf, so be advised. I will concentrate on the native C++ version of the API. This can be thought of as something like a small UniConf tutorial.

2011-03-26

Empty

If you read my previous UniConf post, you probably have an overview of how simple and straightforward it can be. Everything starts off with the creation of an UniConfRoot object defining our UniConf configuration root. If we interpret the configuration tree as a hierarchical tree similar to those in file systems, the UniConfRoot is the variable tree that is mounted at the root path ("/"). Its constructor, in one of the most commonly used forms, accepts a moniker string as the argument. Monikers are strings used to represent generators available in a UniConf system. I already mentioned some of them previously. Using generators allows using different configuration system backends and modificators. Monikers can be mixed together.

Some of the available monikers:

By defining the root UniConf entry, we need to decide what generators we want to use. Let us consider 2 .ini files we would like to use as part of our configuration: foo.ini and bar.ini.

foo.ini:


hostname = ala.lan
ip_address = 192.168.0.41/24
gateway = 192.168.0.3

bar.ini:


[misc]
welcome_text = Hello world!

[terminal]
width = 90
height = 40

As stated above, we first start by defining the UniConfRoot. We will use both files at once. Consider the following code:


#include <iostream>
#include <wvstreams/uniconfroot.h>

using namespace std;

int main(void)
{
    UniConfRoot root("cache:list:ini:foo.ini ini:bar.ini");

    UniConf alias(root["/terminal"]);

    cout << "hostname = " << root["hostname"].getme().cstr() << endl;
    cout << "/terminal/height = " << alias["height"].getme().cstr() << endl;

    root["hostname"].setme("yuki.lan");
    root.commit();

    cout << "hostname (new) = " << root["hostname"].getme().cstr() << endl;

    return 0;
}

We defined the UniConfRoot to include caching of values for faster accessibility, and told UniConf to use a list containing two ini: generators for fetching the configuration variables. This means that in result, mounted on top of the UniConf tree we will have the contents of our two .ini files at once. Once we want to access a variable, UniConf will browse the whole list looking for the variable definition.

Almost every object in a UniConf tree is of UniConf type. This is quite intuitive, because if we consider the configuration tree as a directory hierarchy tree, even the root of the tree is in fact just another directory. The UniConf type (and, since UniConfRoot is his derivative, this type as well) provides a handy [] operator by which the programmer can easily access variables with a given path relative to that variable. As argument, this operator takes a UniConfKey - a very useful type for UniConf path storage. But since an UniConfKey can be constructed from standard C strings, we can insert a string as the variable path to the [] operator. The resulting UniConf object representing the given variable/object relative to the queried object is returned.

In our example above, the code root["hostname"] will return the object corresponding to the foo.ini's hostname variable, which's VFS-like path is /hostname (because we mounted the configuration file at the root). .ini file sections act as directories used for holding section variables. After defining the root, we define an 'alias' to the terminal section present int the bar.ini file. What I called an 'alias' is nothing more than simply the UniConf object to the terminal object (section). We can now use the alias object to access variables in the terminal .ini section. So, now, instead of writing root["terminal/height"] we can use alias["height"].

Now for fetching variable values. Every UniConf object exports a getme() method that can be used for this purpose. The method returns a WvString, which is a wvstream library string format. In the case when you do not want to use any other elements from the wvstream library instead of the UniConf part - WvString's provide a cstr() method that returns the standard C char string equivalent of the string.
The same way setting variables can be done. Analogously, every UniConf object has a setme() method. As an argument it reqires a WvStringParm (aka WvFastString) object, which is nothing more than a faster WvString for passing function parameters. It's faster and more memory-efficient, because these objects are created only from const char *'s. It does not allocate its own memory for holding the string and copying it, but uses the const char * directly. They are not advised to be used for other purposes. We can therefore use root["hostname"].setme("yuki.lan"); for modifying the contents of the variable hostname to yuki.lan.

After we set the new value of the variable, we want to make sure the change has been propagated to the given configuration subsystem. That is why we commit the changes with a root.commit() call. After changes are committed, the new variable values will be saved to their corresponding .ini files. We can then check if the change really happened by reading the value again and checking the configuration files later.

But now, let us suppose that we have a new configuration tree we want to attach to our configuration system. We could of course define a new, detached UniConfRoot for this purpose, but let us suppose we want it present in the tree we already have. Consider adding the following code.


(...)
    root["/tcp"].mount("tcp:localhost:4111");
    root["/tcp/test"].setme("something");
    cout << "From uniconfd = " << root["/tcp/test"].getme().cstr() << endl;
(...)

The mount() method mounts a given generator at the selected UniConf key. In this case, we try to mount the tree exported by an uniconfd server running on localhost's port 4111 to the key at path /tcp. This way we can extend our UniConf configuration tree dynamically using a similar scheme to the one present on Unix filesystems.
For this example to work, one would need a running uniconfd server on localhost. The simplest way would be running uniconfd in the following way:


# As for uniconfd version 4.6.1
uniconfd -l tcp:4111 /=temp:

This way we will have an uniconfd server running an empty in-memory configuration - consult the uniconfd manual and help text for more details. Since we use the temp: generator, the /tcp configuration tree is empty by default. So to have any variables to access, we have to create them by ourselves first.

One last thing I will explain during this post are iterators. UniConf provides us with a handful of different iterators that can iterate through our variable tree. The most basic one is UniConf::Iter, which simply iterates through all immediate children of an UniConf node. This means we can use this iterator to browse through variables on one level, not looking into sub-branches of the tree (like iterating through files in a given directory, not including sub-directories). If we want a depth-first recursive search of a given branch, we can use the UniConf::RecursiveIter iterator. There are also sorted iterator versions of the two - the UniConf::SortedIter and UniConf::SortedRecursiveIter - which traverse the variables in alphabetic order, by full path.
Consider the following piece of code:


(...)
    UniConf::RecursiveIter i(root);
    for (i.rewind(); i.next(); )
        cout << i.ptr()->fullkey().cstr() << " = " << i.ptr()->getme().cstr() << endl;
(...)

We create a recursive iterator to browse through our whole tree. We first call rewind() to position the iterator on the beginning of the branch in mention, and then move the iterator through consequent next() calls at every iteration. To get the object currently pointed at by the given iterator, all we need to do is a ptr() call. The fullkey() is another UniConf method that returns us the UniConfKey object of the given variable. It contains the full path to the variable. We print it along with the variable's value.

This is all for today's post. In the nearest future I will try to explain some other, maybe a little less basic aspects of UniConf, such as notifications, copying and many others. But as you have probably noticed by now, UniConf is simple. Very simple. And that makes it so interesting.

You can download the source code used in this post (with a small edition and a Makefile) [here].
Remember to have UniConf development libraries and headers installed beforehand - and uniconfd, if you want to check how fetching through the tcp: generator works.