Inhaltsverzeichnis
Architektur
Das Add-in-Modell trennt sauber zwischen dem, was ein Add-in beschreibt, und dem, was es zur Laufzeit tut. Diese Trennung ist kein Selbstzweck. Sie erlaubt dem Host, die Oberfläche vollständig aufzubauen, bevor auch nur eine einzige Add-in-DLL geladen wird. Gleichzeitig definiert sie klare Verantwortlichkeiten: Der Host besitzt die Shell, das Add-in liefert Inhalt.
Zurück zur Übersicht.
Die zwei Ebenen
Ebene 1: Add-in-Metadaten
Die erste Ebene beschreibt das Add-in statisch. Alles, was der Host wissen muss, steht in einer einzigen JSON-Datei, dem Manifest. Dieses Manifest enthält:
- Identität — Id, Name, Version und Publisher identifizieren das Add-in eindeutig. Der Host verwendet diese Angaben für die Versionsverwaltung, die Update-Prüfung und die Anzeige in der Erweiterungsliste.
- Kompatibilität — Das Feld
engineVersiongibt an, mit welcher Host-Version das Add-in zusammenarbeitet. Der Host prüft diesen Wert beim Laden und lehnt inkompatible Add-ins ab, bevor es zu Laufzeitfehlern kommen kann. - Signatur — Jedes ausgelieferte Paket enthält eine Prüfsumme der DLL (
dllChecksum). Der Host verifiziert diese Prüfsumme vor dem Laden. Damit wird sichergestellt, dass die DLL nicht nach dem Signieren verändert wurde. Details zur Signierung stehen im Kapitel Sicherheit. - Contributions — Das Manifest deklariert alle visuellen und funktionalen Beiträge: Commands, Menüpunkte, Toolbar-Buttons, Seitenleisten-Panels, Tastenkürzel und Themes. Der Host registriert diese Beiträge sofort beim Start, ohne die DLL zu laden. Die Oberfläche steht also vollständig, auch wenn das Add-in noch nicht aktiviert ist. Eine vollständige Beschreibung aller Contribution-Typen findet sich unter Contributions.
- Aktivierungsereignisse — Das Feld
activationEventslegt fest, wann der Host die DLL tatsächlich laden soll. Ein Add-in kann beim Start aktiviert werden (onStartup), beim ersten Aufruf eines bestimmten Commands (onCommand:mein.command) oder gar nicht, bis es explizit benötigt wird. Dieses Lazy-Loading hält den Anwendungsstart schnell.
Ebene 2: Add-in-Runtime
Die zweite Ebene ist der lebendige Code. Sobald der Host entscheidet, ein Add-in zu aktivieren, durchläuft es einen definierten Lebenszyklus:
- ABI-Handshake — Der Host ruft die exportierte Funktion
GetAbiInfoauf. Diese liefert eine Magic-Number, die ABI-Version und die FPC-Compiler-Version zurück. Stimmt einer dieser Werte nicht mit den Erwartungen des Hosts überein, wird die DLL sofort entladen. Dieses Vorgehen verhindert Abstürze durch inkompatible Binaries, etwa wenn ein Add-in mit einer anderen Compiler-Version gebaut wurde als der Host. - Plugin-Erzeugung — Nach dem erfolgreichen Handshake ruft der Host
CreatePlugin(IHost)auf. Diese Funktion ist der einzige Einstiegspunkt, den ein Add-in exportieren muss. Sie erhält einIHost-Objekt als Parameter, über das das Add-in auf alle Services des Hosts zugreifen kann: Commands, Menüs, Dokumente, Konfiguration, Events und mehr. Die vollständige Service-Referenz steht unter Services. - Aktivierung — Der Host ruft
IPlugin.Activate(IExtensionContext)auf. DerIExtensionContextist der Lebenszeit-Container des Add-ins. Alles, was das Add-in registriert — Command-Handler, Menüpunkte, Event-Abonnements — wird überSubscribe(IDisposable)an diesen Kontext gebunden. Wenn der Host das Add-in deaktiviert, werden alle Registrierungen in umgekehrter Reihenfolge (LIFO) aufgeräumt. Das Add-in muss sich nicht selbst um Aufräumarbeiten kümmern. - Servicezugriff — Über
IHoststehen dem Add-in über achtzehn Services zur Verfügung: vonICommandServiceüberIDocumentServicebis hin zuIServiceRegistryfür die Kommunikation zwischen Add-ins. Jeder Service gibt bei der Registrierung einIDisposablezurück, das an denIExtensionContextgebunden wird. - Frame-Factories — Wenn ein Add-in Dokumentansichten bereitstellt, registriert es eine
IWvdSDocumentFactorybeimIDocumentService. Der Host ruft diese Factory auf, wenn der Benutzer ein Dokument des entsprechenden Typs öffnet. Die Factory liefert einenTWvdSDocumentFramezurück, den der Host in einen Dokument-Tab einbettet. Details zur Frame-Entwicklung stehen unter Document-Frame.
Der Host-Vertrag
Der Host bleibt Eigentümer von allem, was zur Shell gehört. Diese Trennung ist bewusst gewählt, weil sie verhindert, dass ein fehlerhaftes oder bösartiges Add-in die Anwendung destabilisiert.
Was der Host kontrolliert
- Dokument-Tabs — Der Host erzeugt Tabs, setzt Titel und Icon, verwaltet den Dirty-State und steuert das Schließen. Wenn ein Benutzer einen Tab schließt, fragt der Host den zugehörigen Frame, ob ungespeicherte Änderungen vorliegen (
DoCanClose). Das Add-in entscheidet, ob gespeichert oder verworfen werden soll, aber der Host führt das Schließen durch. - Aktivierung — Welches Dokument gerade aktiv ist, bestimmt der Host. Beim Wechsel feuert er Kontextänderungen, die Menü-Sichtbarkeit und Toolbar-Zustand aktualisieren.
- Layout-Persistenz — Die Anordnung von Seitenleisten, Panels und Tabs wird vom Host gespeichert und wiederhergestellt.
- Menü- und Toolbar-Merge — Add-ins deklarieren Menüpunkte und Toolbar-Buttons im Manifest. Der Host fügt diese Beiträge in die bestehende Menüstruktur ein, sortiert sie nach Gruppen und Prioritäten und blendet sie kontextabhängig ein oder aus. Wie das im Detail funktioniert, beschreibt das Kapitel Menü-Merge.
- Kontextwechsel — Der Host verwaltet einen Satz von Kontext-Schlüsseln (Context Keys), die den aktuellen Zustand der Anwendung beschreiben. Menüpunkte, Toolbar-Buttons und Tastenkürzel können
when-Ausdrücke enthalten, die gegen diese Schlüssel ausgewertet werden. Ändert sich der Kontext, aktualisiert der Host die Sichtbarkeit aller betroffenen UI-Elemente automatisch.
Was das Add-in liefert
- Dokumentinhalt — Der
TWvdSDocumentFrameist das Herzstück eines dokumentorientierten Add-ins. Er füllt den Tab mit Inhalt: Formulare, Grids, Editoren oder beliebige andere LCL-Controls. Der Host kümmert sich um den Rahmen, das Add-in um den Inhalt. - Command-Handler — Commands sind die zentrale Aktionseinheit. Ein Add-in registriert Handler für seine deklarierten Commands. Der Host ruft diese Handler auf, wenn der Benutzer den Command über Menü, Toolbar, Tastenkürzel oder die CommandPalette auslöst.
- Daten für Seitenleisten — Sidebar-Panels werden als
TWvdSDocumentFrameregistriert. Das Add-in stellt den Frame-Inhalt bereit, der Host bettet ihn in die Seitenleiste ein. - Event-Handler — Über den
IEventBuskann ein Add-in auf Anwendungsereignisse reagieren, etwa auf den Start einer Debug-Sitzung oder auf Konfigurationsänderungen.
Was dieses Modell vermeidet
Einige Entwurfsentscheidungen sind bewusst anders als bei verbreiteten Plugin-Systemen. Diese Abgrenzungen sind wichtig, weil sie häufige Fehlerquellen eliminieren.
- Keine rohen Datentypen über die DLL-Grenze — Die Kommunikation läuft ausschließlich über COM-artige Interfaces. Strings, Listen und Records werden nicht direkt übergeben, weil verschiedene Compiler-Versionen unterschiedliche Speicherlayouts verwenden. Ein
TStringList, das mit FPC 3.2 erzeugt wurde, hat ein anderes internes Format als eines von FPC 3.4. Interfaces umgehen dieses Problem, weil sie auf einem stabilen vtable-Layout basieren. - Keine direkte Instanziierung fremder Klassen — Der Host erzeugt niemals direkt eine Klasse aus einem Add-in. Stattdessen ruft er die
IWvdSDocumentFactoryauf, die das Add-in selbst bereitstellt. Diese Indirektion stellt sicher, dass das Add-in die volle Kontrolle über die Erzeugung seiner Objekte behält. - Kein beliebiger Shell-Umbau — Ein Add-in kann keine Top-Level-Fenster erzeugen, keine Menüleiste austauschen und keine Toolbar-Struktur verändern. Es kann nur die dafür vorgesehenen Contribution-Punkte nutzen. Das hält die Anwendung konsistent, auch bei vielen installierten Add-ins.
- Kein Hot-Unload — Add-ins laufen im selben Prozess wie der Host (in-proc). Ein echtes Hot-Unload einer nativen DLL ist technisch nicht zuverlässig möglich, weil noch Referenzen auf Interfaces existieren können. Deshalb bietet WBASH kein Hot-Unload an. Stattdessen werden Änderungen nach einem Neustart der Anwendung wirksam.
- Keine falsche Sandbox — Da Add-ins in-proc laufen, gibt es keine echte Prozess-Isolation. WBASH verwendet stattdessen ein Capability-basiertes Berechtigungsmodell: Ein Add-in deklariert im Manifest, welche Rechte es benötigt (Dateizugriff, Netzwerk, Zwischenablage), und der Benutzer wird beim ersten Start um Zustimmung gebeten. Das ist ehrlich und transparent, statt eine Sicherheit vorzutäuschen, die technisch nicht gegeben ist. Details dazu stehen unter Sicherheit.
Shell vs. Add-in: Grenzziehung
Die Shell und die Add-ins haben klar getrennte Verantwortlichkeiten. Diese Grenzziehung ist kein Detail, sondern ein Architekturprinzip. Jede Verletzung führt zu Duplikation, erschwerter Wartung und falschen Abhängigkeiten.
Was die Shell bereitstellt
Die Shell liefert provider-agnostische Infrastruktur, die für jedes Add-in wiederverwendbar ist:
- Connection Manager — Zentrale Verwaltung benannter Datenverbindungen. Liest Konfiguration aus
user.config.json, erzeugt Provider-Instanzen über austauschbare Factories, stellt Borrowed References bereit. Details unter Connection Manager. - Provider-Factories — Die Shell registriert eingebaute Factories (z. B.
mssqlfür ODBC Driver 17/18). Add-ins können eigene Factories nachregistrieren (rest,sqlite, etc.). - Shell-UI-Container — Dokument-Tabs, Seitenleisten, ActivityBar, StatusBar, BottomPanel, QuickBar. Die Shell besitzt diese Container und bettet die Inhalte der Add-ins ein.
- Lifecycle-Management — Aktivierung, Deaktivierung, Kontext-Wechsel, Event-Routing.
- Builtin Commands — Shell-eigene Commands wie
wvds.settings.open,wvds.quit,wvds.about. Diese können an Add-in-Commands delegieren (z. B.wis.proofExec.open→ das WIS-Add-in übernimmt).
Was die Shell NICHT tut
- Keine SQL-Queries — Konkrete Abfragen (SELECT, TVF-Aufrufe, Stored Procedures) gehören zum Add-in. Die Shell kennt kein Schema, keine Tabellen, keine Geschäftslogik.
- Keine domänenspezifischen Views — Grids, Bäume, Filter-Panels, Detail-Ansichten: alles Add-in-Code. Die Shell stellt nur den Container (Tab, Sidebar-Slot) bereit.
- Keine Daten-Bindung — DataSets, DataSources, FieldDefs, Column-Definitionen: Add-in-Verantwortung. Die Shell hat keine Kenntnis der Datenstruktur.
- Keine domänenspezifische Event-Logik — Filter→Tree→Grid-Kaskaden, Selection→Detail-Wechsel, Bulk-Save-Refresh: alles interne Verdrahtung des Add-ins.
Warum diese Trennung?
1. Keine Duplikation. Wenn die Shell SQL-Queries oder View-Frames enthält, die das Add-in ebenfalls implementiert, existiert dieselbe Logik an zwei Stellen. Änderungen müssen synchron erfolgen — das passiert in der Praxis nicht.
2. Unabhängige Release-Zyklen. Add-ins können unabhängig vom Host aktualisiert werden. Wenn die Shell domänenspezifischen Code enthält, erzwingt jede Änderung am Datenmodell einen Shell-Release.
3. Testbarkeit. Die Shell lässt sich ohne Datenbank testen. Add-ins lassen sich mit Mock-Providern testen. Vermischte Verantwortlichkeiten machen beides unmöglich.
4. Wiederverwendbarkeit. Der Connection Manager, die Provider-Factories und die UI-Container sind für jedes Add-in nutzbar — nicht nur für ein bestimmtes Fachmodul.
Phasen der Verarbeitung
Der Host verarbeitet Add-ins in vier Phasen, die strikt nacheinander ablaufen:
- Scan — Der Host durchsucht zwei Verzeichnisse nach
plugin.json-Dateien:<exe>/plugins/(Machine-Scope, vom Administrator verwaltet) und~/.wvdsx/extensions/(User-Scope, Roaming-Profile-kompatibel). Jedes gefundene Manifest wird geparst und validiert. Fehlende Pflichtfelder führen dazu, dass das Add-in übersprungen wird. - Resolve — Der Host baut einen Abhängigkeitsgraphen auf. Add-ins können andere Add-ins als Abhängigkeit deklarieren oder Services konsumieren, die ein anderes Add-in bereitstellt. Der Graph wird topologisch sortiert (Kahn-Algorithmus), um die korrekte Aktivierungsreihenfolge zu bestimmen. Zirkuläre Abhängigkeiten werden erkannt und gemeldet.
- Contributions registrieren — Noch bevor eine einzige DLL geladen wird, registriert der Host alle statischen Contributions aus den Manifesten. Für jeden deklarierten Command wird ein Lazy-Stub angelegt — ein Platzhalter, der beim ersten Aufruf die Aktivierung des Add-ins auslöst. Menüpunkte, Toolbar-Buttons und Tastenkürzel werden ebenfalls sofort registriert. Die Oberfläche steht damit vollständig.
- Activate — Add-ins mit
onStartupwerden sofort aktiviert. Alle anderen warten, bis ihr Aktivierungsereignis eintritt, typischerweise der erste Aufruf eines ihrer Commands. Bei der Aktivierung werden die Lazy-Stubs entfernt und durch die echten Handler des Add-ins ersetzt.