Einbinden externer Inhalte
Große Datenmengen direkt im Quellcode unterzubringen, macht Programme unübersichtlich und schwer wartbar. Lookup-Tabellen, Konfigurationsdaten und eingebettete Ressourcen lassen sich besser auslagern. C bietet dafür zwei Mechanismen: die klassische #include-Direktive für Textdaten und die neue #embed-Direktive (seit C23) für Binärdaten.
Beginnen wir mit einer Motivation, warum solche Einbindetechniken nützlich sind. Nehmen wir an, wir benötigen eine Lookup-Tabelle mit 256 Werten. Prinzipiell könnte das Array direkt in den Quellcode geschrieben werden:
#include <stdio.h>
int main() {
const double sine_table[256] = {
0.000000, 0.024541, 0.049068, 0.073565,
0.098017, 0.122411, 0.146730, 0.170962,
// … 240 weitere Werte …
-0.098017, -0.073565, -0.049068, -0.024541
};
printf("sine_table[64] = %.6f\n", sine_table[64]);
}
Natürlich könnte man in diesem Fall die Sinus-Tabelle auch zur Laufzeit berechnen, aber darum geht es hier nicht ...
Solche Tabellen erschweren nicht nur das Lesen des Codes, sondern auch Änderungen. Jede Anpassung der Auflösung oder Genauigkeit erfordert eine manuelle Aktualisierung aller Werte. Es ist sinnvoll, diese Daten auszulagern.
Textdaten einbinden mit #include
Mit der #include-Direktive lässt sich der Inhalt einer Datei während der Präprozessor-Phase einbinden, als wäre er direkt an dieser Stelle geschrieben:
Datei: sine_table.inc
0.000000, 0.024541, 0.049068, 0.073565,
0.098017, 0.122411, 0.146730, 0.170962,
// … alle 256 Werte …
-0.098017, -0.073565, -0.049068, -0.024541
Hauptprogramm:
#include <stdio.h>
int main() {
const double sine_table[256] = {
#include "sine_table.inc"
};
printf("sine_table[0] = %.6f\n", sine_table[0]);
printf("sine_table[64] = %.6f\n", sine_table[64]);
printf("sine_table[128] = %.6f\n", sine_table[128]);
}
Der Programmtext bleibt übersichtlich, während die Tabelle vollständig ausgelagert ist.
Automatische Generierung
Externe Daten lassen sich ohne Zusatzwerkzeuge direkt auf dem System erzeugen. Unter Unix steht fast immer bc zur Verfügung, ein kleiner Rechner mit eigener Sprache. Viele kennen das Programm nicht, aber es wird im Linux-Umfeld häufig genutzt, wenn Skripte Berechnungen durchführen müssen und keine weitere Programmiersprache installiert werden können. bc ist dafür ausreichend und kann auch Schleifen und Funktionen ausführen.
Das folgende bc-Skript erzeugt eine Sinustabelle mit 256 Einträgen:
scale = 6 /* 6 Nachkommastellen */
/* pi über arctan(1) */
pi = 4*a(1)
for (i = 0; i < 256; i++) {
angle = 2*pi*i/256
value = s(angle)
if (value == -0) value = 0
print value
if (i < 255) {
print ","
if ((i + 1) % 8 != 0)
print " "
}
if ((i + 1) % 8 == 0)
print "\n"
}
print "\n"
Das Skript wird aus der Shell aufgerufen und die Ausgabe in eine Datei umgeleitet:
$ bc -l sine_table.bc > sine_table.inc
Das C-Programm bleibt unverändert. Beim nächsten Kompilieren wird automatisch die neu erzeugte Datei eingebunden, da #include lediglich Text in den Quelltext einfügt.
Binärdaten einbetten mit #embed
Während Textdateien problemlos mit #include eingebunden werden können, bietet C (seit C23) auch eine Möglichkeit, Binärdateien zur Kompilezeit einzubetten. Dafür kann die #embed-Direktive verwendet werden. Der Inhalt einer Ressourcendatei wird dabei als Sequenz ganzzahliger Konstanten expandiert, typischerweise zur Initialisierung von Arrays.
(Quelle: ISO/IEC 9899:2024, § 6.10.12 Embed directive)
Die Syntax entspricht der von #include:
- Spitze Klammern
<datei>für systemweite Suchpfade - Anführungszeichen
"datei"für lokale Pfade. Relative Pfade sind plattformunabhängiger als absolute. Die Dateien sollten sich im Projektverzeichnis oder in einem konfigurierten Include-Pfad befinden.
Ein typischer Anwendungsfall ist das Einbetten von kleinen Grafiken:
#include <stdio.h>
#include <stdint.h>
static const uint8_t icon_png[] = {
#embed "icon.png"
};
int main() {
printf("Icon-Größe: %zu Byte\n", sizeof(icon_png));
// Übergabe an Grafikbibliothek
// decode_png(icon_png, sizeof(icon_png));
}
Nach der Einbettung mit #embed sieht der vom Präprozessor erzeugte Code für das Array ungefähr so aus:
static const uint8_t icon_png[] = {
0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A,
0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52,
0x00, 0x10, 0x00, 0x10, 0x08, 0x06, 0x00, 0x00,
0x00, 0x1F, 0xF3, 0xFF, 0x61,
// … viele weitere Byte …
};
Der Präprozessor ersetzt die #embed-Direktive durch eine Sequenz der Werte, die den exakten Inhalt der Datei icon.png repräsentieren. Der C-Standard schreibt keine bestimmte Notation (dezimal oder hexadezimal) vor. Er garantiert nur die Werte (und dass es eine Liste von Integer-Konstanten ist). Die exakte Token-Schreibweise ist Implementierungsdetail.
Diese statische Einbettung der Daten ermöglicht dem Compiler Optimierungen, wie das Erkennen und Ersetzen konstanter Ausdrücke (Konstantenfaltung), und eignet sich besonders für eingebettete Systeme, die keinen direkten Zugriff auf ein Dateisystem haben.
#embed unterstützt?
#embed ist ein neues Sprachfeature aus C23. Die Unterstützung kann zur Kompilierzeit überprüft werden:
#if !__has_embed
# error "Dieser Compiler unterstützt #embed (C23) nicht."
#endif
Viele Compiler definieren das Makro __has_embed, das während der Präprozessorphase ausgewertet wird. Wenn dieses Makro nicht bekannt ist, wird der obige Test fehlschlagen und damit anzeigen, dass #embed in der aktuellen Toolchain nicht verfügbar ist.
In diesem Fall stehen mehrere Alternativen zur Verfügung:
- Daten zur Laufzeit laden: Die Binärdatei kann mit
fopen()undfread()eingelesen werden. - Vorab erzeugte Headerdateien: Werkzeuge wie
xxd -i,bin2coder eigene Skripte können die Datei in ein C-Array konvertieren, das anschließend in den Build-Prozess eingebunden wird.
Über Präprozessorabfragen lässt sich #embed nur dann aktivieren, wenn es unterstützt wird, z. B.:
#if __has_embed
static const uint8_t icon_png[] = {
#embed "icon.png"
};
#else
#include "icon.inc" // Fallback: von Tool generierte Datei
#endif
#embed-Parameter
Zusätzlich stehen Parameter zur Verfügung:
limit(n): begrenzt auf n Byteprefix(token): fügt vor jedem Byte ein Token einsuffix(token): hängt nach der Sequenz Tokens anif_empty(token): Alternative bei leerer Ressource
Die Parameter sind optional und können kombiniert werden. Jeder Parameter darf höchstens einmal erscheinen.
Beispiel 1: Ist die Datei leer, soll der angegebene Fallback-Wert 0 eingefügt werden. Das leistet if_empty:
static const uint8_t icon_png[] = {
#embed "icon.png" if_empty(0)
};
Beispiel 2: Die Direktive begrenzt die Einbettung auf 1023 Byte und hängt anschließend ein Null-Byte an. Die Daten werden direkt in das ausführbare Programm eingebettet, ohne dass zur Laufzeit auf Dateien zugegriffen werden muss.
#include <stdio.h>
#include <stdint.h>
const uint8_t data[] = {
#embed "beispiel.bin" limit(1023) suffix(, 0)
};
int main() {
for (size_t i = 0; i < sizeof(data); i++) {
printf("%02X ", data[i]);
}
printf("\n");
// Beispielausgabe bei beispiel.bin = {0x01, 0x02, 0x03, 0x04, …}:
// 01 02 03 04 … 00
}
Das suffix(, 0) fügt hinter dem letzten eingebetteten Byte ein Komma und eine 0 ein.
Durch die Begrenzung auf 1023 Byte und das angehängte Null-Byte enthält das Array immer höchstens 1024 Werte. Das schützt vor zu großen eingebetteten Dateien und ermöglicht optional eine einfache Markierung des Datenendes.
Hinweis: Bestehen die Dateien nicht aus einzelnen Bytes, sondern bilden sie zusammenhängende Gruppen wie 16-Bit-Werte, ist Vorsicht geboten.
#embedliefert die Bytes der Datei in der Reihenfolge ihres Auftretens. Wenn Endianness eine Rolle spielt, muss die Interpretation und Umwandlung im Code erfolgen:// 16-Bit-Werte in Little-Endian const uint8_t raw_data[] = { #embed "values.bin" }; uint16_t get_value(size_t index) { size_t pos = index * 2; return raw_data[pos] | (raw_data[pos + 1] << 8); }
Beispiel 3: Mit prefix(...) lassen sich zusätzliche Werte oder Kennungen vor die eingebetteten Daten setzen. Das ist nützlich, um etwa eine ›Magic Number‹ oder Versionskennung in einem Binärblock zu hinterlegen.
#include <stdint.h>
// Daten beginnen mit 0xDEADBEEF, danach folgt der Inhalt von firmware.bin
const uint8_t firmware_blob[] = {
#embed "firmware.bin" prefix(0xDE, 0xAD, 0xBE, 0xEF, )
};
Das prefix(...) fügt die angegebenen Werte einmalig vor die eingebetteten Bytes ein.
Ein typischer Anwendungsfall ist die Kennzeichnung oder Versionierung von Binärdaten,
z. B. bei Firmware-Images oder anderen eingebetteten Ressourcen.
Prüfen der Verfügbarkeit
Mit __has_embed lässt sich vorab testen, ob eine Ressource verfügbar ist:
#if __has_embed("beispiel.bin")
const uint8_t config[] = {
#embed "beispiel.bin"
};
#else
const uint8_t config[] = { /* Fallback-Werte */ };
#endif
Diese Technik verhindert Kompilierfehler bei fehlenden Dateien.
Entscheidungshilfen
Die folgende Tabelle fasst die Vor- und Nachteile der Methoden zusammen und vergleicht sie miteinander.
Neben den statischen Einbindungen zur Kompilezeit mit #include und #embed besteht auch eine dritte Möglichkeit: das dynamische Einlesen von Dateiinhalten zur Laufzeit über Funktionen wie fopen() oder fread(), was insbesondere bei häufig ändernden Daten oder großen Dateien Flexibilität bietet.
| Kriterium | #include |
#embed |
Laufzeit (fopen) |
|---|---|---|---|
| Datentyp | Text/Code | Binär | Beliebig |
| Datengröße | Klein bis Mittel | Klein (< 1 MB) | Beliebig |
| Änderungshäufigkeit | Mittel | Selten | Häufig |
| Performance | Schnell | Sehr schnell | Langsam (I/O) |
| Binary-Größe | Wächst | Wächst | Bleibt klein |
| Compiler-Support | Alle | C23+ | Alle |
Hinweis:
#embedeignet sich am besten für Ressourcen bis zu einigen hundert Kilobyte. Sehr große eingebettete Dateien führen durch die ASCII-Codierung zu entsprechend großen C-Quelldateien, was den Kompiliervorgang deutlich verlangsamen kann.