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
http://wiki.openwrt.org/doc/techref/uci
https://forum.openwrt.org/viewtopic.php?pid=183335#p183335
Posted 2015-12-30