C++/clang: dlsym & shared lib options

rubricanis

Homo ludens
Ich bin gerade dabei mich in dynamisches linken hereinzuarbeiten, aus konkretem Anlass aber auch um die ganze Sache zu verstehen. Dazu habe ich eine dynamische lib mit einer einzigen function void foo() gemacht. Das scheint soweit auch zu funktionieren denn mit dlopen("...") bekomme ich eine Addresse, also keinen nullptr. Allerdings bekomme ich mit dlsym oder dlfunc(handle,"foo") immer einen nullptr.

Ich frage mich ob ich bei den flags irgend etwas falsch gemacht habe was ich mir eigenlich nicht vorstellen kann denn sonst würde dlopen vermutlich einen nullptr zurückgeben. Ich benutze clang++36 (unter dragonfly aber das sollte nichts bedeuten) und bei den flags habe ich die normalen flags verwendet und zusätzlich -shared -o $(lib)/myLib. $(lib) ist der abs. Pfad, -fPIC ist in den flags gesetzt.

Was mache ich da falsch und wie macht man es richtig?

Peter

PS: Ich habe noch mehr Fragen dazu aber erst einmal muss ich das Minimum zum laufen bekommen.
 
Erstmal eine dumme Frage vorweg: Beachtest du, dass C++ die Funktionsnamen durch sein Name Mangeling dreht und damit verändert? Die Funktionen, die du dynamisch linken möchtest, müssen als entwedern 'extern "C"' sein oder du musst dlsym() die gemangelten Namen übergeben.
 
Die Frage ist durchaus nicht dumm! Ich habe blind angenommen dass das durch die libs gemacht wird- Nein, das habe ich nicht beachtet. Ich werde das mal mit extern C ausprobieren.

Falls das funtioniert verbleibt die Frage wie ich an den gemangelten Namen komme...:(
 
Yep, das wars! Jetzt muss ich erst einmal weiter testen. Evtl. kann mir die funtion ja einen ptr auf eine struct mit den entsprechenden addressen geben. Oder es gibt einen Weg die geamangelten Namen zu bekommen. Mal sehen...

Danke Yamagi !
 
Hmmm, das Ganze ist doch recht kompliziert. Zwar ist es kein Problem z.B. einen ptr auf eine Klasse zu bekommen, aber dann braucht man auch noch alle method-ptr. Man muss also in irgeneiner Form einen Proxi bauen, vermutlich am besten auf der Lib-Seite.

Wie auch immer: Ich werde mich mal bei Github umsehen was es an allgemeinen Lösungen gibt obwohl das für meine Zwecke vermutlich zu aufwendig ist. die LLVM DynamicLibrary kann die Libs nicht entladen, was ja kaum Sinn der Sache ist.
 
Die gemangelten (tolles Wort) Namen zu nehmen hat auch das Problem, dass das Mangeling compilerabhängig ist. Du würdest dich entweder an g++ und clang++ binden, oder müsstest im Code Fälle für alle weiteren möglichen Compiler beachten. In wie weit dich das behindert, hängt natürlich vom Anwendungsfall ab.

In der Praxis geht man zum Verbinden der Bibliothek mit dem Hauptprogramm normalerweise über einen Proxy, also genau wie du sagst. Etwas abstrakt gesprochen hast du eine einzige Funktion als 'extern "C"', die du per dlsym() verbindest. Diese Funktion gibt bei Aufruf einen Pointer auf eine virtuelle Klasse zurück, die dann als Interface dient. Eine wahrscheinlich zu kurze Google-Suche findet dieses etwas schlanke Beispiel der Idee: http://cboard.cprogramming.com/linu...plusplus-name-mangling-error.html#post1088627
 
Yep, da in etwa bin ich inzwischen auch gelandet. Man kann dann noch ein paar templates und für die C-Seite defines herumbasteln, aber das alles gefällt mir nicht so recht. Ich denke da ist OOP das Problem, nicht die Lösung. Ich werde wohl den klassische C-Weg gehen und nur eine Funktion verwenden die mir etwas liefert oder eben eine kleine Zahl von Funktionen zwischen denen Data zwischen Client und Lib per Ptr hin- und her geschoben werden.

Meine Güte, da verwendet man einen unheimlich komplexen Compiler/Sprache und dann muss man doch auf solche Wege zurückgreifen. It is a mess! :rolleyes:

Erst einmal vielen Dank soweit! Mir sind noch ein paar Flags unklar aber erst einmal weitersehen...
 
Wenn Du nicht zur Laufzeit linken musst, kannst Du dir den ganzen Ärger sparen.
 
Gut, das ist klar. Statisches Linken hat seine Vorzüge. Wenn aber ein größerer Brocken (sagen wir mal so etwas wie SQLight) eher selten genutzt wird oder eben von mehreren Programm-Instanzen, macht dynamisches Linken schon Sinn. Die Alternative wäre jeweils unabhängige Programme aufzurufen was einiges mehr an Overhead sein dürfte.

Ich beschäftige mich ja damit um herauszufinden was geht und was nicht und wie groß der Aufwand ist. Und um C++ kennen zu lernen ...:)
 
Kamikaze meint, die Bibliothek nicht explizit per dlopen() zu laden sondern sie durch den Runtime Linker (rtld) beim Programmstart zu linken. Also das "normale" linken dynamischer Bibiliotheken.
 
Ich glaube, dass wir aneinander vorbeireden:
  • Statisches Linken ist, wenn die Blibliothek ins Binary eingebunden wird. Also "clang++ -static ..." oder "clang++ /pfad/zur/lib.a". Statisch linken tut man heute nur noch selten, da man in Fehlern von Bibliotheken die Programme neu bauen und damit neu ausliefern muss. Auf einem FreeBSD-System ist zum Beispiel der Kram in /rescue statisch gelinkt.
  • Dynamisches Linken ist, wenn zur Linkzeit Binary und Bibliothek referenziert werden. Beim Start des Programm linkt dann der dynamische Linker des Systems (unter FreeBSD ist es /libexec/ld-elf.so.1) die Binary mit den Blibliotheken zu einem funktionsfähigen Prozess zusammen. Der Vorteil ist, dass man die Bibliotheken unabhängig von den Programmen aktualisieren kann. Der Nachteil ist, dass die Programme ohne die Bibliotheken nicht funktionieren. Heute werden Programme meist dynamisch gelinkt. Nehmen wir als Beispiel mal FreeBSDs /bin/sh:
    Code:
    # ldd /bin/sh
    /bin/sh:
       libedit.so.7 => /lib/libedit.so.7 (0x800840000)
       libncurses.so.8 => /lib/libncurses.so.8 (0x800a6c000)
       libc.so.7 => /lib/libc.so.7 (0x800cb9000)
    Sowas wird ganz einfach und wie gewohnt gelinkt: "clang++ -lbeispiel ..."
  • Dynamisches Linken zur Laufzeit ist, dass die Bibliothek mit dlopen() geöffnet und mit dlsym() über Pointer verbunden wird. Der Vorteil ist, dass das Programm auch ohne die Bibliothek starten kann. Der Nachteil, dass man beide über eine meist recht komplexe API zusammenfummeln muss. Wird meisten für Plugin-Systeme und ähnliches genutzt.
 
Hmmm, I see! Dann habe ich mein Makefile wohl falsch angelegt. Ich dachte dass Libs die von Anfang an dabei sein müssen immer statisch gelingt werden müssen (.a) . Verstehe ich das richtig das ich also alles was ich bislang immer als statische Lib gelinkt habe einfach als dynamische Lib compilieren kann und dann mit -lName beim Start eingebunden wird.

Hier mal kurz mein generelles Design.

(1) Ein oder oder auch mehr Programme mit main(). Jetzt nur mit CmdLine, könnte aber noch etwas anderes dazukommen, z.B. GUI.

(2) CoreLib. Alle Grundlegende Funktionalität. Die habe ich z.Z. aber auch in 3 libs aufgeteilt um besseren Überblick zu haben. Die kann ich also auch zu dynamischen libs (.so) kompilieren wenn ich das richtig verstehe.

(3) Libs die dan je nach (1) beim Programmstart oder später mit dlopen gelinkt werde.

Prima, das wäre dann alles ja noch flexibler!

Macht das so Sinn und was ist evtl. noch etwas zu bedenken?
 
Ja das dich User hassen werden, wenn du dlopen() als Selbstzweck verwendest. Macht aber nichts, weil weil du dich über kurz oder lang deswegen auch selbst hassen wirst und du dir schon mit deinen Nutzern einig wirst.
 
Ja das dich User hassen werden, wenn du dlopen() als Selbstzweck verwendest.
:) :) :) Nööö, Selbstweck soll das nicht sein. Der Selbstzweg ist erst einmal nur wirklich zu verstehen wie das alles läuft, wie es gemacht wird, was dazu zu tun ist und was der Preis ist. Wofür und ob ich das nachher verwende, ist eine ganz andere Frage...

Aber im Ernst: Haben Plugins unter User-Gesichtspunkten einen derart schlechten Ruf und was sind die Nachteile ?
 
dlopen() ist in einigen Fällen unvermeidbar oder zumindest der kürzeste Weg zum Ziel. Aber wie so viele Low-Level-Funktionen ist es auch problematisch:
  • Wenn man der reinen Lehre folgt und wirklich standardkonformes C oder C++ schreiben möchte, ist dlopen() nicht anwendbar. Grund ist die auf x86 und den meisten anderen hochentwickelten Architekturen irrelevante Trennung in Pointer auf Code (Object Pointer) und Daten (Data Pointer). dlopen() gibt eine Object Pointer zurück, void* ist ein Data Pointer. Das ist inkomaptibel und bei -pedantic haut der Compiler es dir um die Ohren. Auf "niederen" Architekuren wie vielen Microcontrollern ist dlopen() daher auch erst gar nicht anwendbar.
  • dlopen() ist unzuverlässig. Du musst in realen Anwendungen höllisch aufpassen, dass die per dlsym() gesuchten Symbole wirklich den Funktionen entsprechen, die du erwartest. Weichen die erwartete Version einer Bibliothek und die tatsächlich vorhandene voneinander ab und die API ist schwach oder gar nicht definiert, ist der Ärger vorprogrammiert.
  • dlopen() funktioniert in diversen Situationen einfach nicht. Ein ganz klassisches Beispiel sind Threading Bibliotheken wir pthreads. FreeBSD hatte z.B. lange zeit das Problem, dass ein Aufruf von dlopen() im zusammenspiel mit libthr (die pthread-Implementierung) unter gewissen Umständen den Prozess zerdeppern konnte. Die Folge waren dann Craches. Auch andere Low-Level-Funktionen wie longjmp() bekommen sich mit dlopen() gern mal in die Haare. Von Dingen wie Garbage Collector Libs und ähnlichen sprechen wir erst gar nicht.
  • Die mit dlsym() gefunden Symbole sind durch Funktionspointer verbunden. Funktionspointer sind ein mächtiges Werkzeug und in Maßen angewendet durchaus in Ordnung. Aber sie können schnell zu Hirnfick im Quadrat werden. Und sie sind in Sachen Performance nicht optimal. Man hat einen indirekten Aufruf mehr, der CPU-Cache zuckt mit den Schultern und es hagelt Cache Misses.
  • Es ist nicht portabel. dlopen() kommt aus der weiteren Unix-Suppe, d.h. nur einige unixoide Systeme haben es und dann kann es durchaus noch dazu kommen, dass es sich je nach System subtil anders verhält. Das Binärformat (meist ELF, aber unter OS X z.B Mach-O) spielt da auch noch mit rein. Windows hat dann gleich eine ganz andere Schnittstelle, die zwar ähnlich funktioniert, aber anders implementiert ist.
Wenn man dlopen() und seine Kollegen nutzt, muss oder besser gesagt sollte man also einige Dinge im Kopf behalten. Man sollte seine API so aufbauen, dass man mit sehr hoher Wahrscheinlichkeit nicht passende Bibliotheken erkennen und zurückweisen kann. Es ist sinnvoll sich Gedanken zu machen, wie man die Funtionspointer behandelt. Ein oft gegangener Weg ist nur eine oder zwei Funktionen mit dlsym() zu suchen, diesen einen Pointer auf eine Struct zu übergeben und die Bibliothek selbst die Struct mit den Pointern füllen zu lassen. Und nicht zuletzt muss man sicherstellen, dass der Aufruf zu dlopen() sicher ist. Das ist die oben angesprochene Sache mit den Threading Bibliotheken und anderem.
 
Danke (einmal mehr!) Yamagi für die ausfühliche Erklärung!

Portabilität ist immer ein Problem! Ich habe mir das bei LLVM und Lua angesehen die ja für (relative) Portabilität bekannt sind. Das Windows/Unix/Mac Problem ist mir bekannt und das kann man wohl mit entsprechen Maßnahmen in den Griff bekommen. Zur Zeit interessiert mich das allerdings noch nicht aber ich weiß in etwa wie das gehen kann. Meine Makefiles sind eh für crosscompiling vorbereitet.

Threading ist - wie immer - eine andere Kiste. Ich werde vermutlich nur 2 Threads nutzen, einen interaktiven und einen Worker-Thread (der auch als service detached werden kann). Aber das kann sich evtl ja ändern. Um das Problem zu minimieren ist meine Idee Plugins nur in jeweils einem einzigen Thread zu benutzen, in keinem Falle im interaktiven. Damit sollte das gelöst sein (abgesehen von den üblichen threading Problemen).

Weiter habe ich mich entschieden doch C++ Klassen zu benutzen wie du das in deinem ersten Beitrag beschrieben hast. Man sollte wohl eher nicht gegen das Konzept der Sprache arbeiten. Die von der dynamischen Lib zurückgegeben Pointer werden durch einen unique_pointer mit refCount und einem entsprechend Deleter geschützt. Ob auch shared_ptr gehen habe ich noch nicht untersucht, aber ich will eh durchgängig mit unique_ptr arbeiten so dass mich das z.Z. nicht interessiert.

Der einzige Schwachpunkt den ich noch sehe sind die beiden C-Funtionen <libname>_<classname>_construct() und <libname>_<classname>_destruct(T *) da die vermutlich keine Exceptions abfangen können (oder doch?). Wie auch immer: Die kann ich ja noch durch einen kleinen C++ Proxy schützen. Ansonsten werde ich kein C benutzen oder bei Bedarf zum einbinden einer C-Lib in einem C++Proxy verstecken. Alles ziemlich viel Aufwand aber der runtime Overhead dürfte begrenzt sein da er nur im ctor/dtor anfällt bzw. beim laden/endladen der Lib. Und von Nix kommt Nix! ;)

Das Ganze ist von der Programmierung her zwar ziemlich komplex und auch ein bißchen ein Hack, aber in der Anwendung eher einfach: Man bekommt einen unique_ptr<T> und der erledigt den Rest. Templates sind da wirklich nützlich und evtl. kommen noch 1-2 #define dazu. Mal sehen...

Ganz schön schwieriges Problem, macht aber Spass. Und wer beschäftigt sich schon gerne mit Trivialitäten ? :)

Peter
 
Noch ein Nachtrag: (dein Beitrag hilft beim Nachdenken):
Wenn man dlopen() und seine Kollegen nutzt, muss oder besser gesagt sollte man also einige Dinge im Kopf behalten. Man sollte seine API so aufbauen, dass man mit sehr hoher Wahrscheinlichkeit nicht passende Bibliotheken erkennen und zurückweisen kann.
Mir geht es nicht um eine allgemeine Lösung um z.B. Plugins von dritten zu laden. Vielmehr werden die wenigen Libs zusammen mit dem Pragramm mit dem gleichen Compiler und den gleichen Flags kompiliert und nur auf bestimmten Pfaden gefunden. Wildwuchs brauche ich nicht!
Es ist sinnvoll sich Gedanken zu machen, wie man die Funktionspointer behandelt. Ein oft gegangener Weg ist nur eine oder zwei Funktionen mit dlsym() zu suchen, diesen einen Pointer auf eine Struct zu übergeben und die Bibliothek selbst die Struct mit den Pointern füllen zu lassen.
Da muss ich noch drüber nachdenken: Zur Zeit verwende ich 2 Funktionspointer (ctor/dtor) pro Klasse. Sollten das mehr werden lohnt sich sicherlich eine struct. Das hieße dann aber auch eine größere Komplexität auf der Client-Seite die nicht so einfach zu bewältigen sein dürfte. Eine andere Möglichkeit ist eine Managerklasse die den Rest verwaltet. Es ist ja nicht so dass man unterschiedliche Konzepte in einer Lib zusammenfaßt. Vielmehr geht es ja um eine vernünftige Modularisierung eines Konzeptes. Ich werde erst einmal den letzteren Weg gehen (ist schwierig genug) und das kann man immer noch ändern falls Bedarf sichtbar wird.

Peter
 
Als kleine Ergänzung, das dynamische Linken zur Laufzeit (via dlopen(3)) wird auch als „dynamisches Laden“ bezeichnet, um es vom dynamischen Linken zur Linkzeit zu unterscheiden.

Als C++-ABI-Stabilitätsmuster scheint ein C89-Wrapper relativ beliebt zu sein, die zugehörige Präsentation finde ich momentan gerade aber nicht.
Im Endeffekt gehts darum, dass man sein C++-Objekt über einen transparenten Zeiger (z.B. void) an seine C-API übergibt, die im Endeffekt nur ein Wrapper für die C++-Methoden ist:
Code:
class Foo
{
    Foo();
    int bar();
    void baz(int v);
};
Code:
#ifdef __cplusplus
extern "C" {
#endif
void *foo_create(void);
void foo_delete(void *foo);
int foo_bar(void *foo);
void foo_baz(void *foo, int v);
#ifdef __cplusplus
}
#endif
Code:
void * foo_create(void)
{
    return reinterpret_cast<void *>(new Foo());
}

void foo_delete(void *)
{
    delete reinterpret_cast<Foo *>(foo);
}

int foo_bar(void *foo)
{
    return reinterpret_cast<Foo *>(foo)->bar();
}

void foo_baz(void *foo, int v)
{
    reinterpret_cast<Foo *>(foo)->baz(v);
}

Das ist zwar nicht für so Scherze wie near und far Pointer geeignet, aber auf solchen Architekturen hast du eh noch ganz andere Probleme.

Im Übrigen würde ich Yamagis ersten Punkt etwas einschränken, die Code-/Data-Zeigergeschichten sind extrem abhängig von der zugrundeliegenden Hardwarearchitektur und dem verwendeten Ausführungsformat (z.B. a.out vs. ELF). Dazu kommen noch Segmentierung, Overlays usw. Wenn du mehr wissen willst, ich lese gerade „Linkers and Loaders“ von John Levine, da wird das ganze ausgiebig behandelt. Wer schon immer mal wissen wollte, wie das Linken auf OS/360 funktioniert…
dlopen(3) selbst gibt erstmal nur ein Handle zurück. Für Symbole und Funktionen gibt es dann dlsym(3) und dlfunc(3), aber AFAIK hat der C Standard selbst keine Meinung zu Code- und Datapointern, ich würde mal schwer davon ausgehen, dass das implementation defined ist. Im C99 draft finde ich weder die Wörter „data pointer“ noch „code pointer“, daher finde ich die manpage etwas unpräzise: „The dlsym() function returns a data pointer; in the C standard, conversions between data and function pointer types are undefined.“. Das ist IMHO ziemlich x86 spezifisch, andere Plattformen handhaben Code und Datensegmente anders. Außerdem sollte sich das Code-/Datenpointer-Problem durch einen cast in einen Funktionspointer auch wieder erledigen lassen, auf AVR µCs ist es zumindest üblich, wild zwischen void * und void (*)(void) hin und her zu casten um an eingebaute Funktionalität zu kommen.
 
goblin schrieb:
Im Übrigen würde ich Yamagis ersten Punkt etwas einschränken, die Code-/Data-Zeigergeschichten sind extrem abhängig von der zugrundeliegenden Hardwarearchitektur und dem verwendeten Ausführungsformat (z.B. a.out vs. ELF). Dazu kommen noch Segmentierung, Overlays usw. Wenn du mehr wissen willst, ich lese gerade „Linkers and Loaders“ von John Levine, da wird das ganze ausgiebig behandelt. Wer schon immer mal wissen wollte, wie das Linken auf OS/360 funktioniert…
Ja, volle Zustimmung. Da habe ich etwas sehr verallgemeinert.
 
Vielen Dank Goblin für Info!

Einem bibliophlen Mensch sei eine weitere Frage gestattet: Empfielt sich die Anschaffung von Linkers & Loaders ? Die Kommentare bei Amazon sind ja durchaus gemischt und ein für mich wichtiger Kritikpunkt scheint zu sein dass es reichlich veraltet ist. OS2 interessiert mich nicht, sehr wohl aber arm und mips. Und kennst du zu dem Thema informative Websites ? (Ich habe noch nicht gesucht)

Peter
 
Zurück
Oben