Kompatiblität von DLLs

Alles zu Zusi-Performance, Frameraten, ruckelnden Bildern, Grafik, Treibern usw.
Antworten
Nachricht
Autor
Benutzeravatar
F. Schn.
Beiträge: 7156
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 (records) 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. Edit: Es dürfen keine Felder gelöscht werden. Sie müssen gegen ein anderes Feld vom selben Datentyp (oder Datentyp der selben Größe) ersetzt werden oder gegen ein Platzhalter-Feld "Reserviert" ersetzt werden, das an der selben Position in der Reihenfolge stehen muss.
    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.
    7. Die Strukturen TGrundfahrdaten und TKombischalterFkt sind Strukturen (records) die in einer anderen Struktur (record) verwendet werden. Hier dürfen keine zusätzlichen Felder eingefügt werden. Man kann daher nur Datentypen ändern oder Felder auf Reserviert und zurück ändern.
Zuletzt geändert von F. Schn. am 30.10.2024 16:19:53, insgesamt 5-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: 7156
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: 7156
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 ohne const oder var 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.)
Zuletzt geändert von F. Schn. am 01.10.2024 21:46:51, insgesamt 1-mal geändert.
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: 33801
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: 7156
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: 7156
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

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

Re: Kompatiblität von DLLs

#7 Beitrag von F. Schn. »

Nachträgliche Ergänzung zu den Punkten weiter oben:
Zu 3a: Löschen von Felder ergänzt.
Edit 30.10.2024: Punkt 3g Strukturen in Strukturen eingefügt.
Diese Signatur möchte folgendes bekannter machen: ZusiWiki · ZusiSK: Streckenprojekte · YouTube: Objektbau für Zusi · euirc: Zusi-Chat

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

Re: Kompatiblität von DLLs

#8 Beitrag von F. Schn. »

8. Zusätzliche technische Details zum internen Aufbau von Structures (records in Delphi):
Das Grundsätzliche habe ich ja schon unter Punkt 3 behandelt, auf Wunsch aber noch mal ein Abschnitt für die Details.
Beim Aufbau von Strukturen/Records gilt, dass sich aus der Reihenfolge der Felder in der Record und deren Datentypen ein Aufbau ergibt, an welcher Adresse innerhalb des Records welcher Wert zu erwarten ist. Diesen Aufbau müssen DLL und Zusi identisch verstehen - oder zumindest identisch mit Bezug auf die Werte, auf die es jeweils ankommt.

Grundsätzlich gilt, dass alle Felder nacheinander stehen. Enums sind in Delphi 1 byte, wenn das byte ausreicht. Records, die in der Record vorkommen, werden weitgehend behandelt, als wie wenn die einzelnen Member nacheinander in der übergeordneten Record stehen würden.
Jetzt gibt es darüber hinaus aber noch Alignment. Das heißt, der Kompiler versucht, die Adressen so zu legen, dass sie aus Sicht des Datentyps "gerade" sind. Also ein 2-Byte-Wert soll auf einer Adresse liegen, die ein vielfaches von 2 ist, ein 4-Byte-Wert soll auf einer Adresse liegen, die ein vielfaches von 4 ist, und ein 8-Byte-Wert auf ein vielfaches von 8. Das bedeutet, dass der Kompiler zwischen einzelnen Werte Lücken lässt, die erst mal ungenutzt sind. Records, die in Records liegen, werden dabei stets als ganzes verschoben. Es wird nicht umsortiert. Arrays werden als Aneinanderreihung ihrer Einträge gehandhabt.

Beispiel: Die Record TKombischalterFkt besteht aus einer Enum (1) und einem Single (4). Der Kompiler lässt jetzt zwischen dem Enum und der Single 3 bytes Abstand.

Beispiel: Die Record TProtokollFst.
Zunächst kommt 3x TKombischalterFkt, diese wird wie oben Beschrieben als enum(1) + 3 Abstand + Single (4) ausgeführt, und das dann drei mal. Bei der Bool (1) SandEin sind wir also wieder bei einem vielfachen von 8.
Jetzt mal der alte Fall: 4 x Bool (1) (incl. Sand) = vielfaches von 4, danach ein Word (2), das Word liegt schon direkt richtig, danach noch mal 2 Bool. Es ist also keine Leerstelle nötig.
Dann mal der neue Fall aus dem anderen Thema: TTraktsionssperre ist ein int64 = (8). Diese wird Alignt, also eine Leerstelle von 4, dann die 8 bit von der Traktionssperre. Dann 2 Byte, klar. Dann kommen wir an die Stelle, wo ich erst nachschauen musste, ob TKombischalterFkt auf 8 oder auf 4 Alignt wird, und ob dies unter 32 und 64 bit gleich ist. Offenbar auf 4, da es nur gemäß dem größten Member von TKombischalterFkt alignt wird, also nur noch mal 2 Lücke (andernfalls wäre es 6 gewesen). Insgesamt verursacht diese Änderung also noch mal 6 zusätzliche Bytes an Lücke.
(Danach muss man noch mal aufpassen, ob irgendwo im weiteren Verlauf noch mal ein Double, ein Int64 oder ein Pointer auftaucht, weil wir dann noch mal um 4 (bzw. genauer gesagt 12 Modulo acht) gegenüber dem bisherigen verschoben sind, dies ist aber nicht der Fall.)
Dann mal der Gegenvorschlag aus dem Thema: Das Word wird gegen die zwei neuen Bool(1) und Byte(1)-Member ersetzt. 2 Bytes, die nicht alignt werden mussten, werden also durch 2 andere Bytes ersetzt. Es gibt also gegenüber der Ursprungsvariante keine Änderung. Alte DLLs laufen also unverändert weiter, sofern die DLL nicht auf den Member Traktionssperre zugegriffen hat.
Im weiteren Verlauf der Record gibt es ansonsten wenig interessantes zu sehen. Auf 2 Stellen, wo der Kompiler Platz lässt, möchte ich aber noch hinweisen: Hinter Automatischfahren (1), da seit der letzten Single erst 7 Bool vorbei sind, und nach SchleuderschutzElektronischWirksam (1), da seit der letzten Single nur ein Word(2) und ein Bool(1) = (3) kamen. In diesen Lücken könnt man theoretisch bei Bedarf ohne große Probleme neue, 1 Byte große Felder einfügen.
Auch in TKombischalterFkt haben bei Bedarf noch 3 weitere Bytes Platz.

In der Praxis ist das alles relativ kompliziert. Es ergeben sich daher sinnvollerweise die Regeln, die ich unter 3. geschrieben habe: Wenn man Felder löscht, sie nicht wirklich löschen, sondern in "Reserviert 1, 2, 3, ..." umbenennen. Wenn man bei einem Feld den Datentyp ändert, und es dadurch eine andere Größe hat, das Feld quasi Löschen (s. dort) und unten in der Struktur wieder neu anfügen. Felder nur unten hinzufügen, es sei denn, man will ein altes Reserviert-Feld wieder verwenden. Records, die in der Record auftauchen am besten nicht ändern (wie gesagt, bei TKombischalterFkt würde es theoretisch gehen, aber das wäre schon wieder eine erweiterte Technik.)
Diese Signatur möchte folgendes bekannter machen: ZusiWiki · ZusiSK: Streckenprojekte · YouTube: Objektbau für Zusi · euirc: Zusi-Chat

Antworten