Kompatiblität von DLLs

Alles zu Zusi-Performance, Frameraten, ruckelnden Bildern, Grafik, Treibern usw.
Antworten
Nachricht
Autor
Benutzeravatar
F. Schn.
Beiträge: 6827
Registriert: 24.10.2011 18:58:26

Kompatiblität von DLLs

#1 Beitrag von F. Schn. »

So, ausgehend vom Zusi-Treffen und mit Blick auf die aktuellen DLLs mal ein paar Gedanken zum Thema Kompatiblität von DLLs und Pointern auf Strukturen in DLLs.
Das Thema richtet sich primär an Carsten, der ja die Schnittstellen der DLLs festlegt.
  1. Der Name von Methoden muss eindeutig sein. Methoden dürfen nicht wie in vielen Programmiersprachen üblich überladen sein, d.h. sich nur in den Parametern unterscheiden.
  2. Die Signatur von Methoden sollte sich natürlich nicht ändern. Das heißt, es muss Reihenfolge, Anzahl und Datentyp der Parameter gleich sein, es darf sich aber z.B. der Wertebereich ändern.
    1. Dies sollte auch DLL-Übergreifend der Fall sein. Das heißt, es ist schlecht, wenn eine DLL eine Init mit einem Parameter hat, und die andere mit null Paramtern.
    2. Wenn das nicht beachtet wird, ist das bei x64 trotzdem kein Problem. Wenn eine Funktion einen Parameter mehr bekommt, können alte DLLs weiterhin mit dem neuen Zusi laufen und neue DLLs laufen mit dem alten Zusi, sofern sie nicht auf den Paramter zugreifen. Wenn sie doch zugreifen, bekommen sie bei Zahlen Müll, bei Pointern stürzen sie ab.
    3. Bei x86 (32 bit) ist bei __stdcall aber (frei nach Alwin) Tag des Kotzenden Einhorns und der Programmierer hat den Hauptpreis gewonnen und muss die Größe des zu bereinigenden Stacks von Hand in Assembler festlegen. Zusi könnte da zwar vermutlich auch selbst aufpassen, aber vermutlich wird auch das Eingriffe in Assembler erfordern. Also einfachste Lösung: 32bit aufgeben, oder einfach unterschiedliche Namen verwenden.
    4. Der Standard-Name für Funktionen, die geänderte Parameter bekommen haben, lautet unter Windows "Blub" -> "BlubEx" (Extension) -> "BlubEx2" -> ...
    5. Wenn Zusi die alten DLLs unterstützen will, kann Zusi prüfen, ob eine "BlubEx" existiert und wenn nicht die "Blub" aufrufen.
  3. Übergabe von Strukturen von Zusi an eine DLL:
    1. Grundsätzlich gilt: Die Reihenfolge der Felder in einer Struktur darf nicht geändert werden. Das heißt neue Felder müssen immer ganz unten dazu geschrieben werden.
    2. Werden zusätzliche Felder in die Struktur eingefügt, muss man nicht zwingend eine neue Methode aufmachen. Man muss aber dafür ein paar Vorarbeiten getroffen haben:
    3. Option 1: Man macht einfach ein paar "Reserviert: Immer 0"-Felder ans Ende der Struktur (oder auch zwischen rein). Die DLL greift auf diesen Wert dann einfach nicht zu, man kann den Datentyp dann nachher weitgehend beliebig ändern, solange er gleich groß ist.
    4. Option 2: Man macht als erstes Feld in der Structure ein Feld, in dem die Größe der Struktur angegeben ist. Also sitz_t size = sizeof(Structure); Einige Windows-Messages haben so ein Vorgehen. Die DLL kann dann anhand dieses Größen-Feldes unterscheiden, ob es auf die neu hinzugekommenen Felder zugreifen darf oder nicht. Alte DLLs würden weiterhin funktionieren, neue könnten bewusst feststellen, was sie gerade machen wollen. Edit: Zudem müssen die Strukturen als var übergeben werden, ansonsten geht die 32-Bit-Verison nicht.
    5. Option 3: Man macht gar keine Vorbereitungen. Wenn man die Reihenfolge der Felder in der Struktur nicht ändert und neue Felder immer ganz ans Ende schreibt, funktionieren unter allen Prozessor-Architekturen die alten DLLs weiterhin. Neue DLLs sind dann unproblematisch, wenn sie nicht auf die neuen Felder zugreifen. Wenn sie doch auf die neuen Felder zugreifen, aber mit dem alten Zusi betrieben werden, stürzt Zusi ab. Das wäre aber in meinen Augen trotzdem ein verkraftbarer Sonderfall. Edit: Zudem müssen die Strukturen als var übergeben werden, ansonsten geht die 32-Bit-Verison nicht.
    6. Im Notfall kann man dann noch eine neue Methode aufmachen, aber in meinen Augen ist das nicht nötig. Ich denke, Option 2 wäre die einfachste.
Zuletzt geändert von F. Schn. am 17.09.2023 01:12:16, insgesamt 1-mal geändert.
Diese Signatur möchte folgendes bekannter machen: ZusiWiki · ZusiSK: Streckenprojekte · YouTube: Objektbau für Zusi · euirc: Zusi-Chat

Benutzeravatar
F. Schn.
Beiträge: 6827
Registriert: 24.10.2011 18:58:26

Re: Kompatiblität von DLLs

#2 Beitrag von F. Schn. »

So, noch ein paar Gedanken zur procedure Berechnung(Instanz:Pointer; dt:double; AntriebsRenderModus:TAntriebsRenderModus; Mehrfachtraktionsdaten:TMehrfachtraktionsdaten); stdcall;
bzw. ein paar Ergänzungen bzw. eigentlich sogar Korrekturen zu oben. Konkret geht es um den TMehrfachtraktionsdaten-Parameter. TAntriebsRenderModus ist eine enum, aber TMehrfachtraktionsdaten ist eine Struktur.

4. Übergabe von Strukturen von Zusi an eine DLL ohne das Stichwort "var":
In der 64-Bit-Version wird die Angabe stdcall ignoriert und stattdessen die x64 calling convention angewendet. Strukturen bis zur Größe von 64 bit werden hier ganz normal übergeben. Bei größeren Strukturen wird stattdessen ein Pointer auf die Struktur übergeben. Dies führt effektiv dazu, dass TMehrfachtraktionsdaten wie eine konstante Referenz, also quasi wie ein TPMehrfachtraktionsdaten oder wie ein var TMehrfachtraktionsdaten übergeben wird. Wenn ich also die Mehrfachtraktonsdaten nicht benötige, und besonders faul bin, kann ich auch darauf verzichten, die Klasse TMehrfachtraktionsdaten nach C zu konvertieren und stattdessen einfach void* schreiben.

Für die Zukunft heißt das: Künftige Änderungen der TMehrfachtraktionsdaten sind in 64 bit nicht so wichtig. Stattdessen gilt, was ich oben zu 3. Übergabe von Strukturen von Zusi an eine DLL geschrieben habe. Was im Prinzip relativ Handzahm ist.

Anders natürlich bei 32bit __stdcall. Für var Prot:TProtokollFst gilt zwar das gleiche. Aber TMehrfachtraktionsdaten (ohne var) wird auf 32-Bit-Vielfaches aufgeblasen und auf den Stack gelegt. Mit dem Ergebnis, das sich unter 32 bit effektiv die Signatur ändert, wenn man Änderungen an der Struktur macht.

Und das heißt für die Zukunft: Es gilt das, was ich unter 2. Die Signatur von Methoden geschrieben habe. Das heißt, es gibt jetzt sogar für 32 bit einen schwer vorhersehbaren Fall, bei dem Fehler in der 64-bit-Version unentdeckt bleiben können, in der 32-bit-Version aber schweren Schaden anrichten können. Noch dazu wird der Fehler nicht bemerkt, wenn die Prozedur nicht aufgerufen werden muss, z.B. weil bestimmte Prozeduren nicht aufgerufen werden, wenn der Spieler den Zug nicht fährt. (Wenn sie aufgerufen wird, Crasht es aber in diesem Fall zuverlässig.)

Ich würde daher empfehlen, bei DLLs Strukturen grundsätzlich mit dem var-Schlüsselwort (oder const-var-Schlüsselwörtern, wenn es die in Delphi gibt) zu übergeben.

5. Enums:
Enums und bools werden in vielen Fällen auf 32 bit aufgeblasen. Aber in dem aufgeblasenen Rest kann Müll stehen. Im Endeffekt ist das nur bei enums relevant: Dort muss man aufpassen, wenn man die Grenze von 256 Einträgen überschreitet. Ansonsten ist das Hinzufügen von Enum-Einträgen aber erst mal unproblematisch. C-Nutzer müssen halt von byte erben. (-> enum class TAntriebsRenderModus : byte)
Diese Signatur möchte folgendes bekannter machen: ZusiWiki · ZusiSK: Streckenprojekte · YouTube: Objektbau für Zusi · euirc: Zusi-Chat

Benutzeravatar
F. Schn.
Beiträge: 6827
Registriert: 24.10.2011 18:58:26

Re: Kompatiblität von DLLs

#3 Beitrag von F. Schn. »

So, noch eine kleine ergänzende Erläuterung zu Thema:
Führt es zu Speicherproblemen, wenn die Struktur im Zusi-Code vergrößert wird, und die DLL davon nichts weiß? (Angenommen die Struktur wird als const oder var übergeben.)
Kurze Antwort: Nein. Der Zugriff auf Felder und wie Daten in Feldern abgelegt sind, ist standardisiert. Wenn die DLL die Struktur einmal komplett kopieren möchte, ist es Zusi egal, ob die DLL sie wirklich komplett oder nur zur Hälfte kopiert, es bekommt Zusi ja schließlich eh nicht mit.

Was nicht immer standardisiert ist, ist wie man die Struktur anlegt und entsorgt. Aber wenn man sie als const oder var übergibt, ist sowohl für anlegen wie auch entsorgen (jeweils erst mal auf dem Stack) Zusi zuständig. Das gleiche gilt bei 64-bit-Programmen die implizit die x64-calling-convention nutzen. Beim 32-bit-Stdcall ist hingegen die DLL für das entsorgen zuständig. Wie sie das genau macht, ist Standardisiert, aber dazu muss die DLL die korrekte Größe der Struktur kennen und zwar am besten zur Kompilierzeit, sonnst hat man Spaß mit Assembler... Weswegen ich mich wie gesagt für var stark mache.

Etwas anders kann die Sache aussehen, wenn man die Struktur mit new auf dem Heap anlegt, und dann Pointer zwischen der DLL und Zusi austauscht. In dem Fall muss der selbe sie entsorgen, der sie auch angelegt hat, sonnst muss man den Memory Manager bedenken. Aber auch darum geht es hier ja nicht. (In der Tat müsste man sich über sowas bei der function VariantenName(index:integer):PUnicodeChar; eigentlich Gedanken machen, aber bislang war das nicht Praxisrelevant.)
Diese Signatur möchte folgendes bekannter machen: ZusiWiki · ZusiSK: Streckenprojekte · YouTube: Objektbau für Zusi · euirc: Zusi-Chat

Benutzeravatar
Carsten Hölscher
Administrator
Beiträge: 33548
Registriert: 04.07.2002 00:14:42
Wohnort: Braunschweig
Kontaktdaten:

Re: Kompatiblität von DLLs

#4 Beitrag von Carsten Hölscher »

Mit 3.5.8.0 sollte es dann hoffentlich laufen wie gewünscht?

Carsten

Benutzeravatar
F. Schn.
Beiträge: 6827
Registriert: 24.10.2011 18:58:26

Re: Kompatiblität von DLLs

#5 Beitrag von F. Schn. »

Sieht gut aus.
Dann muss ich mal die Maschinerie anlaufen lassen, mit der ich dann mal die ersten Demos für .Net herausbringen kann.
Du hast ein paar auskommentierte Methoden hinterlassen. Dazu werde ich dann noch einen 2. Beitrag anfügen.

Hinweis an alle:
Änderungen sind: PWideChar, Signaturänderung von Berechnung -> var und HSGesperrt, Typo Zeile 687, sowie die erwähnten auskommentierte Methoden.
Diese Signatur möchte folgendes bekannter machen: ZusiWiki · ZusiSK: Streckenprojekte · YouTube: Objektbau für Zusi · euirc: Zusi-Chat

Benutzeravatar
F. Schn.
Beiträge: 6827
Registriert: 24.10.2011 18:58:26

Re: Kompatiblität von DLLs

#6 Beitrag von F. Schn. »

So, dann ein paar weitere Gedanken:
6.: Übergabe von Klassen an Methode
Beispiel: Die (vermutlich geplante) Methode function Zugstatus(FzgNode:TTreenode):byte; gibt einen Zeiger auf eine Klasse zurück, den die DLL bis zum Ende der Methode verwenden kann.
Dies ist in DLLs eine große Hürde. Nehmen wir mal die Methode TTreenode -> procedure Expand();. Diese Methode ist nicht virtuell. Sie ist also Teil des Codes von Delphi. Im Endeffekt wird also der Code für die Methode mit der Zusi.exe ausgeliefert. Jetzt kann die DLL auch gegen Delphi kompiliert werden. Aber das geht dann nur für Delphi-Programme und nur, wenn die Delphi-Version der DLL die selbe interne Organisation von TTreenode erwartet, wie die exe-Datei. Der Code würde dann quasi noch ein zweites mal mit der DLL ausgeliefert werden.
Ein bisschen einfacher ist es bei der Virtuellen Methode function GetOwner; Diese Methode funktioniert so: Die Klasse hat an allererster Stelle einen Zeiger auf alle Virtuellen Methoden; der sogenannte vtable. Ab der zweiten Stelle folgt dann der eigentliche Inhalt der Klasse. Wenn jetzt eine DLL kommt, nimmt sie sich einfach diesen Zeiger und führt daher Code vom Zusi-Delphi aus. Damit ist es der DLL egal, wie der "Inhalt der Klasse" ab der zweiten Stelle jetzt konkret strukturiert ist.
In diesem Fall hilft das noch nicht ganz weiter, da die Methode noch die Delphi-Calling-Convention hat, wir von Delphi keine Garantie über die Methodenreihenfolge in der vtable bekommen und es ohnehin die falschen Methoden sind. (Dynamisches Linking lass ich auch mal außen vor, haben wir hier nicht.)

7.: Übergabe von Interfaces an Methoden
Eine Lösung, die hingegen möglich seien müsste, wäre, der DLL keine Klasse sondern ein Interface zu geben. COM nutzt Interfaces exzessiv, aber im Grundsatz ist COM dafür nicht notwendig oder besonders sinnvoll. Wir können also das geerbte Interface und die GUID weglassen, diese wären eh nur für das Aufrufen von außen relevant.
Bei einem Interface ist nicht viel mehr zu beachten, als bei Punkt 2 und 3. (Also Signaturen der Methode wie oben besprochen, stdcall nötig, und keine Reihenfolgeänderung, aber hinzufügen möglich.) Propertys sollten nicht verwendet werden.
Das Interface wird dann von einer Klasse implementiert, die die DLL nie wirklich zu sehen bekommt. Der Aufbau dieser Klasse ist der DLL dann egal. Beispiel für ein mögliches Interface:

Code: Alles auswählen

type ITreeBuilder = interface
   function ErstelleKindknoten(Farbe: Integer, Text: PWideChar): ITreeBuilder; stdcall;
   procedure SetzeText(Text: PWideChar); stdcall;
   procedure LoescheKnoten(); stdcall;
end;
Mit diesem müsste man so etwas wie den Zugstatus und die TCP-Nodes lösen können.

Nachträgliche Ergänzung zu den Punkten weiter oben:
3. g: Dürfen sich die Namen von Feldern in einer Struktur ändern? Ja. Der Zugriff erfolgt über Index.
Diese Signatur möchte folgendes bekannter machen: ZusiWiki · ZusiSK: Streckenprojekte · YouTube: Objektbau für Zusi · euirc: Zusi-Chat

Antworten