Abwärtskompatibilität von DLL- und COM-Schnittstellen — Eine Entscheidungstabelle dafür, welche Änderungen Aufrufer brechen
· Go Komura · COM, DLL, .NET, C#, C++, Abwärtskompatibilität, Versionierung, Legacy-Technologie, Nutzung vorhandener Ressourcen, Entscheidungstabelle
„Können wir für diesen Fix einfach die neue DLL austauschen, oder brauchen die Aufrufer auch einen Neubau?“ — wer eine gemeinsam genutzte DLL oder COM-Komponente pflegt, auf die mehrere Anwendungen verweisen, landet bei jedem Release bei genau dieser Frage. Beantworten Sie sie falsch, kann eine beim Kunden noch laufende alte EXE komplett den Start verweigern — oder schlimmer, sie startet weiterhin klaglos, während sich ihre Berechnungsergebnisse still und leise darunter ändern.
Was das Ganze knifflig macht: Die Entscheidung wird meist aus einem vagen „das fühlt sich riskant an“-Bauchgefühl heraus getroffen. In Wirklichkeit lässt sich fast mechanisch bestimmen, welche Änderungen die Kompatibilität brechen. Native DLLs haben klar definierte Regeln rund um Exporte und Aufrufkonventionen; COM hat die explizite, schriftlich niedergelegte eiserne Regel, dass Schnittstellen unveränderlich sind1; und .NET verfügt über eine veröffentlichte Liste von Kompatibilitätsänderungsregeln, die Microsoft selbst bei der Entwicklung der .NET-Bibliotheken verwendet2.
Auf diesem Blog haben wir die Grundlagen von COM bereits in Was ist COM / ActiveX / OCX? und dessen Designphilosophie in Was ist COM — Warum das Design von Windows-COM auch heute noch trägt behandelt. Dieser Artikel ordnet als Entscheidungstabelle, welche Änderungen für DLLs, COM und .NET-Assemblies jeweils Aufrufer brechen, und behandelt das Verfahren, wenn ein Kompatibilitätsbruch unvermeidbar wird.
1. Das Ergebnis zuerst
- Kompatibilität hat drei Ebenen: binäre Kompatibilität (funktioniert ohne Neubau), Quellcode-Kompatibilität (funktioniert bei Neubau) und Verhaltenskompatibilität (das Verhalten ändert sich nicht). „Kein Neubau nötig“ ist nicht gleichbedeutend mit „sicher“ — Sie müssen auch die Verhaltenskompatibilität beurteilen.3
- Für native DLLs gilt als Grundregel: Einen Export hinzuzufügen ist sicher; einen bestehenden Export zu ändern oder zu entfernen ist ein Bruch. Funktionssignaturen, Aufrufkonventionen und Struct-Layout sind der binäre Vertrag selbst.
- Eine COM-Schnittstelle ist nach Veröffentlichung unveränderlich. Methoden nach der Veröffentlichung hinzuzufügen, zu entfernen oder umzuordnen verstößt gegen die Spezifikation; Änderungen müssen als neue Schnittstelle mit neuer IID hinzugefügt werden (IFoo → IFoo2).41
- VB6-/VBA-Clients nutzen frühe Bindung, die Vtable-Slot-Positionen fest einprogrammiert und sie damit zu den Aufrufern macht, die am ehesten durch jede Änderung am Layout einer Schnittstelle brechen.
- Für .NET wird veröffentlicht, was als Breaking Change an einer öffentlichen API gilt — Microsofts Kompatibilitätsänderungsregeln —, und die Liste klassifiziert nicht nur das Entfernen von Methoden oder Signaturänderungen, sondern auch das Hinzufügen von virtual zu einem Member und sogar das Umbenennen eines Parameters als Breaking Change.2
- Semantische Versionierung ist die Konvention, bei einem Breaking Change die Hauptversion zu erhöhen, funktioniert aber erst, wenn Sie eine Definition dessen festlegen, was als Breaking Change gilt.5 Sie können die Entscheidungstabelle dieses Artikels als diese Definition verwenden.
- Ist ein Kompatibilitätsbruch unvermeidbar, gehen Sie in dieser Reihenfolge vor: Alt und Neu parallel anbieten → eine Übergangsfrist → eine Bestandsaufnahme der Aufrufer → Abschaltung. Das Prinzip lautet, niemals alles auf einmal auszutauschen.
2. Die drei Ebenen der Kompatibilität — wer wann bricht
Was salopp als „Abwärtskompatibilität“ in einen Topf geworfen wird, gliedert sich tatsächlich in drei Ebenen. Selbst die offizielle Dokumentation von .NET klassifiziert Breaking Changes entlang der Linien Quellcode-, binärer und Verhaltenskompatibilität.3
| Ebene | Bedeutung | Was beim Bruch passiert | Wer hauptsächlich leidet |
|---|---|---|---|
| Binäre Kompatibilität | Aufrufer funktionieren mit der neuen DLL ohne Neubau | Einstiegspunkt beim Start nicht gefunden, eine MissingMethodException zur Laufzeit, Abstürze |
Beim Kunden noch laufende alte EXEs, Drittanbieter-Apps, die Sie nicht neu bauen können |
| Quellcode-Kompatibilität | Aufrufer funktionieren, wenn Sie sie neu bauen | Kompilierfehler beim nächsten Build | Ein anderes internes Team, Entwickler, die die Quelle besitzen |
| Verhaltenskompatibilität | Das spezifizierte Verhalten ändert sich nicht | Ergebnisse, Timing oder die Art der geworfenen Ausnahme ändern sich ganz ohne Fehler | Endanwender (und alle, die den entstehenden Vorfall am Ende untersuchen müssen) |
Der wichtige Punkt zu diesen drei Ebenen ist, dass eine äußere Ebene brechen kann, selbst wenn eine innere Ebene intakt bleibt. Ein Fix, der beispielsweise die Bedeutung des Rückgabewerts einer bestehenden Funktion ändert, bewahrt sowohl die binäre als auch die Quellcode-Kompatibilität, während er nur die Verhaltenskompatibilität bricht. Weil diese Art von Änderung weder einen Linkfehler noch einen Kompilierfehler erzeugt, ist sie die Zeile in der Entscheidungstabelle, die am leichtesten übersehen wird.
Umgekehrt müssen Sie in einer Umgebung, in der jeder Aufrufer die Quelle besitzt und gleichzeitig neu gebaut werden kann (etwa ein internes System in einem einzigen Repository), nur Quellcode- und Verhaltenskompatibilität bewahren — binäre Kompatibilität kann aus den Anforderungen gestrichen werden. Ob irgendein Aufrufer Ihrer DLL ein Binary ist, das Sie nicht neu bauen können, ist die erste Weggabelung beim Lesen der Entscheidungstabelle.
3. Die Kompatibilitäts-Entscheidungstabelle für native DLLs (C/C++)
Die Kompatibilität einer nativen DLL wird durch ihre Export-Tabelle, Aufrufkonventionen und ihr Speicherlayout bestimmt. Wie eine DLL überhaupt gefunden und geladen wird, behandelt Wie die Namensauflösung von Windows-DLLs funktioniert; sobald das Laden gelingt, ist die folgende Tabelle der Maßstab zur Beurteilung der Kompatibilität.
| Änderung | Binäre Kompatibilität | Anmerkungen |
|---|---|---|
| Eine exportierte Funktion hinzufügen | Bricht nicht | Der sicherste Weg, eine API zu erweitern. Verlassen Sie sich jedoch auf implizite Ordinalzahlen aus einer .def-Datei, kann das Hinzufügen einer Funktion bestehende Ordinalzahlen je nach Position neu nummerieren; verlinkt ein Client per Ordinalzahl, fixieren Sie die bestehenden Ordinalzahlen explizit und hängen neue am Ende an |
| Eine exportierte Funktion entfernen oder umbenennen | Bricht | Die Import-Auflösung schlägt fehl, was einen Fehler beim Laden oder bei GetProcAddress erzeugt |
| Die Signatur einer bestehenden Funktion ändern (Parameter hinzufügen/entfernen/umtypisieren, den Rückgabetyp ändern) | Bricht | Stack- und Registerübergabekonventionen stimmen nicht mehr überein. Dasselbe gilt für Rückgabewerte — wechseln Sie von einer Ganzzahl (RAX) zu einem Gleitkommawert (XMM0), liest der Aufrufer unter dem alten ABI Datenmüll. Es kann ohne Fehler laufen und einfach entgleisen |
Die Aufrufkonvention ändern (__cdecl <-> __stdcall) |
Bricht (32-Bit) | Auf x86 tauscht die Verantwortung für das Aufräumen des Stacks, was zu Stack-Beschädigung führt. x64 hat eine einzige Aufrufkonvention und ignoriert diese Spezifizierer faktisch, sodass diese Zeile eine 32-Bit-DLL-Angelegenheit ist |
| Eine Export-Ordinalzahl ändern | Bricht, bedingt | Ein per Ordinalzahl verlinkender Aufrufer ruft am Ende eine andere Funktion auf. Keine Auswirkung, wenn jeder Aufrufer nur per Name verlinkt |
| Einem vom Aufrufer allozierten Struct ein Feld hinzufügen | Bricht | Ein alter Aufrufer alloziert und übergibt weiterhin die kleinere Version (dies kann mit der unten besprochenen cbSize-Konvention gemildert werden) |
Das Packing/Alignment eines öffentlichen Structs ändern (#pragma pack, /Zp, ein Toolchain-Wechsel) |
Bricht | Die Offsets bestehender Member und die Gesamtgröße ändern sich, selbst wenn Sie keinen einzigen Member anfassen. cbSize kann Sie nicht vor einem verschobenen Layout retten, fixieren Sie das Packing daher explizit im öffentlichen Header |
| Interne Änderungen an einem Struct, das nur die DLL alloziert und freigibt | Bricht nicht | Wenn das Design nach außen nur einen Zeiger (ein Handle) offenlegt, darf sich das Innenleben frei ändern |
| Die Bedeutung eines Rückgabewerts oder Fehlercodes ändern | Bricht nicht (aber die Verhaltenskompatibilität schon) | Das Linken gelingt weiterhin, während sich das Verhalten ändert — das Muster, das am langsamsten entdeckt wird |
| Einer direkt exportierten C++-Klasse ein Datenmember oder eine virtuelle Funktion hinzufügen | Bricht | Objektgröße oder Vtable-Layout ändern sich. Nur eine nicht-virtuelle Memberfunktion hinzuzufügen lässt das Layout unverändert und bricht bestehende Clients nicht direkt, aber das direkte Exportieren einer C++-Klasse hat von vornherein keine Compiler-übergreifende Kompatibilität, und diese Einzelfallentscheidung jedes Mal treffen zu müssen, ist selbst ein Zeichen für ein fragiles ABI |
Die Design-Leitlinie, zu der diese Tabelle führt, hat sich seit Jahrzehnten nicht geändert: Halten Sie die Grenze auf ein C-ABI beschränkt (extern "C"-Funktionen und einfache Structs) und erweitern Sie durch Hinzufügen von Funktionen. Dasselbe gilt, selbst wenn Sie eine native DLL aus C# bauen: Die in Wie man eine C#-Native-AOT-DLL von C/C++ aus aufruft behandelte Exportoberfläche wird genau nach dieser Tabelle verwaltet.
3.1 Die cbSize-Konvention — Win32s Trick, um Structs erweiterbar zu machen
Die klassische Gegenmaßnahme gegen „das Hinzufügen eines Struct-Members bricht Aufrufer“ ist die Win32-Konvention, ein Größenfeld an den Kopf des Structs zu setzen. Der Aufrufer füllt cbSize mit der ihm zur Kompilierzeit bekannten Struct-Größe und übergibt das; die DLL sieht sich diese Größe an, um zu bestimmen, welche Generation des Structs dieser konkrete Aufrufer versteht.
typedef struct KS_CONFIG {
DWORD cbSize; // Der Aufrufer setzt sizeof(KS_CONFIG)
DWORD dwMode;
DWORD dwTimeout;
// Neue Member immer am Ende hinzufügen
} KS_CONFIG;
// DLL-Seite: cbSize nutzen, um Generationen zu unterscheiden, und für alte Aufrufer auf einen Standardwert zurückfallen
if (pConfig->cbSize >= FIELD_OFFSET(KS_CONFIG, dwTimeout) + sizeof(DWORD)) {
timeout = pConfig->dwTimeout; // Neuer Aufrufer
} else {
timeout = DEFAULT_TIMEOUT; // Alter Aufrufer
}
Tatsächlich wird das NOTIFYICONDATA-Struct der Windows-API genau nach diesem Schema generationsweise versioniert, und es ist offiziell dokumentiert, dass das Setzen des richtigen Werts in cbSize die Kompatibilität mit älteren Versionen von Shell32.dll erhält.6 Setzen Sie cbSize von Anfang an, also ab der allerersten Version, in die öffentlichen Structs Ihrer eigenen DLL, wandern spätere Erweiterungen von „Breaking Change“ auf die sichere Seite der Entscheidungstabelle. Allerdings müssen neue Member immer am Ende stehen, und das Ändern von Typ oder Reihenfolge eines bestehenden Members bleibt weiterhin verboten. Noch etwas: Bei einem für Ausgaben genutzten Struct übernimmt die DLL-Seite zusätzliche Verantwortung — Schreibvorgänge und Initialisierung müssen sich immer innerhalb der vom Aufrufer tatsächlich übergebenen cbSize bewegen. Bedingungslos die vollständige neue sizeof zu schreiben, läuft über den kleineren Puffer hinaus, den ein alter Aufrufer alloziert hat, und die DLL verursacht am Ende genau die Art von Bruch, die diese Konvention verhindern sollte.
4. Die eiserne Regel der COM-Schnittstellen — keine Änderungen nach Veröffentlichung
COM ist die Technologie, die auf dieses Problem die klarste Antwort gab. Gemäß der COM-Spezifikation folgt eine Schnittstelle diesen Regeln:
- Eine Schnittstelle hat eine eindeutige IID (Interface-ID).1
- Eine Schnittstelle ist unveränderlich. Einmal erstellt und veröffentlicht, darf kein Teil ihrer Definition jemals geändert werden.1
- Eine Methode hinzuzufügen oder zu entfernen oder ihre Semantik zu ändern bedeutet nicht, „eine neue Version der alten Schnittstelle“ zu erstellen — es bedeutet, eine neue Schnittstelle mit einer anderen IID zu erstellen.4
Der Grund für eine solche Strenge ist, dass die eigentliche Substanz einer COM-Schnittstelle ein binäres Layout ist — die Vtable, eine Tabelle von Funktionszeigern. Ein C++- oder VB6-Client programmiert zur Kompilierzeit fest ein, dass „Slot 3 GetName ist“ — eine Position. Fügen Sie nach der Veröffentlichung eine Methode ein, ruft der alte Client, völlig ohne Fehler, eine andere Methode auf. Genau deshalb hat COM die Operation des „Änderns“ einer Schnittstelle selbst aus der Spezifikation gestrichen und stattdessen folgendes Erweiterungsverfahren bereitgestellt.
// v1: bereits veröffentlicht. Nie wieder ändern
[object, uuid(1111....)]
interface ICalc : IUnknown {
HRESULT Add([in] long a, [in] long b, [out, retval] long* result);
};
// v2: eine neue Schnittstelle mit neuer IID. Erweitert durch Vererbung von ICalc
[object, uuid(2222....)]
interface ICalc2 : ICalc {
HRESULT AddChecked([in] long a, [in] long b, [out, retval] long* result);
};
Die implementierende Klasse (die Coclass) implementiert sowohl ICalc als auch ICalc2; alte Clients nutzen ICalc genau wie zuvor weiter, und neue Clients fragen ICalc2 per QueryInterface an. Die offizielle Versionierungstheorie für RPC/COM legt es genauso dar: eine neue Schnittstelle, die von der alten erbt, entspricht einer Nebenversionserhöhung, während das Ändern einer bestehenden Methode oder eines Typs eine vollständig neue, nicht erbende Schnittstelle erfordert — das Äquivalent einer Hauptversionserhöhung.7 Was dieses Schema funktionieren lässt, ist, dass QueryInterface einem Aufrufer erlaubt, zur Laufzeit sicher zu prüfen, welche Schnittstellen unterstützt werden. Die Eleganz des Designs hinter diesem Mechanismus haben wir in Was ist COM genauer betrachtet.
4.1 Verantwortungsteilung zwischen CLSID, ProgID und IID
Beim Nachdenken über COM-Versionierung hilft es, die Rollen dreier Arten von Bezeichnern zu trennen.8
- IID identifiziert eine Schnittstelle (einen Vertrag). Jede Änderung am Vertrag bedeutet immer eine neue IID.
- CLSID identifiziert die implementierende Klasse. Sie können die Implementierung bei gleichbleibender CLSID frei austauschen, solange Sie den Vertrag jeder veröffentlichten Schnittstelle einhalten.
- ProgID ist ein menschenlesbarer Alias (
KomuraSoft.Calc.1), der zum Nachschlagen der zugehörigen CLSID in der Registrierung verwendet wird. Es ist üblich, sowohl eine versionierte ProgID als auch eine versionsunabhängige ProgID (KomuraSoft.Calc) zu führen, die immer auf die neueste Version zeigt, wobei letztere perCurVerauf die aktuelle Version abgebildet wird.8
Mit anderen Worten: „Die Implementierung aktualisieren“ gehört zur Welt von CLSID und ProgID, während „den Vertrag ändern“ zur Welt der IID gehört — vermengen Sie die beiden nicht. Möchten Sie die Registrierung ganz vermeiden, behandelt diese Option Was ist Reg-Free-COM?
4.2 Warum VB6-/VBA-Clients besonders zerbrechlich sind
Wenn VB6 oder VBA eine COM-Komponente über eine Projektreferenz nutzt (frühe Bindung), liest es die Typbibliothek zur Kompilierzeit, um Aufrufe aufzulösen. Frühe Bindung ist die empfohlene Form — Sie erhalten IntelliSense, Typprüfung und schnellere Ausführung9 —, doch der Preis ist, dass sie sich eng an das Layout der Typbibliothek bindet. Nicht nur eine Änderung an der Vtable einer Schnittstelle verursacht Ärger; sogar eine rein an den Definitionen der Typbibliothek vorgenommene Änderung kann als „die Referenz war beim Öffnen des Projekts defekt“ oder als Laufzeitfehler 430/438 auftauchen.
Das bedeutet, dass Komponenten mit VB6, VBA oder Excel-Makros als Aufrufer die Regel der Schnittstellen-Unveränderlichkeit am allerstrengsten durchsetzen müssen. Auch Typbibliotheken tragen eine Version (Major.Minor), die Sie bei jeder Vertragserweiterung erhöhen. Wie Sie beim Veröffentlichen typisierten .NET-Codes für VBA eine Typbibliothek generieren, behandelt Eine typisierte .NET-8-DLL von VBA aus aufrufen — dscom und TLB. Spät gebundene Clients, die nur CreateObject nutzen, lösen dagegen namentlich auf und sind damit widerstandsfähig gegen Layoutänderungen — sind aber ebenso einer Änderung an der Bedeutung einer Methode (Verhaltenskompatibilität) ausgesetzt.
5. .NET-Assembly-Kompatibilität — mechanisch mit den offiziellen Regeln beurteilen
.NET verfügt über einen veröffentlichten Satz von „Änderungsregeln für Kompatibilität“, den Microsoft selbst bei der Entwicklung der .NET-Bibliotheken verwendet und der jede Änderungsart als erlaubt (✔️), verboten (❌) oder beurteilungsbedürftig (❓) klassifiziert.2 Die Dokumentation stellt ausdrücklich fest, dass Sie sie unverändert als Kriterien für Ihre eigenen Bibliotheken übernehmen können, daher hier die wichtigsten Zeilen.
| Änderung an einer öffentlichen API | Urteil | Anmerkungen |
|---|---|---|
| Eine Methode, einen Typ oder ein Member hinzufügen | ✔️ Grundsätzlich sicher | Achten Sie jedoch auf eine Ergänzung, die die bestehende Überladungsauflösung ändert. Einem öffentlichen Struct ein Instanzfeld hinzuzufügen ist eine Ausnahme: es ändert Größe und Layout und bricht Interop und Unsafe-Konsumenten |
| Einen öffentlichen Typ oder ein Member entfernen oder umbenennen | ❌ Bruch | Bricht zur Laufzeit mit einer MissingMethodException oder Ähnlichem |
| Eine Signatur ändern (Parameter hinzufügen/entfernen/umordnen/umtypisieren, oder den Rückgabetyp) | ❌ Bruch | Bricht sowohl binäre als auch Quellcode-Kompatibilität |
| Einen Parameter umbenennen | ❌ Bruch | Bricht benannte C#-Argumente und späte VB-Bindung. Leicht zu übersehen |
| virtual zu einem Member hinzufügen | ❌ Bruch | Eine klassische Falle, die sicher aussieht, weil es „nur eine Ergänzung“ ist. Ein Mismatch zwischen den IL-Instruktionen call/callvirt kann die Folge sein |
| virtual entfernen, oder einen virtuellen Member abstrakt machen | ❌ Bruch | Bricht Overrides in abgeleiteten Klassen |
| Einem nicht versiegelten öffentlichen Typ ein abstraktes Member hinzufügen | ❌ Bruch | Bestehende abgeleitete Klassen haben dafür keine Implementierung |
| Einen Typ versiegeln (sealed) | ❌ Bruch | Bestehende abgeleitete Klassen kompilieren nicht mehr |
| Einer Schnittstelle ein Member hinzufügen | ❓ Beurteilungsbedürftig | Kann mit einer Default-Interface-Implementierung (DIM) gemildert werden, die aber Sprach-/Laufzeitbedingungen mit sich bringt |
| Eine Konstante, oder einen Enum-Wert ändern, oder ein Enum-Member umbenennen/entfernen | ❌ Bruch | Der Wert wird beim Aufrufer zur Kompilierzeit fest einprogrammiert |
| Code so ändern, dass eine spezifischere Ausnahme geworfen wird | ✔️ Erlaubt | Bestehende catch-Blöcke funktionieren weiter |
| Auf einem bestehenden Codepfad eine neue Art von Ausnahme werfen | ❌ Bruch | Sie nur für einen neuen Parameterwert zu werfen, ist in Ordnung |
Es ist nicht so einfach wie COMs „Schnittstellen sind unveränderlich“, aber die zugrunde liegende Philosophie ist dieselbe: Eine öffentliche API ist ein Vertrag; Sie können den Vertrag erweitern, aber den bestehenden Vertrag nicht ändern. Und dass Änderungen wie das Hinzufügen einer virtuellen Methode oder das Umbenennen eines Parameters — Dinge, die auf den ersten Blick sicher aussehen — als Breaking Change klassifiziert werden, ist genau der Grund, warum Sie nach der Tabelle und nicht nach Gefühl urteilen sollten.
5.1 Starke Namen und drei Versionsnummern
.NET-Assemblies tragen mehrere Versionsnummern, jede mit einer anderen Rolle.10
- AssemblyVersion: die einzige Version, die die Laufzeitumgebung zur Identifizierung und zum Laden einer Assembly verwendet. Bei stark benannten Assemblies verlangt die CLR des .NET Framework eine exakte Übereinstimmung, sodass jede Erhöhung Aufrufer zwingt, eine Binding-Umleitung hinzuzufügen (.NET / .NET Core akzeptiert dagegen automatisch eine höhere Version). Um Umleitungen zu reduzieren, empfiehlt die offizielle Anleitung, nur die Hauptversion in AssemblyVersion widerzuspiegeln.
- FileVersion (AssemblyFileVersion): nur in den Dateieigenschaften des Explorers sichtbar, ohne Auswirkung auf das Laufzeitverhalten. Der empfohlene Ort, um eine CI-Build-Nummer einzustempeln.
- InformationalVersion: eine freiformatige, menschenlesbare Zeichenkette. Wird verwendet, um eine im Semver-Format formatierte Paketversion oder den Quell-Commit-Hash festzuhalten.
In der Praxis ist damit der funktionierende dreistufige Aufbau: Kompatibilität über die Paket-/Produktversion deklarieren (Semver), nur die Hauptversion in AssemblyVersion widerspiegeln und Builds mit FileVersion nachverfolgen.
6. Wie Sie Versionsnummern vergeben — Semver funktioniert erst mit einer „Definition“
Semantische Versionierung (Semver) passt in drei Zeilen: MAJOR bei einer inkompatiblen Änderung erhöhen, MINOR bei einer abwärtskompatiblen Funktionsergänzung, und PATCH bei einem abwärtskompatiblen Bugfix.5
Was sich leicht übersehen lässt: Die allererste Anforderung der Semver-Spezifikation ist, dass Software, die Semver verwendet, eine öffentliche API deklarieren muss.5 Ohne zu deklarieren, was als öffentliche API gilt, gibt es kein Kriterium dafür, was eine „inkompatible Änderung“ überhaupt ist, und ob die Hauptversion erhöht wird, hängt von der Tagesform der jeweiligen Person ab. An den meisten Stellen, an denen Semver in der Praxis nicht funktioniert, liegt das Problem nicht daran, wie die Nummern vergeben werden — sondern daran, dass diese Deklaration übersprungen wurde.
Ein realistisches Betriebsmodell für eine intern verteilte DLL sieht so aus.
- Den Umfang der öffentlichen API deklarieren — bei einer nativen DLL sind das die exportierten Funktionen und öffentlichen Header, bei COM das IDL/die Typbibliothek, bei .NET öffentliche Typen und Member. Halten Sie ausdrücklich fest, dass „alles außerhalb davon ein internes Implementierungsdetail ist, das sich ohne Vorwarnung ändern kann“.
- Eine Definition eines Breaking Change übernehmen — bringen Sie die Entscheidungstabellen dieses Artikels aus den Kapiteln 3 und 5, zusammen mit .NETs Änderungsregeln2, als „unsere eigene Definition“ in Ihr Repository ein.
- Die Prüfung automatisieren — für .NET können Werkzeuge wie Package Validation und ApiCompat die binäre Kompatibilität gegenüber der vorherigen Version mechanisch verifizieren.11 Das eliminiert die „vermutlich passt schon“-Beurteilung während des Reviews.
- Ein Kompatibilitätsfeld zu den Release-Notes hinzufügen — legen Sie jedes Mal ausdrücklich einen von drei Werten fest: kein Neubau nötig / Neubau empfohlen / Breaking Change enthalten. Das ist ein Mechanismus, um die Antwort auf „können wir die neue DLL einfach austauschen?“ schriftlich festzuhalten, bevor überhaupt jemand fragen muss.
7. Das Verfahren, wenn ein Kompatibilitätsbruch unvermeidbar ist
Erweist sich eine Änderung, die die Entscheidungstabelle als Breaking Change kennzeichnet, als unvermeidbar, gehen Sie so vor, dass Sie beide Versionen parallel anbieten, statt eine gegen die andere auszutauschen.
- Alt und Neu parallel anbieten — bei COM heißt das,
IFoo2hinzuzufügen, währendIFoobestehen bleibt (Kapitel 4). Bei einer nativen DLL fügen Sie eine neue Funktion hinzu (FooEx) oder lassen eine neu benannte DLL neben der alten koexistieren. Bei .NET liefern Sie die Änderung als neues Paket mit erhöhter Hauptversion aus und halten die alte Hauptversion nur für Bugfixes am Leben. - Eine Übergangsfrist festlegen — in .NET kann das Attribut
[Obsolete]eine Compilezeit-Warnung auslösen. Für nativen/COM-Code deklarieren Sie es über Header-Kommentare und Release-Notes, und legen Sie das geplante Entfernungsdatum ausdrücklich fest. Der Schlüssel ist, sich auf ein tatsächliches Datum festzulegen, nicht auf ein vages „wir entfernen es irgendwann“. - Bestandsaufnahme der Aufrufer machen — bauen Sie eine Liste, wer noch die alte API aufruft, indem Sie internen Quellcode durchsuchen, Installer-Verteilungsdatensätze prüfen und, bei COM, die Nutzung von Registrierungsreferenzen prüfen. Finden Sie ein Binary, das Sie nicht neu bauen können — ein Tool, das jemand gebaut hat, der inzwischen ausgeschieden ist, oder eine Drittanbieter-App —, verlängern Sie entweder die Lebensdauer der alten API speziell für diesen Fall, oder überbrücken Sie mit einem Wrapper.
- Die alte API entfernen — erst nachdem die Bestandsaufnahme bestätigt hat, dass keine Aufrufer mehr übrig sind, entfernen Sie sie und erhöhen die Hauptversion.
Dieses Verfahren ist teuer. Genau deshalb ist paradoxerweise eine kleine API von der allerersten Version an mit der Entscheidungstabelle im Hinterkopf zu entwerfen — da alles, was Sie nie veröffentlichen, überhaupt keine Kompatibilitätsverpflichtung mit sich bringt — die größte Kompatibilitätsabsicherung, die Sie haben.
8. Zusammenfassung
- Denken Sie über Kompatibilität in drei Ebenen: binär, Quellcode und Verhalten. Die Verhaltenskompatibilität kann selbst ohne Neubau brechen.3
- Für native DLLs gilt: Hinzufügen ist sicher; einen bestehenden Export, eine Signatur oder ein Struct-Layout zu ändern ist ein Bruch. Geben Sie Structs ein
cbSize-Feld, um Erweiterungsraum zu lassen.6 - Eine COM-Schnittstelle ist nach Veröffentlichung unveränderlich. Fügen Sie Änderungen als neue Schnittstelle mit neuer IID hinzu (IFoo2) und lassen Sie Aufrufer per
QueryInterfaceunterscheiden.147 Setzen Sie das besonders streng durch, wann immer früh gebundene VB6-/VBA-Clients beteiligt sind. - Für .NET können Sie mit den offiziellen Kompatibilitätsänderungsregeln mechanisch urteilen. Achten Sie auf „scheinbar sichere“ Änderungen — virtual hinzufügen, einen Parameter umbenennen, einen Typ versiegeln —, die als Breaking Change klassifiziert werden.2
- Ein realistischer dreistufiger Aufbau: AssemblyVersion trägt nur die Hauptversion, FileVersion verfolgt den Build, und Semver deklariert die Kompatibilität.10
- Semver funktioniert erst, wenn Sie eine öffentliche API und eine Definition eines Breaking Change deklarieren.5 Übernehmen Sie die Entscheidungstabelle als diese Definition und prüfen Sie sie automatisch mit Werkzeugen wie Package Validation.11
- Brechen Sie tatsächlich etwas, gehen Sie parallel anbieten → Übergangsfrist → Bestandsaufnahme → Entfernung. Niemals alles auf einmal auszutauschen, ist genau das, was die beim Kunden noch laufende alte EXE schützt.
Verwandte Artikel
- Was ist COM / ActiveX / OCX? — Unterschiede und Zusammenhänge gemeinsam erklärt
- Was ist COM — Warum das Design von Windows-COM auch heute noch trägt
- Eine typisierte .NET-8-DLL von VBA aus aufrufen — dscom und TLB
- Wie die Namensauflösung von Windows-DLLs funktioniert — Suchreihenfolge und SxS
- Was ist Reg-Free-COM? — COM ohne Registrierung nutzen
- Wie man eine C#-Native-AOT-DLL von C/C++ aus aufruft
Verwandte Beratungsbereiche
Die KomuraSoft LLC (合同会社小村ソフト) übernimmt das Kompatibilitätsdesign für DLLs, COM-Komponenten und .NET-Bibliotheken, auf die andere Systeme verweisen, die Bestandsaufnahme öffentlicher APIs und die Einrichtung einer Versionierungsrichtlinie sowie das Design und die Implementierung von Erweiterungen — das IFoo2-Muster, paralleles Anbieten —, die bestehende Clients nicht brechen.
- Nutzung vorhandener Ressourcen & Migrationsunterstützung
- Änderung & Wartung bestehender Windows-Software
- Technische Beratung & Design-Review
- Kontakt
Referenzlinks
-
Microsoft Learn, Interface Design Rules. Dazu, dass Schnittstellen von COM-Objekten eine eindeutige IID benötigen und dass kein Teil der Definition einer Schnittstelle geändert werden darf, sobald sie erstellt und veröffentlicht wurde (Unveränderlichkeit). ↩ ↩2 ↩3 ↩4 ↩5
-
Microsoft Learn, Change rules for compatibility (.NET). Dazu, dass .NET-API-Änderungen als erlaubt, verboten oder beurteilungsbedürftig klassifiziert werden; dass das Entfernen oder Umbenennen öffentlicher Typen/Member, Signaturänderungen, Parameterumbenennungen, das Hinzufügen/Entfernen von virtual, Versiegelung und Änderungen an Konstanten-/Enum-Werten allesamt als verboten (Breaking Change) klassifiziert werden; dass das Hinzufügen eines Members zu einer Schnittstelle beurteilungsbedürftig ist; und dass Bibliotheksautoren die Regeln als Bewertungskriterium für ihre eigene Bibliothek verwenden können. ↩ ↩2 ↩3 ↩4 ↩5
-
Microsoft Learn, Breaking changes (.NET library guidance). Dazu, dass Breaking Changes als quellcode-brechend, verhaltens-brechend oder binär-brechend klassifiziert werden, und dass binär-brechende Änderungen dazu führen, dass eine gegen die alte Version kompilierte Assembly zur Laufzeit mit einer MissingMethodException oder Ähnlichem fehlschlägt. ↩ ↩2 ↩3
-
Microsoft Learn, Interface Pointers and Interfaces. Dazu, dass COM-Schnittstellen unveränderlich sind und das Hinzufügen oder Entfernen einer Methode oder das Ändern ihrer Semantik bedeutet, eine neue Schnittstelle statt einer neuen Version der alten zu erstellen, wobei die IID den Vertrag eindeutig definiert. ↩ ↩2 ↩3
-
semver.org, Semantic Versioning 2.0.0. Dazu, MAJOR bei inkompatiblen API-Änderungen zu erhöhen, MINOR bei abwärtskompatiblen Funktionsergänzungen und PATCH bei abwärtskompatiblen Bugfixes; dass Software, die Semver verwendet, eine öffentliche API deklarieren muss; und dass eine rückwärtsinkompatible Änderung der öffentlichen API immer eine MAJOR-Versionserhöhung erfordert. ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, NOTIFYICONDATAW structure (shellapi.h). Dazu, dass die Struct-Größe im Member cbSize gesetzt wird, dass das Struct über Generationen hinweg erweitert wurde, und dass das Setzen des passenden cbSize-Werts die Kompatibilität mit älteren Versionen von Shell32.dll erhält. ↩ ↩2
-
Microsoft Learn, The Versioning Theory for RPC and COM. Dazu, dass das Erstellen einer neuen Schnittstelle der beste Weg ist, Funktionalität in COM zu erweitern, dass eine neue Schnittstelle, die von der alten erbt, einer Nebenversionserhöhung entspricht, dass Änderungen an bestehenden Methoden oder Typen eine vollständig neue, nicht erbende Schnittstelle erfordern, und dass QueryInterface Aufrufern erlaubt zu prüfen, was unterstützt wird. ↩ ↩2
-
Microsoft Learn, COM Registry Keys. Dazu, dass die CLSID die GUID ist, die eine COM-Klasse identifiziert, dass die ProgID eine menschenlesbare, ohne Eindeutigkeitsgarantie auf eine CLSID abgebildete Zeichenkette ist, dass die versionsunabhängige ProgID per CurVer auf die neueste Klassenversion abgebildet wird, und dass der Interface-Schlüssel IIDs registriert. ↩ ↩2
-
Microsoft Learn, OLE programmatic identifiers, late binding, and early binding (Project). Dazu, dass frühe Bindung über eine Projektreferenz in VBA empfohlen wird, dass späte Bindung (CreateObject/ProgID) beim Schreiben von Code keine Member anzeigt und langsamer läuft, und dass frühe Bindung eine Referenz auf die Ziel-Objektbibliothek erfordert. ↩
-
Microsoft Learn, Versioning (.NET library guidance). Dazu, dass AssemblyVersion von der Laufzeitumgebung zum Laden verwendet wird und bei stark benannten Assemblies unter .NET Framework eine exakte Übereinstimmung erfordert, dass empfohlen wird, nur die Hauptversion in AssemblyVersion aufzunehmen, dass FileVersion für die Windows-Anzeige gedacht ist, ohne Auswirkung auf das Laufzeitverhalten, dass InformationalVersion zusätzliche Versionsinformationen festhält, und dass Semver 2.0.0 für NuGet-Paketversionen empfohlen wird. ↩ ↩2
-
Microsoft Learn, NuGet package compatibility rules. Dazu, dass binär-brechende Änderungen vermieden werden müssen, dass die Werkzeuge Package Validation und ApiCompat die Kompatibilität gegenüber einer Baseline-Version automatisch erkennen können, und dass AssemblyVersion zwischen Releases niemals sinken darf. ↩ ↩2
Verwandte Artikel
Aktuelle Artikel mit denselben Schlagwörtern führen zu verwandten Themen weiter.
Eine Legacy-Business-Anwendung ohne Tests sicher verändern ── Charakterisierungstests und Refactoring in der Praxis
Um eine Business-Anwendung ohne Tests sicher zu verändern, erklärt dieser Artikel anhand von C#-Beispielen das Vorgehen bei einem Charakt...
Wie lange laufen VB6-Anwendungen noch? — Support-Status der Laufzeitumgebung und ein praxisnaher Weg zur .NET-Migration
Wie lange laufen VB6-Anwendungen noch? Dieser Artikel ordnet die Asymmetrie zwischen der Support-Richtlinie für die VB6-Laufzeitumgebung ...
Windows-Prozesskommunikation richtig wählen ── Eine Entscheidungstabelle für Named Pipes / TCP / gRPC / Shared Memory / COM
Wie wählt man den richtigen Weg, damit Windows-Anwendungen miteinander sprechen? Dieser Artikel ordnet Named Pipes, lokales TCP, gRPC, Sh...
COM und .NET aus PowerShell aufrufen — Was Ihre Skripte erreichen können, erweitern
Ein praktischer Leitfaden zum Aufrufen von .NET-Klassen aus PowerShell, zum Einbetten von C#- und Win32-APIs mit Add-Type, zur Steuerung ...
Code-Design für Geschäftssysteme — Produkt- und Kundencodes festlegen, und Prüfziffern
Ein praxisnaher Leitfaden zur Festlegung des Codesystems eines Geschäftssystems, einschließlich Produkt- und Kundencodes. Behandelt eine ...
Verwandte Themen
Diese Seiten ordnen den Artikel in einen größeren Leistungs- und Entscheidungskontext ein.
Technische Windows-Themen
Portal zu Windows-Entwicklung, Fehleranalyse und der Nutzung bestehender Assets.
ActiveX-Migration
Entscheidungen zum Beibehalten, Kapseln oder Ersetzen von COM / ActiveX / OCX.
Leistungen zu diesem Thema
Dieser Artikel ist direkt mit den folgenden Leistungen verbunden.
Windows-App-Entwicklung
Geschäftsanwendungen, Geräteintegration und Kommunikationstools von den Anforderungen bis zur Umsetzung.
Nutzung und Migration bestehender Assets
Wiederverwendung und Migration von COM / ActiveX / OCX sowie 32-/64-Bit-Abhängigkeiten.
Häufige Fragen
Fragen, die in Beratungen zu diesem Artikelthema häufig gestellt werden.
- Wenn ich einer DLL nur eine Funktion hinzufüge, müssen Aufrufer dann neu gebaut werden?
- In der Regel nein — wird nur eine exportierte Funktion hinzugefügt, funktionieren bestehende Aufrufer unverändert weiter, da die Import-Auflösung weiterhin gelingt, solange Name, Signatur, Aufrufkonvention oder Export-Ordinalzahl einer bestehenden Funktion nicht geändert werden. Fügen Sie jedoch einem vom Aufrufer allozierten und übergebenen Struct ein Feld hinzu oder ändern Sie die Bedeutung des Rückgabewerts oder Fehlercodes einer bestehenden Funktion, kann die Verhaltenskompatibilität brechen, obwohl kein Neubau erzwungen wird. Die Grundregel lautet: Eine Funktion hinzuzufügen ist sicher, eine bestehende Signatur zu ändern ist ein Bruch.
- Warum ist es verboten, einer COM-Schnittstelle nach der Veröffentlichung eine Methode hinzuzufügen?
- Weil eine Schnittstelle gemäß der COM-Spezifikation nach ihrer Veröffentlichung unveränderlich ist. Die eigentliche Substanz einer Schnittstelle ist ein binärer Layout-Vertrag — die Vtable, eine geordnete Liste von Funktionszeigern —, und das Einfügen, Entfernen oder Umordnen von Methoden bedeutet, dass ein altes Binary an der zur Kompilierzeit fest einprogrammierten Slot-Position eine andere Methode aufruft. Ein Anhängen am Ende verschiebt zwar keine bestehenden Slots, führt aber eine neue Gefahr ein: Ein neuer Client kann eine alte Komponenteninstanz erhalten und annehmen, die angehängte Methode sei vorhanden, und dann einen Slot aufrufen, der dort in Wirklichkeit nicht existiert. Deshalb ist selbst ein Anhängen unter derselben IID nicht erlaubt. Wenn Sie Funktionalität hinzufügen möchten, fügen Sie eine neue Schnittstelle mit einer neuen IID hinzu (IFoo2) und lassen die bestehende IFoo unangetastet. Aufrufer können dann zur Laufzeit über QueryInterface sicher feststellen, ob sie mit der alten oder der neuen Schnittstelle sprechen.
- Wie sollte ich AssemblyVersion, FileVersion und InformationalVersion in .NET unterschiedlich verwenden?
- AssemblyVersion ist die einzige Version, die die Laufzeitumgebung zur Identifizierung und zum Laden einer Assembly verwendet; bei stark benannten Assemblies verlangt die CLR des .NET Framework eine exakte Übereinstimmung, sodass jede Erhöhung Aufrufer zwingt, eine Binding-Umleitung hinzuzufügen. Deshalb empfiehlt die offizielle Anleitung, in AssemblyVersion nur die Hauptversion widerzuspiegeln. FileVersion erscheint nur in den Dateieigenschaften des Explorers und hat keine Auswirkung auf das Laufzeitverhalten, was sie zu einem guten Ort macht, um eine CI-Build-Nummer einzustempeln. InformationalVersion ist eine freiformatige, menschenlesbare Zeichenkette, die verwendet wird, um eine im Semver-Format formatierte Version oder einen Commit-Hash festzuhalten.
- Löst die Einführung semantischer Versionierung (Semver) Kompatibilitätsprobleme von allein?
- Nein, Semver allein löst das nicht. Semver ist die Konvention, bei jeder rückwärtsinkompatiblen Änderung die Hauptversion zu erhöhen, setzt aber voraus, dass Sie bereits festgelegt haben, was als Ihre öffentliche API und was als Breaking Change gilt. Kleben Sie Versionsnummern auf Releases, ohne diese Definition, variiert die Beurteilung von Person zu Person, und das Schema funktioniert nicht mehr. Semver wird erst dann sinnvoll, wenn Sie so etwas wie die Entscheidungstabelle dieses Artikels (für native DLLs) oder Microsofts Kompatibilitätsänderungsregeln (für .NET) als Ihre eigene Definition eines Breaking Change übernehmen und diese Definition in Ihren Release-Prozess einbacken.
Autorenprofil
Profilseite des Artikelautors.
Go Komura
Geschäftsführer von KomuraSoft LLC
Spezialisiert auf Windows-Softwareentwicklung, technische Beratung und Fehleranalyse, insbesondere bei bestehenden Systemen und schwer reproduzierbaren Störungen.