Wed, 30 Dec 2015 00:00:00 +0100
c, linux, openwrt, software, uci

UCI-Konfiguration für C-Programme

Wenn ich ein komplexes Programm schreibe, komme ich irgendwann an den Punkt, wo ich mir Gedanken über die Konfiguration desselben machen muss. Zwar ist es in vielen Fällen besser, wenn mein Programm die optimalen Einstellungen selbst findet, aber bereits etwas so Triviales wie das Verzeichnis für Dateien, die mein Programm schreibt, möchte ich lieber zur Laufzeit mitteilen, als fest einprogrammieren.

Eine einfache und universelle Möglichkeit zur Konfiguration bietet die Kommandozeile, sprich die Argumente, die beim execve() Systemaufruf übergeben werden. Dafür gibt es etablierte Konventionen und Bibliotheken, um übergebene Optionen auszuwerten.

Will ich ein Programm nur mal schnell von der Kommandozeile aufrufen, dann ist es - gerade bei langen Optionen - mühselig, jedesmal die Liste der Optionen aus dem Gedächtnis abzurufen und einzugeben. Die History-Funktion der Shell hilft mir dabei erst ab dem zweiten Aufruf.

Dann wünsche ich mir eine Möglichkeit, die Konfiguration für die Laufzeit permanent abzulegen. Und an diesem Punkt explodiert die Anzahl meiner Optionen. Ich kann die Konfiguration aus dem Netz bekommen (zum Beispiel via DHCP oder DNS), von lokalen Prozessen oder aus Dateien. Bei der Konfiguration aus Dateien habe ich die geringsten Abhängigkeiten zu anderen Prozessen und Systemen, mein Programm kann also relativ autark arbeiten.

Traditionell verwendet man unter Unix Textdateien für die Konfiguration. Dafür gibt es mehrere Gründe. Die beiden wichtigsten sind für mich, dass a) Textdateien sich in so ziemlich jeder Programmiersprache und auf so ziemlich jedem Rechner portabel verwenden und b) mit jedem simplen Editor bearbeiten lassen.

So weit so gut, nur benötigen die Textdateien eine gewisse Struktur, wenn sie von Programmen interpretiert werden sollen. Und hier kommt die nächste Explosion meiner Möglichkeiten: es gibt mittlerweile so viele Konventionen für Konfigurationsdateien, dass es unnötig ist, neue zu erfinden (obwohl auch das hin und wieder noch geschieht).

Das Problem liegt eher darin, ein geeignetes Format und eine Bibliothek dafür auszuwählen und mit dem eigenen Programm zu verknüpfen.

Ich lege in meinen Programmen die Konfiguration während des Ablaufs gern in einer Struktur ab, die ich effizient benutzen kann. In C beispielsweise in einem struct, der die Werte für die Benutzung im Programm optimal aufbereitet hat. Dann lagere ich das Befüllen dieser Struktur aus der Konfigurationsdatei in eine eigene Funktion aus und bin in meinem Programm unabhängig von der Konvention der Textdatei. Diese kann ich dann am Zielsystem orientieren.

Ein Programm, an dem ich in letzter Zeit schreibe, soll auf OpenWrt-Systemen laufen. Diese werden mit UCI, dem Unified Configuration Interface konfiguriert. Damit haben (fast) alle Konfigurationsdateien für OpenWrt-Systeme die gleiche Syntax.

Für viele Programme, die nach OpenWrt portiert wurden, gibt es Wrapper, mit deren Hilfe aus der UCI-Konfiguration beim Start des Programms eine für das Programm geeignete Konfiguration erzeugt wird. Bei einem neuen Programm kann ich jedoch gleich die direkte Konfiguration mit UCI verwenden.

Die nächste Frage ist dann, ob das Programm so auch auf anderen Systemen konfiguriert werden kann. Diese Frage kann ich bereits vorab mit Ja beantworten um mich im Rest des Artikels nun mit dem Wie zu beschäftigen.

Die technische Referenz zu UCI findet sich im Wiki von OpenWrt, darauf gehe ich hier nicht ein. Etwas zu kurz gekommen ist dort die Beschreibung, wie man UCI auf anderen Systemen installiert und wie man in C dafür programmiert. Diese Informationen kann man im Netz finden, hier habe ich sie zusammengetragen.

Installation von UCI

Um UCI auf anderen Systemen, zum Beispiel Debian oder Fedora zu installieren, benötige ich die C-Entwicklungsumgebung, Cmake und Git. Außerdem benötigt libuci zum Kompilieren libubox, welches ich vorher installieren muss.

Installation von libubox

Die einfachste Möglichkeit, libubox zu besorgen, ist das Repository zu klonen.

git clone git://nbd.name/luci2/libubox.git
cd libubox

Ich muss libubox nicht im selben Verzeichnis kompilieren und kann mir dieses sauber halten, falls ich die Bibliothek mit verschiedenen Optionen kompilieren will. Dazu lege ich mir ein neues Verzeichnis für das Kompilieren an:

mkdir build
cd build
cmake ..
make ubox

Das Makefile enthält kein Target für die Installation, also muss ich die Dateien selbst kopieren und benötigte Verzeichnisse vorher anlegen:

sudo mkdir /usr/local/include/libubox
sudo cp ../*.h /usr/local/include/libubox/
sudo cp libubox.so /usr/local/lib

Damit der dynamische Linker die Bibliotheken findet, muss ich sie ihm noch bekannt machen:

sudo ldconfig -v /usr/local/lib

Dabei achte ich darauf, dass libubox.so in der Ausgabe mit erscheint. Damit die Bibliotheken in /usr/local/lib auch auch einem Neustart gefunden werden, achte ich darauf, dass /usr/local/lib in /etc/ld.so.conf verzeichnet ist.

Installation von uci / libuci

Auch bei libuci klone ich zunächst das Repository:

git clone git://nbd.name/uci.git
cd uci

Bei libuci kann ich kein separates Verzeichnis zum Kompilieren verwenden.

Da ich nur für ein C-Programm übersetze und im Moment kein Lua benötige, schalte ich die Übersetzung dafür ab:

cmake -D BUILD_LUA:BOOL=OFF .
make

Bei libuci habe ich ein Target für die Installation im Makefile, so dass ich die Bibliothek und das Programm einfach durch Aufruf von make installieren kann:

sudo make install
sudo ldconfig -v /usr/local/lib

An dieser Stelle kann ich testen, ob uci funktioniert:

mkdir test
cat > test/test << EOF
config 'test' 'abc'
option 'test_var' 'value'
EOF
uci -c test show test

Die Ausgabe sollte so aussehen:

test.abc=test
test.abc.test_var='value'

Nun kann ich mich wieder meinem Programm widmen.

Konfiguration von C-Programmen mit UCI

Hier soll ein kleines Programm die Konfiguration mit UCI demonstrieren. Dafür benötige ich ein paar Header-Dateien:

#include <stdlib.h>
#include <string.h>
#include <uci.h>

Ich definiere eine Struktur für die Konfigurationsdaten, die ich im Programm verwende.

typedef struct {
    struct {
        char *a_string;
        int   an_int;
    } from_uci;
    char * config_path;
} config_s;

In dieser Struktur verweist config_s.config_path auf das Verzeichnis, in dem UCI die Konfigurationsdatei finden kann.

Die Initialisierung dieser Struktur kapsele ich in der Funktion uc_initialize(), der ich einen Zeiger auf die Struktur als Argument übergebe.

void uc_initialize(config_s *cfg) {

    struct uci_context *ctx;
    struct uci_package *pkg;
    struct uci_ptr      ptr;
    char               *str;

    ctx = uci_alloc_context();
    if (cfg->config_path) {
         uci_set_confdir(ctx, cfg->config_path);
    }
    uci_load(ctx, "ucitest", &pkg);

    #define read_config_string(section,option) \
    str = strdup("ucitest.@" #section "[0]." #option); \
    if (uci_lookup_ptr(ctx, &ptr, str, true) == UCI_OK) { \
         cfg->section.option = strdup(ptr.o->v.string); \
    } \
    free(str);

    #define read_config_int(section,option) \
    str = strdup("ucitest.@" #section "[0]." #option); \
    if (uci_lookup_ptr(ctx, &ptr, str, true) == UCI_OK) { \
         cfg->section.option = atoi(ptr.o->v.string); \
    } \
    free(str);

    read_config_string(from_uci, a_string);
    read_config_int(from_uci, an_int);

    uci_free_context(ctx);

} /* uc_initialize() */

Als erstes muss ich mit uci_alloc_context() einen Kontext für UCI in einer stuct uci_context bereitstellen. Diesen Kontext muss ich vor dem Verlassen meiner Funktion wieder mit uci_free_context() freigeben, um kein Speicherleck zu produzieren.

Mit der Funktion uci_set_confdir() kann ich ein anderes Verzeichnis angeben, in dem UCI nach den Dateien sucht. Gebe ich keines an, verwendet UCI /etc/config/.

Nun kann ich mit uci_load() die Konfigurationsdatei laden. Das zweite Argument gibt das Package an, dessen Konfiguration ich laden will. Dieses entspricht dem Namen der Datei im Konfigurationsverzeichnis. Das dritte Argument ist ein Zeiger auf ein struct uci_package, den ich verwende, um die einzelnen Werte zu bestimmen.

Die beiden Makros read_config_string() und read_config_int() zeigen das generelle Vorgehen beim Auslesen von Einstellungen: mit uci_lookup_ptr() suche ich in der struct uci_package nach einem String, der aussieht wie auf der Kommandozeile beim Befehl uci get. Die Funktion uci_lookup_ptr() liefert UCI_OK als Rückgabewert, wenn alles gut ging, und legt den Wert in einer struct uci_ptr ab. Aus dieser bekomme ich einen C-String mit strdup(ptr.o->v.string) beziehungsweise einen Integer mit atoi(ptr.o->v.string).

In dieser Beispiel-Funktion ist das UCI-Package und somit der Name der Datei im Konfigurationsverzeichnis fest auf "ucitest" eingestellt.

Da die C-Strings für meine struct config_s mit strdup() dynamisch erzeugt werden, stelle ich eine weitere Funktion uc_cleanup() bereit, die diesen Speicher wieder freigibt.

void uc_cleanup(config_s *cfg) {
    if (cfg->from_uci.a_string)  {
        free(cfg->from_uci.a_string);
        cfg->from_uci.a_string = NULL;
    }
} /* uc_cleanup() */

Diese Funktion ist nicht notwendig, wenn die Konfigurationsdaten nur einmal eingelesen werden. Sollte mein Programm aber, beispielsweise nach Empfangen eines Signals, die Konfiguration ein weiteres Mal einlesen, muss ich den Speicher davor freigeben, um keine Speicherlecks zur Laufzeit zu erzeugen.

Die Funktion main() des Beispielprogramms zeigt das Zusammenspiel der Funktionen.

int main(int argc, char **argv) {

    config_s cfg = {
       .config_path = "config",
    };
    uc_initialize(&cfg);

    printf("The string is: %s\n", cfg.from_uci.a_string);
    printf("The number is: %d\n", cfg.from_uci.an_int);

    uc_cleanup(&cfg);
    return 0;

} /* main() */

Am Anfang setze ich mit cfg.config_path = "config" das Konfigurationsverzeichnis. Dadurch sucht UCI die Konfigurationsdateien im Verzeichnis config relativ zum Arbeitsverzeichnis. Setze ich stattdessen cfg.config_path = NULL, verwendet UCI das Standardverzeichnis /etc/config.

Danach initialisiere ich cfg in der Funktion uc_initialize() und kann es anschließend verwenden.

Am Ende gibt die Funktion uc_cleanup() den dynamisch belegten Speicher wieder frei. Das ist hier nicht unbedingt notwendig, da der Speicher beim Programmende sowieso freigegeben wird.

Dieses Programm kompiliere ich mit:

gcc -o ucitest ucitest.c -luci

Die Konfigurationsdatei bearbeite ich mit uci selbst:

touch config/ucitest
uci -c config add ucitest from_uci
uci -c config set ucitest.@from_uci[-1].a_string="Don't panic"
uci -c config set ucitest.@from_uci[-1].an_int=42
uci -c config commit

Statt @from_uci[-1] hätte ich hier auch @from_uci[0] schreiben können, da es nur eine Sektion dieses Namens gibt.

In der Konfigurationsdatei config/ucitest erkenne ich deutlich die Spezialbehandlung für den einfachen Anführungsstrich im Text:

config from_uci
    option a_string 'Don'\''t panic'
    option an_int   42

In der Ausgabe des Programms ist wieder alles gut:

The string is: Don't panic
The number is: 42

Weitere Informationen