Seite 1 von 1

Runtime Libraries - Trauer, Hass, Zorn !

Verfasst: 25.03.2010, 17:21
von ponx
hallo zusammen,
ich hab wieder bzw noch Ärger mit meiner sound-library, die ich als DLL anbiete. Eins der Probleme ist, dass scheinbar alle Applikationen in ihrer Debug-Konfiguration crashen, sobald sie meine release-Version einbinden, und zwar mit:
Unhandled exception at 0x7c812afb in bla.exe: Microsoft C++ exception: std::bad_alloc at memory location 0x0012ed38..

.. und zwar genau in der Zeile, in der eine Instanz von der Hauptklasse meiner DLL erstellt wird.
Der Fehler verschwindet, sobald ich in meiner DLL als Runtime Library entweder "Multi-threaded" oder "Multi-threaded DLL" benutze.

Jetzt frag ich mich, wie genau dieser Fehler entsteht.. ich hätte ja bei den unterschiedlichen Runtime Libs mit nem Linker-Error gerechnet, aber ein Crash ?

ich wühl mich seit Tagen durch diesen Konfigurations-Wust und steh immer noch davor wie der Ochs vorm Berg :? Gibts einen Menschen auf der Welt, der das wirklich alles verstanden hat, und was es für Nebenwirkungen verursachen kann ? Wie kriegen andere Libs das hin ?

Für Hilfe oder Beileidsbekundungen immer dankbar:

das frustrierte pönxchen

Re: Runtime Libraries - Trauer, Hass, Zorn !

Verfasst: 25.03.2010, 17:36
von Aramis
ich hätte ja bei den unterschiedlichen Runtime Libs mit nem Linker-Error gerechnet, aber ein Crash ?
Aus diversen Gründen ist das die Realität. In jedem Fall muss ein Stückchen Heap *immer* von dem Modul freigegeben werden, das es auch angefordert hatte. Das heißt beispielsweise generell, dass eine in einer DLL implementiertes API nicht ohne weiteres einen std::string by-value zurückgeben darf - dessen Destruktor würde auf dem falschen Heap aufgerufen (da jedes Modul/DLL seinen eigenen, privaten Heap besitzt), zudem käme auch noch das STL-ABI des Aufrufers zum Einsatz, eine sichere und schwer zu entkäfernde Katastrophe wenn es sich um eine andere Version handelt (z.B. vc8 mit vc9). Die Multithreaded-DLL Runtime löst einen Teil des Problems - new und delete und Konsorten sind dann in der Runtime-DLL implementiert - d.h. alle Allokationen kommen von deren Heap. Wenn also alle Module gegen MT/Dll linken, so teilen sich alle den gleichen Heap. Natürlich nur, wenn auch alle gegen die gleiche Runtime-Version linken :-). Kurz: Lieber beim Designen von APIs über Object-Ownership nachdenken, nicht erst hinterher mit dem Debugger.
Der Fehler verschwindet, sobald ich in meiner DLL als Runtime Library entweder "Multi-threaded" oder "Multi-threaded DLL" benutze.
ABI-Mismatch - Debug- und Release ABIs sind inkompatibel. Siehe oben.

Beispiel

Code: Alles auswählen

// -------------------------------------------------------------------------
// das API unserer Lib - entweder in einer DLL oder in einer statischen Lib.
std::string api1(); 
const std::string& api2(); 
const char* api3(); 
char* api4(); // caller, the buffer is yours!
char* api5(); // caller, the buffer is mine- call free_api5 to hand it back to me!
void free_api5(char* bu);

// -------------------------------------------------------------------------
// Aufruf in unserem Code im Hauptprogramm
int main(int,char*[]) {
// Gefährlich - std::string by-value zurückgegeben, d'tor wird auf unserem Heap aufgerufen und crasht.
/*0*/std::string s = api1();

// Ergo! Hier wird eine Kopie erstellt, diese wird dann an eine konstante Referenz gebunden. D'tor des originalen Strings crasht.
/*1*/const std::string& s2 = api1();

// Wir erhalten nur eine Referenz auf ein von der DLL verwaltetes Objekt - d'tor wird auf dem korrekten Heap aufgerufen.
/*2*/const std::string& s3 = api2();

// Erstellen einer Kopie auf unseren Heap. In Ordnung.
/*3*/std::string s4 = s3;

// Dito.
/*4*/std::string s5 = api2();

// Immer in Ordnung.
/*5*/const char* s6 = api3();

// Soweit in Ordnung, beim delete aber die Katastrophe.
/*6*/char* s7 = api4();
delete s7;

// So ist's fein.
/*7*/char* s8 = api5();
free_api5(s8);
}
0., 1. und 6. klappen prinzipiell mit der DLL-Runtime - alle nutzen ja den gleichen Heap. Sowohl 0., 1., 3. als auch 4. crashen aber, wenn das Hauptprogramm eine andere STL-Version verwendet als die, gegen die die DLL kompiliert wurde. Daher ist es ratsam zum Austausch mit Libs - sofern diese nicht nur für ein bestimmtes Projekt verwendet werden sollen - das Interface radikal zu `ent-C++-isieren`, sprich soweit möglich PODs auszutauschen, anstelle std::string's beispielsweise const char* (5.). Alle obigen Fehler haben die Eigenschaft, dass sie - wie du möglicherweise schon entdeckt hast :-) - nicht vom Linker bemerkt werden sondern erst zur Laufzeit auf mysteriöseste Art und Weise mit wehender Flagge ins Nirvana einziehen. Ich weiß nicht, wieso Microsoft nicht mehr Aufklärung betreibt. Scheinbar rennt jeder mal gegen diese Wand, und das mindestens 5 bis 10 mal.

- Alex

Edit ca. 10 mal editiert, nun ist Schluss.

Re: Runtime Libraries - Trauer, Hass, Zorn !

Verfasst: 25.03.2010, 18:30
von kimmi
Mal ene Frage wegen des Begriffes ABI: habt ihr zu dem Thema einen interessanten Link? Ich bin gerade zu verpeilt und finde nichts im Netz. Ich habe nämlich keine Ahnung, was man unter der besagten ABI versteht.

Danke und Gruß,
Kimmi

Re: Runtime Libraries - Trauer, Hass, Zorn !

Verfasst: 25.03.2010, 18:38
von Aramis
Gut, zugegeben - eine Google-Suche nach 'ABI' ist nicht so wahnsinnig ergiebig :-)
Application Binary Interface. Sprich - das tatsächliche, binäre Layout aller Komponenten eines APIs. Beispielsweise bei einem by-value übergebenen std::vector.

Re: Runtime Libraries - Trauer, Hass, Zorn !

Verfasst: 25.03.2010, 23:04
von kimmi
Ok, danke für die Info :).

Gruß Kimmi

Re: Runtime Libraries - Trauer, Hass, Zorn !

Verfasst: 26.03.2010, 18:34
von ponx
Hallo alle,
erstmal an Aramis: Ich hab mittlerweile einen Aramis-Altar in meinem Zimmer errichtet, du kannst dir nicht vorstellen wie wichtig solche super Erklärungen für mich sind. Vielen Dank !

Dass jedes Modul seinen eigenen Heap hat, war mir neu - ich musste erstmal ne Nacht drüber schlafen und versuche jetzt mal meine restlichen Unklarheiten zu formulieren:

1. ich versteh beim Beispiel...
/*0*/ std::string s = api1();
...nicht genau, was du meinst mit "D'tor wird auf unserem Heap aufgerufen und crasht". Also api1() erstellt ein Objekt auf "unserem" Heap (== Heap der DLL ?), soweit versteh ichs. Aber ich kann mir wohl einfach nichts drunter vorstellen, dass ein D'tor auf einem Heap aufgerufen wird, ich dachte man ruft den auf und fertig. Also liegt das Problem an den (im ungünstigen Fall) unterschiedlichen Implementierungen von delete innerhalb der STLs ?

2. du meintest, all diese Probleme betreffen nur C++, nicht C. Hängt das Problem also direkt und ausschließlich mit den C'tors und D'tors zusammen ? Oder hängt da noch mehr drin ?

3. Ich benutze FMOD und hab dabei festgestellt, dass ich ohne Probleme deren release-Version auch in meiner debug-config benutzen kann. Ich benutze die gleiche Lib also einmal unter /MD und einmal über /MDd. Wieso beißen die sich nicht, müsste ich da nicht auch einen ABI-Mismatch haben ? Oder ist man diesbezüglich aus dem Schneider, sobald man keine Objekte mehr zurückgibt, sondern nur PODs ? (sprich: kein delete, kein new -> kein Problem ? )

nochmals 1000 Dank an euch alle,
ponx

Re: Runtime Libraries - Trauer, Hass, Zorn !

Verfasst: 26.03.2010, 19:53
von Aramis
Gut, um nochmal strukturiert das ganze in Einzelteile zu zerlegen. Es handelt sich um zwei völlig unterschiedliche Probleme, die aber in ihrer Wirkung völllig gleich sind (undefinierte Abstürze und ein ratloser Debugger). Aus deiner anfänglichen Beschreibung habe ich vermutet dass du womöglich gleichzeitig Opfer beider geworden bist, hab sie dann aber evtl. zu wenig stark voneinander angegrenzt:

(i) ABI-Kompatibilität. Damit der C++-Compiler dir erlaubt ein fremdes API aufzurufen, prüft er nur das öffentlich verfügbare API, im Falle einer einfachen Funktion als nur deren Signatur auf korrekte Verwendung. Wenn eine Funktion einen std::string als Parameter haben will, so wird jeder C++-Compiler einen entsprechenden Aufruf zulassen. Wenn der Linker beim Resolven des Aufrufes nach Erfolg hat - sprich er in irgendeiner Importbibliothek oder Übersetzungseinheit ein entsprechendes Symbol findet, so führt auch er keine weiteren Checks durch.

Damit der Aufruf aber problemlos funktioniert, müssen Aufrufer und aufgerufene Funktion sich aber nicht nur darüber einig sein, dass das übergebene Objekt std::string heißt und einen Satz Methoden à la length(), c_str(), find_first_of(), rbegin(), ~string … besitzt. Tatsächlich muss das binäre Layout im Speicher Bit für Bit übereinstimmen. Nachdem der C++-Sprachstandard nur die öffentliche Schnittstelle seiner Container, nicht aber die Implementierung, definiert, ist folgendes legal (Beispiel, vereinfacht, entspricht nicht dem geforderten API eines std::vector's!):

Code: Alles auswählen

template <class T>
class vector {

public:

    bool empty() ..
    size_type length() ..
    T operator[] ..
    

private:

#ifdef DEBUG
    SomeWhat debugging_data;
#endif

   size_t length;
   T* data;
};
Und das ist der Grund, wieso Debug und Release-Builds inkompatibel sind bzw. inkompatibel zueinander sein können ohne dass es dem Linker auffällt. Das gleiche gilt für verschiedene VC-versionen. Theoretisch könnte ein Linker in sein Name-Mangling (das vom Standard bewusst nicht definiert wird) aber auch entsprechende Informationen eingehen lassen um solche Fälle zu erkennen. Macht VC nur leider nicht. Die STL ist das Paradebeispiel, da insbesondere Microsoft in Debug-Builds eine Menge Debug-Checking betreibt und dieses z.B. von VC8 auf VC9 komplett umgeschmissen wurde. Ironischerweise sind die Release-ABIs verschiedener VC-STLs auch noch weitestgehend kompatibel, während Debug-Versionen durch verschiedene Präprozessor-Defines dann auch noch zu sich selber inkompatibel sein können – Microsoft hat gute Arbeit geleistet :-)

(ii) Shared heaps. Dies ist eine Implementierungssache - der C++-Sprachstandard kennt keine `Module` - dieses Konzept wird vom Linker umgesetzt und ist in ähnlicher Form sowohl auf Posix-Systemen als auch unter Windows zu finden.

Unter Windows hat jeder Prozess einen eigenen Heap, WinAPIs wie LocalAlloc operieren auf diesem und funktionieren – afaik – auch über Modulgrenzen. Leider oder gluecklicherweise laufen Allokationen ja meist über den C- bzw C++ way of life, d.h. malloc, calloc, realloc, free, ::new, ::delete. Diese werden von der Standardbibliothek zur Verfügung gestellt, die ihren Heap-Manager selber implementiert (auf dem OS-Heapmanager aufsetzend) und, bei nicht-DLL Runtimes, in jedem Modul auf's neue statisch hinzugelinkt wird. Kurz: jedes Modul hat - vereinfacht - seinen eigenen, logischen Heap. Speicher muss mit dem Allokator freigegeben werden, der ihn auch angefordert hat - und vom gleichen Modul aus. Siehe http://blogs.msdn.com/oldnewthing/archi ... 55966.aspx.
.nicht genau, was du meinst mit "D'tor wird auf unserem Heap aufgerufen und crasht"
Ich gehe im folgenden davon aus, dass (i) erfüllt ist. Der std::string d'tor ist als Template inline in der Standardbibliothek definiert. Beim Aufruf einer DLL–Funktion, die einen string by-value zurückgibt, wird dieser unweigerlich irgendwann zerstört - vom Aufrufer, wobei der Compiler die ihm bekannte Definition des d'tors auf dem Objekt aufruft, der dann aller Wahrscheinlichkeit das ihm bekannte delete aufrufen wird um den für den String allozierten Speicher zu zerstören. Bumm.

Dass es mit Multithreaded-Dll „funktioniert“ ist wiederum ebenfalls ein Implementierungsdetail - oder eher ein Nebeneffekt der Tatsache dass die Heap-Funktionen aus der Laufzeit-DLL importiert werden, und diese - wenn von mehreren Modulen referenziert - nur einmal in den Adressraum geladen wird, womit alle Allokationen von einem Heap stammen. Sich darauf zu verlassen halte ich für nicht ratsam. Das kann sich jederzeit ändern, auf anderen Plattformen gibt es dann womöglich keinen solchen Trick und der ganze Code ist für den Mülleimer. Zudem erfordert die DLL-Runtime auch noch die Installation des Runtime-Redistributables auf jedem Zielsystem (irgendwie müssen die DLLs ja auf das System kommen). Rein theoretisch müssten die auf jedem System schon von einem anderen Programm installiert wurden sein, praktisch ist das irgendwie nie der Fall. Das ist dann Platz 2 auf der Top 10 der häufigsten Stolpersteine für Windows-Entwickler :-)
2. du meintest, all diese Probleme betreffen nur C++, nicht C. Hängt das Problem also direkt und ausschließlich mit den C'tors und D'tors zusammen ? Oder hängt da noch mehr drin ?
beide sind universell, aber insbesondere im Zusammenhang mit C++ gefährlich. Auf der einen Seite wird extensive Verwendung von STL und Boost als guter Stil, idiomatischer Stil gehandelt - andererseits sind nahezu alle STL/Boost-Tools anfällig für (i) und (ii). Hauptsaechlich weil diese a) generell nur das API, nicht das ABI, definieren und b) weil ein Großteil der Funktionalität inline implementiert ist, d.h. vom Compiler in alle Uebersetzungseinheiten und somit auch alle Module dupliziert wird. Schlussendlich hast du z.B. bereits beim Austausch von C-Strukturen (`PODs`) das Problem, dass padding, alignment usw. zwischen verschiedenen Builds mit womöglich verschiedenen Compilern abweichen können.

Grundsätzlich hat es mit c'tors, d'tors nichts zu tun. Folgendes ist beispielsweise völlig in Ordnung -- (test in der DLL implementiert):

Code: Alles auswählen

class __declspec(dllimport) test {

public:
   test();
   ~test();

public:
   void foo();

private:
  void* somewhat;
};

int main(int, char*[]) {
   test* t = new test();
   t->foo();
   delete t;
}
Allokation und Deallokation finden auf dem gleichen (`unserem`) Heap statt, das Objekt gehört uns, obwohl D'tor und C'tor in der DLL liegen. Beide können selber so viel Speicher allozieren und freigeben wie sie wollen (z.b. für test::somewhat) – sie sind ja beide im gleichen Modul.

Mein Lösungsvorschlag oben klang vielleicht zu rabiat. Allgemein gibt es mehrere Wege:
  • Im API auf jegliche „Spielereien“ verzichten - idealerweise komplett C (auch mit entsprechender Linkage). Beim Austausch von Datenstrukturen alle unterstützten Compiler dazu zwingen, diese im Speicher gleich anzulegen (siehe #pragma pack, __attribute__ ((packed))).
  • Komplett auf den by-value Austausch und den direkten Zugriff auf Datenstrukturen verzichten. Siehe letzter Abschnitt.
  • Für den Fall, dass es nur um ein privates Projekt geht und nicht um eine Bibliothek für Jedermann and his dog, lautet die pragmatische Lösung: einfach *immer* alle Module im Projekt mit gleichen Bedingungen kompilieren und linken.
Wir haben uns bei Assimp notgedrungen für Lösung 1 entschieden. std::string war bei uns was (ii) angeht der einzige Problempunkt - alle std::string-APIs wurden durch const char*'s ersetzt (die alten APIs existieren immer noch, wrappen bloß inline die Neuen). Das wurde nötig nachdem es fast 10 Millionen Threads im SF.net Supportforum deswegen gab, alle mit mysteriösen, nicht-debugbaren Abstürzen. (i) hat uns auch Probleme bereitet, da unseres API in Bezug auf Object-Ownership teilweise etwas unsauber designt ist - Instanzen einer bestimmten Klasse werden u.U. vom Aufrufer erstellt, aber von Assimp gelöscht. Anfangs war die Empfehlung nur Multithreaded-Dll zu nutzen (weil es da ja funktionierte …). Leider kann man als externe Lib nicht einfach den Leuten vorgeben was für eine Laufzeitbibliothek sie zu nutzen haben. Anstelle das API zu ändern hat ein überladener operator new für besagte Klasse das Problem gelöst - er führt bereits die Allokation durch den User auf dem Assimp'schen Heap durch. Im oben verlinkten Chen-Artikel entsprich das dem Konzept eines `externen Allokators`.
. Ich benutze FMOD und hab dabei festgestellt, dass ich ohne Probleme deren release-Version auch in meiner debug-config benutzen kann
Ich kenne FMOD und sein API nicht. Ich beziehe mich im folgenden auf diesen GD.net-Artikel.

Code: Alles auswählen

int main ()
{
   // init FMOD sound system
   FSOUND_Init (44100, 32, 0);

   // load song
   handle=FMUSIC_LoadSong ("canyon.mid");

   // play song only once
   // when you want to play a midi file you have to disable looping
   // BEFORE playing the song else this command has no effect!
   FMUSIC_SetLooping (handle, false);

   // play song
   FMUSIC_PlaySong (handle);

   // wait until the users hits a key to end the app
   while (!_kbhit())
   {
   }

   //clean up
   FMUSIC_FreeSong (handle);
   FSOUND_Close();
}
Ein sauber designtes API, das sowohl den Problemen (i) und (ii) aus dem Weg geht:
  • FMOD wird nicht durch by-value übergebene Datenstrukturen repräsentiert, sondern über ein opakes Handle - schlussendlich vermutlich ein Pointer in einen Speicherbereich der FMOD-Runtime. Das Handle selber wird durch ein FMUSIC_LoadSong, FMUSIC_FreeSong in's Leben gerufen und auch wieder zerstört. Auch das ist quasi ein externer Allokator.
  • Um Methoden darauf aufzurufen, werden immer externe Funktionen aus der FMOD-Runtime bemüht, die bloß das Handle übergeben bekommen und dann sicher darauf zugreifen können.
Alex

Re: Runtime Libraries - Trauer, Hass, Zorn !

Verfasst: 26.03.2010, 20:23
von Aramis
Was das ganze werden soll? Ein FAQ-Artikel für die Resourcensektion. Wenn ihr Fehler oder Ungenauigkeiten findet, so meldet euch bitte :-)

- Alex

Re: Runtime Libraries - Trauer, Hass, Zorn !

Verfasst: 26.03.2010, 20:57
von Lord Delvin
Aramis hat geschrieben:Tatsächlich muss das binäre Layout im Speicher Bit für Bit übereinstimmen.
Gibts dafür nicht -fPIC oder versteh ich das jetzt falsch?

@Ressourcenallokation/Ownership: Das wird noch viel schlimmer, wenn irgendjemand Threads auspackt, weil du Speicher mit genau dem Thread zurückgeben musst, der ihn geholt hat. D.h. letztlich willst du eigentlich ne strikte Ownership Struktur haben, d.h. genau der Mensch der Code schreibt der Speicher allokiert muss ihn mit dem selben Thread der ihn bekommen hat auch wieder zurückgeben, also selbe lib & selber thread.

Es klingt zwar irgendwie verlockend mit so zerocopy geschichten Informationen zu transportieren aber erfahrungsgemäß schießt man sich damit nur brutal in den Fuß, weil man zu wenig nachgedacht hat oder der user nicht versteht, was man von ihm will.(Für den Fall, dass du Threads benutzt, im single Threaded Fall ist das eher mal drin)
Gruß

Re: Runtime Libraries - Trauer, Hass, Zorn !

Verfasst: 26.03.2010, 21:34
von Aramis
Gibts dafür nicht -fPIC oder versteh ich das jetzt falsch?
-fPIC ist für position-independent code, d.h. der generierte Maschinencode greift für alle Symboladressen auf eine vom ELF-Loader generierte Tabelle zurück, womit das Modul überall im Adressraum liegen kann. Hat mit dem Problem aber nichts zu tun, denke ich.

EDIT - genauer formuliert.

Re: Runtime Libraries - Trauer, Hass, Zorn !

Verfasst: 28.03.2010, 16:59
von ponx
Wieder erstklassig erklärt Alex, so langsam blick ich durch !

Das mit den external Allocators ist mir noch nicht ganz klar:

1. Wenn das Hauptprogramm über "new" eine Instanz einer Klasse aus meiner DLL anlegt, und diese Instanz irgendwann auch wieder mit delete freigibt, dann hab ich das Prinzip bei (ii) ja eigentlich nicht verletzt - jedes Modul gibt die von ihm allozieren Objekte selbst auch wieder frei. Entsteht das eigentliche Problem dann also erst beim Zugriff auf die Member dieses Objekts, weil die CRT des Hauptprogramm und die meiner DLL jeweils von einem anderen Speicherlayout ausgehen ?

2. Wie genau kann ich dieses Problem mit externen Allocators umgehen, sprich wie habt ihr z.B. in der von dir erwähnten Klasse euer new überladen ? LocalAlloc alloziert mir ja nur einen Speicherblock - muss ich dann parallel immer noch "new()" aufrufen und das Objekt dann in den allozierten Block rüberkopieren ? Oder muss ich sozusagen rekursiv für jede enthaltene Member-Klasse auch das new überladen ?

3. du hast empfohlen, die API idealerweise komplett in C anzubieten, mit entsprechender Linkage. Welche Linker-Einstellung meinst du da genau ? Und das bezieht sich aber wirklich nur auf das API, richtig ? Intern kann ich schön C++ benutzen, mit allem Firlefanz und Compiler-Optimierungen ?

andy / ponx

Re: Runtime Libraries - Trauer, Hass, Zorn !

Verfasst: 28.03.2010, 17:44
von Aramis
1. Wenn das Hauptprogramm über "new" eine Instanz einer Klasse aus meiner DLL anlegt, und diese Instanz irgendwann auch wieder mit delete freigibt, dann hab ich das Prinzip bei (ii) ja eigentlich nicht verletzt
Genau, siehe zweites Beispiel.
Entsteht das eigentliche Problem dann also erst beim Zugriff auf die Member dieses Objekts, weil die CRT des Hauptprogramm und die meiner DLL jeweils von einem anderen Speicherlayout ausgehen ?
Wenn die ABIs inkompatibel sind. Ist (i) erfüllt, so ist beidseitiger Zugriff auf Member usw. vollständig in Ordnung, solange dabei keine Speicherblöcke den Inhaber wechseln, die dann ja wiederum in Bezug auf (ii) kritisch wären (`Inhaber/Owner` eines Speicherblocks: derjenige, der ihn angelegt hat und das Recht/die Pflicht hat ihn auch wieder freizugeben).
. Wie genau kann ich dieses Problem mit externen Allocators umgehen, sprich wie habt ihr z.B. in der von dir erwähnten Klasse euer new überladen
Das ist ein Trick, den ich bislang noch nirgendwo publiziert gesehen habe. Entweder ist er tatsächlich weitestgehend unbekannt, oder er enthält einen Denkfehler, oder er ist den ganz großen Gurus zu trivial.

Header:

Code: Alles auswählen

struct __declspec(dllNNN) AllocateFromDllHeap	
{
	// new/delete overload
	void *operator new    ( size_t num_bytes);
	void  operator delete ( void* data);

	// array new/delete overload
	void *operator new[]    ( size_t num_bytes);
	void  operator delete[] ( void* data);

}; 

class __declspec(dllNNN) MyClass : public AllocateFromDllHeap	
{
	void foo();
}

DLL:

Code: Alles auswählen

void* AllocateFromDllHeap::operator new ( size_t num_bytes)	{
	return ::operator new(num_bytes);
}

void AllocateFromDllHeap::operator delete ( void* data)	{
	return ::operator delete(data);
}

void* AllocateFromDllHeap::operator new[] ( size_t num_bytes)	{
	return ::operator new[](num_bytes);
}

void AllocateFromDllHeap::operator delete[] ( void* data)	{
	return ::operator delete[](data);
}
Verwendung:

Code: Alles auswählen


// Da `new` und `delete` in Wahrheit in der DLL implementiert sind,
// wird der Speicher auf dem DLL-Heap angelegt. Unabhängig von
// der verwendeten Runtime ist problemloser Ownership-Wechsel
// zwischen verschiedenen Modulen möglich.
MyClass* c = new MyClass();
delete c;

// Natürlich auch mit Smart-Pointern
{
std::auto_ptr<MyClass>(new MyClass());
}
. du hast empfohlen, die API idealerweise komplett in C anzubieten, mit entsprechender Linkage. Welche Linker-Einstellung meinst du da genau ?
Damit meinte ich extern "C". Dann ist das API auch tatsächlich von C aus nutzbar, ergo auch von allen Sprachen die irgendwie C-APIs aufrufen können - also praktisch alle. das Ergebnis wäre eine nahezu universell einsetzbare Lib.

Intern kann ich schön C++ benutzen, mit allem Firlefanz und Compiler-Optimierungen ?
Jepp :-)

Re: Runtime Libraries - Trauer, Hass, Zorn !

Verfasst: 29.03.2010, 10:31
von kimmi
Ein kurzer Hinweis der Vollständigkeit halber:
Du solltest in deinem Memory-Struct not entsprechende new / delete-Überladungen einfügen, die nothrow sind.

Gruß Kimmi

Re: Runtime Libraries - Trauer, Hass, Zorn !

Verfasst: 29.03.2010, 13:20
von Aramis
Stimmt, danke für den Hinweis.

Sieht damit so aus:

Code: Alles auswählen

struct __declspec(dllNNN) AllocateFromDllHeap   
{
        // new/delete overload
        void *operator new    ( size_t num_bytes);
        void *operator new    ( size_t num_bytes,const std::nothrow_t&);
        void  operator delete ( void*);

        // array new/delete overload
        void *operator new[]    ( size_t);
        void *operator new[]    ( size_t, const std::nothrow_t&);
        void  operator delete[] ( void*);

}; 

Re: Runtime Libraries - Trauer, Hass, Zorn !

Verfasst: 31.03.2010, 12:35
von ponx
nochmals vielen Dank an euch ! Ich glaub ich hab jetzt alles verstanden, eine letzte Frage zu deiner Memory-Struct, Alex:
Dieses __declspec(dllNNN) wird wahrscheinlich per define durch __declspec(dllexport) ersetzt, ja ? Ich find bei Google nur unseren Thread :)

edit: doch noch ne Verständnisfrage:
Ein Beispiel: Um das shared Heap-Problem zu umgehen, stelle ich in meiner DLL-Klasse über eine static-Methode names "Create" o.ä. sozusagen einen künstlichen ctor zur Verfügung, der intern auf dem Heap eine neue Instanz meiner DLL-Klasse erzeugt und dann per pointer an das Hauptprogramm zurückgibt. Dann hab ich zwar das shared-Heap Problem umgangen, aber crashe bei ABI Inkompatibilität trotzdem, sobald ich drauf zugreife, weil er u.U. die Funktionspointer an einer falschen Stelle vermutet - stimmt das so ?

Ich würde gern erreichen, dass man eine Instanz meiner Klasse benutzen und ganz normal ihre Methoden aufrufen kann, auch wenn man eine andere Runtime benutzt. Wenn ich's richtig verstanden habe ist das theoretisch möglich, solange ich

1. die Instanz von meiner DLL erzeugen und später auch wieder freigeben lasse
2. in meiner DLL-Klasse nicht irgendwelche zusätzlichen Debug-Informationen innerhalb von #ifdef _DEBUG blöcken einbaue
3. meine Methoden-Signaturen nur PODs und pointer übernehmen (wobei alle übergebenen Objekte auch von der DLL erstellt wurden)

Hat das eine Aussicht auf Erfolg, oder ist mein aktuelles C++-Bild doch etwas idealisiert und es führt kein Weg an der passenden Runtime vorbei :) ?

viele Grüße,
ponx

Re: Runtime Libraries - Trauer, Hass, Zorn !

Verfasst: 31.03.2010, 16:11
von Aramis
Dieses __declspec(dllNNN) wird wahrscheinlich per define durch __declspec(dllexport) ersetzt, ja ? Ich find bei Google nur unseren Thread
Oder durch dllimport. Wenn die Header eingebunden werden /aus/ der DLL um selbige zu erstellen muss es export sein, wenn ein Aufrufer dagegen linken will, import. Es ist hässlich, aber scheinbar der einzige brauchbare Weg.

Code: Alles auswählen

// I_AM_CURRENTLY_BUILDING_MY_DLL wird von den Buildsettings der DLL definiert.
#ifdef I_AM_CURRENTLY_BUILDING_MY_DLL
#  define MY_API_ENTITY __declspec(dllexport)
#else
#  define MY_API_ENTITY __declspec(dllimport)
#endif

class  MY_API_ENTITY TheGodOfThisLibrary
{
   …
};
Ein Beispiel: Um das shared Heap-Problem zu umgehen, stelle ich in meiner DLL-Klasse über eine static-Methode names "Create" o.ä. sozusagen einen künstlichen ctor zur Verfügung, der intern auf dem Heap eine neue Instanz meiner DLL-Klasse erzeugt und dann per pointer an das Hauptprogramm zurückgibt. Dann hab ich zwar das shared-Heap Problem umgangen, aber crashe bei ABI Inkompatibilität trotzdem, sobald ich drauf zugreife, weil er u.U. die Funktionspointer an einer falschen Stelle vermutet - stimmt das so ?
Exakt.
Hat das eine Aussicht auf Erfolg, oder ist mein aktuelles C++-Bild doch etwas idealisiert
Ganz im Gegenteil, das ist die richtige Strategie.

Die einfachste Lösung bestünde immer darin exakt zueinander passende Runtimes zu wählen, natürlich. Leider kann man das nicht immer sicherstellen.
Wenn du dir die (C-)APIs bekannter Bibliotheken anschaust, wirst du immer wieder feststellen dass manche scheinbar eigentümliche Konstruktionen nur dazu dienen das copy/access-by-value und ownership-Problem zu umgehen. Reine C++-APIs (also solche, die auch von kompleren Sprachfeatures Gebrauch machen), neigen eher dazu dann DLLs bzw. Import-Libs für alle möglichen Kombinationen aus Compiler, Runtime und WasWeißIchWas bereitzustellen. Die boostpro-Installer beispielsweise. Das ist dann auch wieder nervig. Den Master-Weg gibt es also nicht.

Re: Runtime Libraries - Trauer, Hass, Zorn !

Verfasst: 01.04.2010, 11:24
von ponx
Ich glaub es klappt jetzt, hurra ! :P
Eine letzte Verständnisfrage hab ich noch, und zwar bin ich noch nicht sicher, was alles von der Runtime der Hauptapplikation alloziert wird, sobald sie eine Instanz meiner Dll-Klasse erstellt. Seh ich's richtig, dass dann wirklich alle Member meiner Dll-Klasse mit der Runtime der Hauptapplikation initialisiert werden, und erst wenn irgendwo new() innerhalb der Dll aufgerufen wird, greift die Runtime meiner DLL?
Das hieße: Ich brauche keine Create()-Methode, solange Instanzen meiner DLL-Klasse ..
1. die Ownership nicht wechseln
2. nie per value übergeben werden
3. als Member nur pointer und PODs haben (weil inkompatible ABIs zum Crash führen, wenn das Hauptprogramm "meine" Member alloziert hat, und ich in meiner DLL drauf zugreife)

Ich hab's vorhin auch mal ausprobiert ohne die Create() Methode und den normalen ctor benutzt, und alle Kombinationen (vc8 vs. vc9, debug und release runtime (jeweils statisch gelinkt)) liefen bei mir... was natürlich nicht viel heißt, soviel Bauchgefühl hab ich mittlerweile :)

Hab ich 's begriffen ?? sag ja ! :)

Re: Runtime Libraries - Trauer, Hass, Zorn !

Verfasst: 01.04.2010, 17:31
von Aramis
weil inkompatible ABIs zum Crash führen, wenn das Hauptprogramm "meine" Member alloziert hat, und ich in meiner DLL drauf zugreife
Inkompatible ABIs führen immer zu undefiniertem Verhalten, da ist die Speicher-Ownership Sache dann egal.
Seh ich's richtig, dass dann wirklich alle Member meiner Dll-Klasse mit der Runtime der Hauptapplikation initialisiert werden, und erst wenn irgendwo new() innerhalb der Dll aufgerufen wird, greift die Runtime meiner DLL?
Hängt davon ab *wo* der c'tor deiner Klasse definiert ist. Angenommen du schreibst ihn inline in die Klassendeklaration hinein, oder du verwendest den vom Compiler bereitgestellten Default-c'tor. In beiden Fällen wird der c'tor vom Compiler in dein Hauptprogramm hineingezogen, und alles geschieht auf diesem Heap und dieser Runtime. Wenn der c'tor allerdings in der DLL definiert wird, so spielt sich alles was er macht auf dem DLL Heap ab.
Hab ich 's begriffen ?? sag ja !
Ja :-) Zum Thema Bauchgefühl: wenn ein Programm ziemlich frühzeitig an absolut merkwürdiger Stelle (meist in der STL) mit unbrauchbren Debug-Informationen abstürzt, hast du ganz gute Chancen dass es mit ABIs oder Heaps zusammenhängt.

Re: Runtime Libraries - Trauer, Hass, Zorn !

Verfasst: 03.04.2010, 11:34
von dowhilefor
Kurze Frage was zum Geier ist eine Abi ;) ich hab nämlich kein Abi bin also dumm, wäre aber dankbar für eine Aufklärung.

Meint ihr Apis? :)[/size]

Re: Runtime Libraries - Trauer, Hass, Zorn !

Verfasst: 03.04.2010, 12:32
von Schrompf
Nach oben scrollen! Aramis hat das bereits in einem seiner umfangreichen Erklärungen geschrieben.

Nebenbei: Du bekommst von mir den Orden der Kategorisierung in Silber. Für Gold hatten noch ein paar Bildchen, Diagramme und so mit reingehört :-)

Re: Runtime Libraries - Trauer, Hass, Zorn !

Verfasst: 03.05.2011, 08:46
von Brainsmith
Ich mach mal den thread-Totengräber. Aber man sagt ja immer: Benutz vorher die Suchfunktion. =D

. Wie genau kann ich dieses Problem mit externen Allocators umgehen, sprich wie habt ihr z.B. in der von dir erwähnten Klasse euer new überladen


Das ist ein Trick, den ich bislang noch nirgendwo publiziert gesehen habe. Entweder ist er tatsächlich weitestgehend unbekannt, oder er enthält einen Denkfehler, oder er ist den ganz großen Gurus zu trivial.
Das Thema interessiert mich gerade, da ich selber an einer Bibliothek arbeite, die ich eventuell als dll anbieten will. Kann ich mir das überladen nicht sparen, wenn ich als Kommunikation eine einzige Klasse habe, die alles regelt und diese über ein Callback initialisiert wird? Oder geht da gerade die Theorie an mir vorbei?

Re: Runtime Libraries - Trauer, Hass, Zorn !

Verfasst: 03.05.2011, 22:38
von Aramis
Ich hab grade Probleme mir vorzustellen was du damit genau meinst, vielleicht kannst du ein kurzes Code-Beispiel machen?

Gruss, Alex

Re: Runtime Libraries - Trauer, Hass, Zorn !

Verfasst: 03.05.2011, 22:54
von CodingCat
Brainsmith hat geschrieben:Das Thema interessiert mich gerade, da ich selber an einer Bibliothek arbeite, die ich eventuell als dll anbieten will. Kann ich mir das überladen nicht sparen, wenn ich als Kommunikation eine einzige Klasse habe, die alles regelt und diese über ein Callback initialisiert wird? Oder geht da gerade die Theorie an mir vorbei?
Das hängt sehr davon ab, was du unter Kommunikation verstehst. Letztlich geht es hier um das Erzeugen und Zerstören von Objekten bzw. noch genauer die konkrete Quelle des Speichers für dieselben. Wenn du das Erzeugen und Zerstören aller Objekte konsequent in fest gelinkte Funktionen derselben DLL implementierst, einfacher gesagt, außerhalb der DLL NIE von new und delete im Zusammenhang mit Klassen der DLL Gebrauch machst, kannst du dir das Überladen tatsächlich sparen. Schlussendlich geht es darum, dass ein Objekt stets auf demselben Heap zerstört wird, auf dem es auch erzeugt wurde, was du auch in diesem Fall erreichst. Allerdings forderst du so vom Nutzer, dass er sich dieser Tatsache stets bewusst ist und deine API entsprechend korrekt benutzt, weswegen du in diesem Fall dafür Sorge tragen solltest, dass new und delete außerhalb der DLL für entsprechende Klassen nicht mehr verfügbar sind (COM arbeitet z.B. implizit so, weil es nur Interfaces anbietet).

Was in deinem Post irritierend ist, ist, dass du von Callbacks sprichst. Wenn du aus einer DLL heraus eine fest gelinkte Funktion einer Benutzeranwendung zurückrufst, befindet sich die Funktion der Benutzeranwendung auch wieder im Kontext dieser Anwendung, verwendet also den Anwendungs-Heap, nicht den DLL-Heap, das Problem bist du also damit NICHT los.

Bzgl Tricks: Habe gerade die vorangegangenen Posts überflogen, und steuere hier einfach mal einen Link zu meiner Website bei, auf der ich vor einiger Zeit noch einige weitere Beobachtungen zum Thema festgehalten habe: http://www.alphanew.net/index.php?secti ... &newsID=86

Re: Runtime Libraries - Trauer, Hass, Zorn !

Verfasst: 04.05.2011, 09:39
von Brainsmith
Was in deinem Post irritierend ist, ist, dass du von Callbacks sprichst.
Ich bin selber gerade ein wenig verwirrt. Die Sache mit den Callbacks war auch eigentlich mehr ins Blause geraten. =D Deshalb erklär ich am besten mal ein wenig.

Ich arbeite in meiner Freizeit an einer kleinen Soundengine. Ich hatte sie bis jetzt als lose Klassensammlung in einem Test-3D-Projekt, um die Funktionalität zu gewährleisten. Ich würde die Engine allerdings gerne wiederverwenden können, weshalb sich eine lib oder eine dll anbieten würde. Als lib habe ich das Ganze zum laufen gebracht, auch wenn ich da noch mit einigen Schönheitsproblemen kämpfe. Jetzt hab ich allerdings ein Problem: Alles, was ich über C++ weiß, hab ich mir selbst beigebracht und angelesen. Daher weiß ich leider noch nicht so genau, wo wer wann Speicher allokiert und freigibt. Bis ich auf diesen Thread gestoßen bin, wusste ich nichtmal, dass diese Art von Problem überhaupt existiert. Bitte korrigiert mich, wenn ich falsch liege:
Sobald ich eine Klasse aus der dll mit new erzeuge, bekomme ich Probleme mit den Heaps. Warum hat man das nicht mittlerweile gefixt, sodass new-Anweisungen, die sich auf Klassen der dll beziehen, auch immer auf dem Heap der dll landen? Oder ist diese Frage vielleicht zu naiv?

Im Idealfall muss man doch einfach nur sagen: cMachmal pMachMalSchneller* = new cMachmal(Übergabekram);
Wenn ich das nun mit einer Klasse aus der dll mache, die ich in meiner End-Anwendung so initialisiere, wird alles auf den Heap der Anwendung gestopft. Wenn ich die Instanz wieder löschen will, fliegt mir alles um die Ohren, ohne, dass ich eine Ahnung hätte, woher das kommt. Die nächste Frage wäre: Wenn eine Klasse so instanziert habe und diese Klasse dann andere dll-Klassen instanziert, werden diese dann auf dem dll-Heap oder dem Anwendungsheap landen?

Mein bisheriger Plan war es, den Datenaustausch der Anwendung und der dll über konstante Referenzen zu handhaben. Diese geben ja keine Probleme, wie ich es verstanden habe. Also bleibt das Problem der Instanzierung der Klasse, die die Anwendung nutzt. Eigentlich finde ich die Idee der Überladung des new-Operators ziemlich elegant. Auf der anderen Seite habe ich das noch nie gemacht und bin mir auch (wie man meinem Text wahrscheinlich ansehen kann) in keinster Weise sicher, wann das genau notwendig ist.

Wenn jemand einen Artikel oder eine Buchempfehlung hat, sodass ich diese Wissens-Lücken schließen kann, wäre ich auch sehr dankbar.
@CodingCat
Den Link muss ich in Ruhe genießen. So, wie ich es verstanden habe, gibts 3 Lösungen:
- nichts per Value teilen
- eine nicht Header-basierte Bibliothek (wobei ich mich damit noch nicht auseinandergesetzt habe)
- Allokatoren benutzen

Ich hoffe, mein Text ist nicht zu verquirlt. Es ist noch früh. Zusätzlich gilt es zunächst, das Problem in allen Facetten zu verstehen, damit ich das immer umgehen kann, wenn es mal droht, mich zu verfolgen. :D

Edit: möglicherweise steht hier auch einiges an Unsinn oder ich habe etwas grundsätzlich falsch verstanden. Muss halt einiges mehr lernen, um gute Fragen und guten Code zu produzieren.

Re: Runtime Libraries - Trauer, Hass, Zorn !

Verfasst: 04.05.2011, 11:38
von Krishty
Brainsmith hat geschrieben:Sobald ich eine Klasse aus der dll mit new erzeuge, bekomme ich Probleme mit den Heaps.
Falsch.

Zuerst einmal: Du hast unterschiedliche Module von kompiliertem Programmtext. Ein Modul, was mindestens da ist, ist deine Exe. Jede weitere DLL, die du lädst, ist ein weiteres Modul – auch deine Audiobibliothek. Statische Bibliotheken sind keine Module, weil ihr Text in der Exe oder DLL landet, zu der sie gelinkt werden.

Probleme hast du dann, wenn du irgendwas(!) in einem Modul allokierst und es in einem anderen Modul freigibst.

Das rührt daher, dass jedes Modul nur seinen „eigenen“ Freispeicher sieht. Eine DLL weiß ja nicht, von welcher Exe sie mal geladen wird und welchen Freispeicher diese Exe benutzen wird, darum kriegt sie ihren eigenen, persönlichen.

Wenn du also in Programmtext, der in der DLL landet, etwas allokierst und es von Programmtext, der in der Exe liegt, aus freigeben willst, kracht es – weil der Programmtext der Exe nur „seinen“ Freispeicher kennt und nicht den der DLL (ergo auch nicht den Zeiger, den er da zerstören soll). Genau so, wenn du in der Exe allokierst und in der DLL freigibst. Oder in einer DLL allokierst und in einer anderen DLL freigibst. Halt jedes Mal, wenn es über eine Modulgrenze hinaus geschieht.

(Das ist btw nicht nur für Allokationen der Fall, sondern auch für Ausnahmen. Eine Ausnahme im einen Modul zu werfen und im anderen zu fangen ist … undefiniert. Da kann (und wird) alles passieren. Darum müsste theoretisch jeder Aufruf einer Funktion aus einem anderen Modul durch try { … } catch(...) { }; abgesichert werden.)
Brainsmith hat geschrieben:Die nächste Frage wäre: Wenn eine Klasse so instanziert habe und diese Klasse dann andere dll-Klassen instanziert, werden diese dann auf dem dll-Heap oder dem Anwendungsheap landen?
Immer auf dem, von dem aus der Text läuft. Wenn der allokierende Text in eine DLL kompiliert wurde, wird die Instanz im Freispeicher der DLL landen. (Von Ausnahmen wie überladenen new-Operatoren mal abgesehen.)

Und das ist dann auch die Erklärung dafür, warum es funktioniert, wenn man die C-Laufzeitbibliothek (CRT) als DLL linkt: Wenn Modul A etwas allokiert, ruft es operator new auf. Der ist in der Laufzeitbibliothek implementiert, und die ist wiederum ein eigenständiges Modul (MSVCRT.DLL). Die eigentliche Allokation wird also von MSVCRT.DLL durchgeführt. Wenn Modul B den Speicher wieder freigeben soll, ruft es operator delete auf – der auch in MSVCRT.DLL implementiert ist. Also wird vom selben Freispeicher aus zerstört, wie allokiert wurde, und alles ist gut.
Linkt man die CRT hingegen statisch, bekommen die Module A und B je eine eigene Kopie der Implementierung von operator new und operator delete, und die Allokation und Freigabe geschehen mit unterschiedlichen Freispeichern.

Re: Runtime Libraries - Trauer, Hass, Zorn !

Verfasst: 04.05.2011, 11:54
von donelik
Da ich früher häufig über dieses "Problem" stolperte, bin ich dafür dass Krishtys Text bei "Artikel, Tutorials und Materialien" aufgenommen wird.

Schön ausformuliert und von Beispielen begleitet: Sehr fein!

Re: Runtime Libraries - Trauer, Hass, Zorn !

Verfasst: 04.05.2011, 12:04
von Krishty

Re: Runtime Libraries - Trauer, Hass, Zorn !

Verfasst: 04.05.2011, 14:51
von CodingCat
Brainsmith hat geschrieben:Den Link muss ich in Ruhe genießen. So, wie ich es verstanden habe, gibts 3 Lösungen:
- nichts per Value teilen
- eine nicht Header-basierte Bibliothek (wobei ich mich damit noch nicht auseinandergesetzt habe)
- Allokatoren benutzen
Die Antwort erübrigt sich eigentlich nach Krishtys ausführlichen Erläuterungen, by Value / by Reference sagt erstmal nichts darüber aus, es kommt letztlich immer ganz drauf an, was die Klasse am Ende konkret tut und in welchem Modul sie das tut, siehe Krishtys Modulerklärungen. Mit konstanten Referenzen ist man relativ sicher, aber nicht mal das ist eine Garantie, z.B. wenn im Hintergrund Copy-On-Write betrieben wird.

Die gezielte Überladung von new und delete ist auf jeden Fall eine recht elegante Lösung, allerdings löst auch das eben nur das Teilproblem geteilter Objekte eigener Klassen. Letztlich musst du dir den in diesem Thread beschriebenen Tatsachen immer bewusst sein, und bei header-only Bibliotheken möglicherweise sogar über die internen Abläufe und Garantien der vorliegenden Bibliothek Bescheid wissen, wie z.B. die Speicherverwaltung mittels STL Allokatoren.

Re: Runtime Libraries - Trauer, Hass, Zorn !

Verfasst: 04.05.2011, 15:00
von Brainsmith
Ich glaube, so langsam hab ichs. Ich muss mir das alles noch ein paar mal durchlesen.

Vielen Dank für die Erklärungen.