Nach sehr langer Zeit habe ich es endlich auch auf meinem Unix mal geschafft, mich zum Erstellen und Nutzen von shared Librarys durchzuringen. Naja, im Hintergrund stand auch eine universitaere Aufgabe, aber dennoch bin ich endlich mal dazu gekommen. Und da die Einfuehrungen in dieses Thema, die ich bisher gefunden habe, mich immer dadurch abschreckten, dass sie gleich auch dazu uebergegangen sind zu beschreiben wie man das Kompilat in das jeweilige System integriert, habe ich es hier einmal ohne diesen Aufwand beschrieben: Creating shared libs using gcc, not more. Dass ich dabei mein FreeBSD genutzt habe, ist zwar toll fuer mich, hat aber zentral mit dem Thema nichts zu tun, es sollte auf anderen Systemen genauso funktionieren.

Was ist eine Shared Library?

Was ist eine Library, erstmal allgemein, statisch

Wenn man einen Haufen Quellcode zum Laufen bewegt, dann kann man entweder alle 10.000 Zeilen nehmen und dann durch den Compiler und Linker in ein Binary zusammendestillieren, dass dann alles enthaelt, was so zum laufen notwendig ist. Nun kann man auf die Idee kommen, die 3.500 Zeilen, die notwendig sind, um eine Datei zu oeffnen nicht in jedes Binary einzeln aufs neue wieder hineinzukompilieren. Also jedesmal wieder den Quellcode fuer das Oeffnen einer Datei uebersetzen in ein Objectfile. Das ist ja irgendwie doof, dauert jedesmal wieder einen Moment und es kommt dazu auch noch jedes Mal das gleiche dabei raus. Eigentlich kann man sich das sparen. Und das tut man auch -- solche Funktionen, die immer wieder eingebunden werden, sind in Form von Libraries bereits vorhanden und werden beim sogenannten Linken (dem Moment, wo die einzeln uebersetzten Quellcode-Dateien zu einer ausfuehrbaren Datei zusammengebunden werden) eingefuegt. Eine Library ist also erstmal eine Ansammlung von allgemein nuetzlichen Funktionen, die zentral in einer Datei gelagert werden. Hier befinden neben dem genannten Beispiel noch Ein-/Ausgaberoutinen, solche zur Prozessverwaltung und aehnliches.
Der Vorteil eines Programmes, welches in dieser Art statisch mit einer Library verlinkt ist, ist, dass es die Tatsache, dass einige Funktionen aus Bibliotheken kommen, gar nicht beachten muss -- sie verhalten sich wie lokale Funktionen. Mehr noch -- wenn voellig statisch gelinkt wird, dann wird der Code aus der Bibliothek in das Binary eingefuegt und fuehrt dazu, dass die Lib auf dem Zielsystem nicht mehr vorliegen muss.

Und was ist eine dynamische Library?

Der Unterschied laesst sich (jenseits von Details wie Speicherbelegungen) einfach damit beschreiben, dass eine shared Library in diesem Fall vom Programm "bewusst" geladen wird -- das Programm enthaelt in irgendeiner Form Quellcode, der die Library laedt, die Funktionen dort drinnen sucht und nach der Benutzung die geladene Library wieder freigibt (und nicht beim Linken gesagt bekommt, dass diese- und jene Lib zu nutzen ist). Dieses Konzept ist beispielsweise bei Plugins sehr nuetzlich, denn ansonsten blieben nur zwei Moeglichkeiten: Plugins koennen nur in einer Makro- oder Skriptsprache der Anwendung oder aber in einer in die Anwendung eingebetteten andern Sprache geschrieben sein. Wenn hingegen shared Librarys als Plugins fungieren, dann ist die Sprache, mit der sie entwickelt wurden halbwegs egal, solange das Binaerformat das gleiche bleibt. Weitere typische Argumente fuer eine shared Library sind die Tatsachen, dass der Quellcode in compilierter Form daher kommt (also nicht von jedem eingesehen oder modifiziert werden kann).
Alles in allem hoechst praktisch, so eine shared Library.

Erstellen und Nutzen von shared Libraries unter Microsoft Windows (Ueberblick)

Visual Basic 6

Da ich eine Vergangenheit als Programmierer unter Microsoft Windows habe, bin ich dort bereits mit shared Libraries in Kontakt gekommen. In meinen ganz fruehen Tagen habe ich mit Microsoft Visual Basic 6 gearbeitet (das waren noch Zeiten). Da mein Taschengeld nicht fuer mehr als die "Standard Edition" ausreichte, habe ich damit Vorlieb nehmen muessen. Aber auch damit konnte man DLLs entwickeln. Zwar keine "richtigen", wie sie die "Grossen" hervorbringen, sondern "nur" ActiveX-DLLs, aber immerhin. Das Aufrufen des in DLLs verpackten Codes war denkbar einfach; die DLL musste im Pfad liegen und registriert sein, dann konnte man einfach ein entsprechendes Objekt anlegen und fertig. Keine Handles, keine Pointer. Eigentlich ja ganz schoen.
Aber irgendwie blieb immer der Wundsch, auch "richtige" DLLs zu schreiben, also diejenige Sorte, die man aus Visual Basic heraus nur mit Handstaenden der folgenden Art (hier fuer eine statisch eingebundene Funktion) aufrufen kann.
Private Declare Function FlashWindow Lib "user32" (ByVal hwnd As Long, _
    ByVal bInvert As Long) As Long
Dieser Code wuerde dafuer sorgen, dass ein VB-Programm die Funktion FlashWindow nutzen kann, als wenn sie lokal im Programm definiert waere. Wenn man nun auch noch das Ganze in dynamisch machen will, dann muss man (Ironie) erstmal die folgenden Library-Calls statisch einbinden:
Private Declare Function LoadLibrary Lib "kernel32" _
                         Alias "LoadLibraryA" ( _
                         ByVal lpLibFileName As String) As Long
                         
Private Declare Function GetProcAddress Lib "kernel32" ( _
                         ByVal hModule As Long, _
                         ByVal lpProcName As String) As Long
                         
Private Declare Function FreeLibrary Lib "kernel32" ( _
                         ByVal hLibModule As Long) As Long
...freilich bin ich in Visual Basic nie so weit gekommen. Das habe ich dann in Delphi getan, wo man diese Funktionen praktischerweise vordefiniert zur Verfuegung hat. Alles, was man danach noch tun muss, ist einen entsprechenden Funktionstypen definieren und diesem Funktionspointer den von getProcAddress zurueckgegebenen Wert als Sprungadresse zuzuweisen. Konkrete Reihenfolge des Spiels ist also Folgendes:

Erstellen und Nutzen von shared Libraries unter Linux/Unix

So, nach dem langen Prolog und dem Ausblick in die Welt der Fenster nun zum eigentlichen Inhalt: Wie macht man das ganze auf einem Unix?
Grundlage fuer meine Betrachtungen sei hier mal mein FreeBSD mit einem GCC angenommen. Fuer die Versionsfetischisten:

[ad001@glas /usr/home/ad001]$ uname -a
FreeBSD glas.ad001.de 8.0-CURRENT FreeBSD 8.0-CURRENT #15: Tue Feb 19 21:09:01 CET 2008     root@glas.ad001.de:/usr/obj/usr/src/sys/GENERIC  i386
[ad001@glas /usr/home/ad001]$ gcc --version
gcc (GCC) 4.2.1 20070719  [FreeBSD]
Copyright (C) 2007 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
Nachdem wir also informiert sind, dass der GCC shared Libs bauen koennte, aber niemand garantiert, dass er es kann, kann es ja losgehen.
Zunaechst also einmal eine Funktion, die in eine Bibliothek ausgelagert werden soll:
#include<stdio.h>

int
add(int a, int b)
{
   printf("add(%i,%i)=%i\n", a, b, a+b);
   return a+b;
}

Der auch Beobachter, der nicht in C zuhause ist wird mitbekommen, dass diese Funktion nichts weiter macht, als die beiden als Parameter uebergebenen int-Zahlen zu addieren und ausserdem eine Meldung darueber auszugeben. Nichts spektakulaerer also. Als naechstes muss aus diesem Quellcode nun eine dynamisch ladbare Bibliothek gemacht werden:
[ad001@glas /usr/home/ad001/tmp/c/libdemo]$ cc -c libfunct.c
[ad001@glas /usr/home/ad001/tmp/c/libdemo]$ cc -shared -o mylib.so libfunct.o
[ad001@glas /usr/home/ad001/tmp/c/libdemo]$
Das Ergebnis sind nun zwei Dateien mit den Namen libfunct.o ud mylib.so. Die Erstgenannte ist ein normales Objectfile, die letzgenannte ist schon unsere shared Library. Wenn man diese nun Systemweit nutzbar machen wollen wuerde, dann kaemen hier nun Befehle, um sie in ein bestimmtes Verzeichnis zu schieben oder viele andere tolle Dinge zu machen, die sich auch wohl wieder von System zu System unterscheiden und alleine deshalb hier keine Beachtung finden sollen. Hier soll es stattdessen damit weitergehen, wie man an die Funktion nun wieder rankommt :)
Dazu gibt es die caller.c.
#include <stdio.h>
#include <dlfcn.h>

typedef int (*addfunct)(int, int);

int
main(int argc, char** argv)
{
   addfunct myadd;
   void*  hLib = dlopen("./mylib.so", RTLD_LAZY);
   if (hLib) {
      myadd = (addfunct) dlsym(hLib, "add");
      if (myadd)
         printf("Adding... %i+%i=%i.\n", 4,4, myadd(4,4));
      dlclose(hLib);
   } else {
      printf("Error loading lib: %s\n", dlerror());
   }
   return 0;
}

Diese Datei ist schon etwas umfaenglicher, aber auch noch nicht von abschreckender Komplexitaet. Zunaechst das uebliche Einbinden von Headerfiles, hier ist das Einbinden von dlfcn.j hervorzuheben: In dieser Datei befinden sich die zum Umgang mit shared Libraries benoetigten Funktionen.
Als naechstes wird ein Funktionstyp deklariert, dieser muss der Funktion entsprechen, die in aus der Library geladen werden soll (auch nicht weiter verwunderlich). Diese hab ich erstmal addfunct genannt. Danach sind wir bereits in der main. Hier wird nun mit der Deklaration einer Funktionsvariablen begonnen. Diese wird einmal die aufrufbare Funktion werden. Danach wird die Library geladen und ein entsprechender Pointer darauf gesetzt. Der Parameter RTLD_LAZY besagt, dass die Funktionen aus der Bibliothek erst beim ersten Aufruf in den Speicher geladen werden sollen. Dies ist vor allem bei sehr grossen Libraries sinnvoll, wenn ein komplettes Laden aller Funktionen unnoetig waere um nur an eine einzelne zu gelangen.
Wenn die Library erfolgreich geladen ist (die Bedingung des if prueft das und gibt im Fehlerfalle eine Meldung mit einer von dlerror() generierten Erklaerung aus), kann nun dem Funktionspointer auch eine Sprungadresse zugewiesen werden. Das geschieht hier mit einem Call von dlsym, dem als Parameter das Library-Handle und der Name der Funktion mitgegeben werden und der einen void-Pointer zurueckliefert, der hier entsprechend gecastet wird.
Wenn auch das erfolgreich ist, also myadd != NULL, kann die Funktion mal aufgerufen werden. Man beachte hier den Unterschied zwischen myadd und myadd(). Ohne Klammern ist es ein Pointer auf die Funktion, mit Klamern ist es der Rueckgabewert der ausgefuehrten Funktion.
Nachdem sie nun ihre Schuldigkeit getan hat, kann die Bibliothek wieder entladen werden, dies uebernimmt schliesslich dlclose(). Das Compilieren und Ausfuehren des aufrufenden Programms sieht am Ende dann wie folgt aus:
[ad001@glas /usr/home/ad001/tmp/c/libdemo]$ cc -c caller.c
[ad001@glas /usr/home/ad001/tmp/c/libdemo]$ cc -o caller caller.o
[ad001@glas /usr/home/ad001/tmp/c/libdemo]$ ./caller
add(4,4)=8
Adding... 4+4=8.
[ad001@glas /usr/home/ad001/tmp/c/libdemo]$
Wer sich noch mehr und auf ein bestimmtes Betriebssystem abgestimmt und doch unabhaengt mit dem Erstellen und Installieren von shared Libraries befassen will, dem sei die Beschaeftigung mit dem libtool nahegelegt. Dabei handelt es sich eigentlich nur um ein Shellskript, das dem Nutzer aber dennoch sehr viel Arbeit abnehmen kann.

Stichworte:


Impressum