NHollmann

English | German

Hi, ich bin Nicolas Hollmann und das ist meine Seite und Blog.
Viel Spaß beim Lesen! 📖

Single Header Bibliothek

August 13, 20196 Min. Lesedauer
Tags:

Dieser Artikel ist mehr an Anfänger gerichtet und soll nur kurz erklären, was eine Single Header Library/Bibliothek (SHL) ist und wie man eine eigene erstellt. So kann ich dann von anderen Artikeln, die ich bereits in Planung habe, auf diesen hier verweisen.

Der Artikel bezieht sich hierbei auf C und C++, in den meisten anderen Sprachen gibt es das gesammte Prinzip garnicht, bzw. es ist garnicht anwendbar.

Um zu verstehen, was eine Single Header Bibliothek (SHL) ist, schauen wir uns am besten zuerst die einzelnen Begriffe an. Dabei fange ich beim letzten Begriff an, da dieser die größte Bedeutung hat.

Bibliothek

Bibliotheken gibt es in den aller meisten Programmiersprachen in irgendeiner Form. Ganz allgemein lässt sich sagen, dass eine Software-Bibliothek eine Ansamlung von Funktionen und Datentypen ist, die im Normalfall nicht direkt ausgeführt werden kann. Stattdessen stellt sie ihre Funktionen und Datentypen anderer Software (Programme und auch andere Bibliotheken) zur Verfügung. Dieses Prinzip ist sehr sinnvoll, da man so den gleichen Code in unterschiedlichen Projekten nutzen kann, ohne ihn von Hand zu kopieren. Ein Nutzer der Bibliothek muss nur die öffentliche Schnittstelle dieser kennen, wie genau sie im Inneren funktioniert ist nebensächlich. Beispiele für Bibliotheken sind die C++ STL oder libSDL.

Wer schonmal mit C oder C++ gearbeit hat, wird vermutlich wissen, was ein Header ist. Dennoch hier nochmal der Vollständigkeit halber:

In C/C++ muss eine Funktion bekannt sein, bevor sie aufgerufen werden kann. Das gilt nicht nur für Funktionen in der eigenen Quellcode-Dateie (nach Konvention .c oder .cpp) sonder auch für Funktionen aus anderen Quellcode-Dateien oder Bibliotheken. Um diese bekannt zu machen, reicht es den Funktionskopf mit Parametertypen und Rückgabewert einmal vor dem Aufruf zu nennen:

// 1. Nennung
int add(int, int);

int main()
{
    // Nutzung
    printf("%d", add(1, 2));
    return 0;
}

// Implementierung
int add(int a, int b) {
    return a + b;
}

Wie man sieht, müssen sogar nur die Typen der Parameter gennant werden, ihr Name ist irrelevant. Dennoch schreibt man sie meistens dazu, damit sich die Funktionen selbst dokumentieren.

Nun wäre es allerdings umständlich, den Funktionskopf für alle Funktionen die man verwenden will, gerade wenn sie von externen Bibliotheken kommen, vor ihrer Verwendung einmal nennen zu müssen. Deshalb gibt es in C und C++ sogenannte Header Dateien (nach Konvention .h oder .hpp). Sie enthalten normalerweise keinen Code der ausgeführt wird (wobei das sehr wohl möglich wäre) sondern die Funktionsköpfe, die in anderen Quellcode-Dateien dann implementiert werden. Die Header können dann mit #include in Quellcode-Dateien eingebunden werden.

Single

Das “Single” drückt in diesem Context nur aus, dass unsere ganze Bibliothek in einer einzigen Datei ausgeliefert werden kann und somit möglichst einfach in andere Projekte eingebunden werden kann.

Und jetzt alles zusammen!

Den Begriffen nach ist eine SHL also nichts anderes, als eine Software-Bibliothek die aus einer einziegen Header-Datei besteht. Das war’s.

Warum ergeben Single Header Libraries Sinn?

Bibliotheken sind etwas alltägliches im Leben eines Programmierers. Allerdings kann es oft nervig sein, diese in neue Projekte einzubinden. Man muss zuerst die Bibliothek selbst für alle Platformen auf denen der eigene Code laufen soll übersetzen, wobei die Übersetzungsprozesse je nach Bibliothek unterschiedlich ausfallen. Manche Bibliotheken besitzen selbst noch andere Abhängigkeiten damit sie übersetzt werden können. Allgemein ist ihr Handling oft etwas komplizierter.

SHLs bieten dafür eine Lösung: Sie bestehen nur aus einer Datei, die einfach in das eigene Projekt kopiert werden kann und dann eingebunden werden muss. Kein großes Setup nötig, sie benutzen einfach den Build-Prozess des eigenen Projektes mit. Sie können auch wunderbar mit in die Versionsverwaltung eingechekt werden, was es leicht macht, das Projekt auf anderen Computern auszuchecken und ohne große Umwege zu übersetzen.

Das bietet sich sehr gut für Open Source Projekte an, die von vielen Personen auf der ganzen Welt möglichst leicht kompiliert werden sollen.

Ab wann ergeben sie keinen Sinn mehr?

Auch SHLs haben ihre Grenzen. Ein großes Problem ist, dass sie auf jedem Computer und für jedes Projekt in voller Gänze neu übersetzt werden. Das kostet Zeit. Bei klassischen statischen oder dynmaischen Bibliotheken reicht es, wenn man sie einmal auf jeder Zielplatform übersetzt und dann die fertigen Kompilate (.lib, .so, .dll, .dylib, etc.) zwischen den Rechnern hin und her kopiert.

Im Allgemeinen würde ich sagen, dass der Umfang einer Bibliothek das größte Kriterium darstellt, dass darüber entscheidet ob eine SHL oder eine klassiche Bibliothek sinnvoller ist. Wo die Grenze dabei ist, muss allerdings individuell für jedes Projekt festgestellt werden.

Wie bindet man eine SHL ein?

Die genaue Verwendung unterscheidet sich natürlich von Bibliothek zu Bibliothek aber im Allgemeinen ist das Muster oft identisch.

Man sucht sich eine Quellcode-Datei aus, die die Implementierung der Bibliothek enthalten soll und setzt eine Präprozessor-Konstante bevor man die SHL einbindet. Wenn andere Dateien die SHL auch verwenden sollen, reicht es hier nur den Header einzubinden ohne die Konstante zu setzten.

Schauen wir uns das mal für die SHL ”stb_image” an. Diese Bibliothek erlaubt es, diverse Bildformate in ein C/C++ Programm einzulesen.

Zuerst muss die Bibliothek von einer C/C++ Quellcode Datei aus implementiert werden:

#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"

In die Datei, die diese Direktiven enthält, wird der gesammte Bibliothekscode eingebunden. Alle weiteren Quellcode-Dateien, die auch die Funktionalitäten verwenden wollen, brauchen nur die Header Datei ohne das STB_IMAGE_IMPLEMENTATION einzubinden:

#include "stb_image.h"

Oft stellt man fest, dass die Datei, die die Implementierung einer SHL einbindet, deutlich länger zum Übersetzen braucht. Das ist nur logisch, da diese Datei eine Menge Funktionalität aufnehemen muss. Deshalb bietet es sich an, ein Build-System wie make zu verwenden, dass einzelne Quellcode Dateien nur neu übersetzt, wenn sich ihr Inhalt geändert hat. Zusätzlich sollte man für die Implementierung eine Quellcode Datei wählen, die sich dann auch möglichst selten verändert, z.B. eine eigene Datei nur für diese SHL.

Wie erstellt man eine eigene SHL?

Erstmal braucht man überhaupt Code, der losgelöst von seinem Programm sinnvoll in andere Projekte eingebunden werden kann. Gehen wir für diesen Artikel mal davon aus, dass wir so etwas bereits haben.

Danach muss man überlegen, ob das ganze unter C und C++ oder nur für eine der beiden Sprachen lauffähig sein soll. Verwendet man reine C++ Features wie Klassen, kann die SHL definitv nicht mehr in C verwendet werden. Eine für C ausgelegte SHL auf der anderen Seite kann auch in C++ verwendet werden, weicht dann aber oft vom “C++ Stil” ab, bzw. ist von der Bedienbarkeit ggf. nicht 100%ig optimal. Es gibt auch die Möglichkeit mit Präprozessordirektiven dafür zu sorgen, dass die SHL sowohl in C als auch in C++ möglichst idomatisch verwendbar ist, dies steigert aber oft den Aufwand enorm.

Darüber hinaus ist das Schreiben einer SHL dank der Präprozessor-Direktiven sehr einfach. Wir schauen uns hier mal ein kleines Beispiel für eine sinnlose fiktive Mathe-Bibliothek an, die als SHL implementiert werden soll:

#ifndef MINI_MATH_H
#define MINI_MATH_H

/* Öffentliches Interface */

int add(int a, int b);
int sub(int a, int b);

#ifdef MINI_MATH_IMPLEMENTATION_H

/* Implementierung */

int add(int a, int b)
{
    return a + b;
}

int sub(int a, int b)
{
    return a - b;
}

#endif /* MINI_MATH_IMPLEMENTATION_H */

#endif /* MINI_MATH_H */

Und schon haben wir eine eigene SHL geschrieben. Wichtig ist, dass nur eine .c/.cpp Datei MINI_MATH_IMPLEMENTATION_H setzt, wodurch der Code erst wirklich eingebunden wird.

Wenn man Hilfsfunktionen in der Implementierung verwenden will, die jedoch nicht nach Außen hin sichtbar sein sollen, dann kommen diese einfach mit in den MINI_MATH_IMPLEMENTATION_H Teil hinein. Gleiches gilt auch, wenn man externe Header einbinden will, die nicht für das öffentliche Interface erforderlich sind.

Es gibt auch noch weitere Ansätze für die Entwicklung einer SHL. Zum Beispiel kann man Programme benutzen, die mehrere Dateien in eine Header-Datei nach dem obigen Muster zusammenfassen. Das erlaubt es, eine Bibliothek angenehm in mehreren Dateien zu entwickeln aber dennoch als SHL auszuliefern.

Das war’s mit diesem ersten kleinen Artikel.



© 2022, Nicolas Hollmann