Inhaltsverzeichnis

SDK-Übersicht

Das WvdS Add-in SDK definiert die programmatische Schnittstelle zwischen einem Add-in und dem Host. Es besteht aus einer Handvoll Interfaces, einer Basisklasse für Dokumentansichten und einem klar definierten Lebenszyklus. Ein Add-in-Entwickler muss genau zwei Funktionen exportieren und ein Interface implementieren — alles Weitere stellt der Host über IHost bereit.

Zurück zur Übersicht.

DLL-Exporte

Jede Add-in-DLL exportiert exakt zwei Funktionen. Mehr ist nicht nötig und mehr wird vom Host auch nicht erwartet.

GetAbiInfo

function GetAbiInfo: TPluginAbiInfo; stdcall;

Diese Funktion liefert Informationen über die binäre Kompatibilität der DLL. Der Host ruft sie als Erstes auf, noch bevor irgendein Objekt erzeugt wird.

type
  TPluginAbiInfo = record
    Magic: UInt32;       // Fester Wert: $57564453 ('WVDS')
    AbiVersion: UInt32;  // ABI-Versionsnummer, aktuell 1
    FpcVersion: UInt32;  // FPC-Compiler-Version, z.B. 30301
  end;

Der Host prüft alle drei Felder. Die Magic-Number stellt sicher, dass es sich überhaupt um eine WvdS-Add-in-DLL handelt und nicht um eine beliebige Bibliothek. Die ABI-Version ermöglicht dem Host, in Zukunft Änderungen am Interface-Layout vorzunehmen, ohne bestehende Add-ins zu brechen — der Host kann dann ältere ABI-Versionen weiterhin unterstützen oder gezielt ablehnen. Die FPC-Version verhindert Abstürze durch unterschiedliche vtable-Layouts oder String-Implementierungen zwischen Compiler-Versionen.

Stimmt einer der Werte nicht überein, wird die DLL sofort per FreeLibrary entladen und das Add-in als fehlerhaft markiert. Der Benutzer sieht eine entsprechende Meldung in der Erweiterungsliste.

CreatePlugin

function CreatePlugin(const AHost: IHost): IPlugin; stdcall;

Diese Funktion ist die Factory des Add-ins. Der Host ruft sie nach dem erfolgreichen ABI-Handshake auf und übergibt sein IHost-Objekt. Das Add-in erzeugt daraufhin sein Plugin-Objekt und gibt es als IPlugin zurück.

Typische Implementierung:

function CreatePlugin(const AHost: IHost): IPlugin; stdcall;
begin
  Result := TMyPlugin.Create(AHost);
end;

Das IHost-Objekt bleibt über die gesamte Lebensdauer des Add-ins gültig. Es wird vom Host verwaltet und darf nicht vom Add-in freigegeben werden.

IPlugin

type
  IPlugin = interface
    procedure Activate(const AContext: IExtensionContext);
    procedure Deactivate;
  end;

Das IPlugin-Interface ist absichtlich minimal gehalten. Es hat genau zwei Methoden:

IExtensionContext

type
  IExtensionContext = interface
    procedure Subscribe(const ADisposable: IDisposable);
    function GetGlobalStoragePath: string;
    function GetWorkspaceStoragePath: string;
    procedure SetWorkspaceState(const AKey, AValue: string);
    function GetWorkspaceState(const AKey, ADefault: string): string;
  end;

Der IExtensionContext ist der zentrale Lebenszeit-Container des Add-ins. Er erfüllt drei Aufgaben:

Registrierungen sammeln

Jeder Service des Hosts gibt bei der Registrierung ein IDisposable zurück. Dieses Disposable-Token muss an den IExtensionContext übergeben werden:

procedure TMyPlugin.Activate(const AContext: IExtensionContext);
var
  Disposable: IDisposable;
begin
  Disposable := FHost.Commands.RegisterCommand('mein.command', 'Mein Befehl', TMyHandler.Create);
  AContext.Subscribe(Disposable);
 
  Disposable := FHost.Menus.AddMenuItem('menuBar/tools', 'mein.command', '');
  AContext.Subscribe(Disposable);
end;

Wenn der Host das Add-in deaktiviert, ruft er DisposeAll auf dem Kontext auf. Alle gesammelten Disposables werden in umgekehrter Reihenfolge (LIFO) freigegeben. Das stellt sicher, dass Abhängigkeiten korrekt aufgelöst werden — ein Menüpunkt, der auf einen Command verweist, wird vor dem Command selbst entfernt.

Persistenter Speicher

Der Kontext stellt zwei Verzeichnispfade bereit:

Beide Verzeichnisse werden vom Host erstellt, wenn das Add-in zum ersten Mal darauf zugreift.

Workspace-State

Für einfache Schlüssel-Wert-Paare bietet der Kontext SetWorkspaceState und GetWorkspaceState. Diese Werte werden im Arbeitsspeicher gehalten und beim Herunterfahren persistiert. Sie eignen sich für leichtgewichtigen Zustand wie die zuletzt geöffnete Ansicht oder den Scroll-Position eines Panels.

IHost

IHost ist das Tor zu allen Services des Hosts. Das Add-in erhält es bei der Erzeugung über CreatePlugin und verwendet es während seiner gesamten Lebensdauer.

type
  IHost = interface
    property Commands: ICommandService;
    property Events: IEventBus;
    property Configuration: IConfigurationService;
    property Context: IContextService;
    property Menus: IMenuService;
    property Toolbar: IToolbarService;
    property StatusBar: IStatusBarService;
    property Notifications: INotificationService;
    property Documents: IDocumentService;
    property SideBar: ISidebarService;
    property TreeViews: ITreeViewService;
    property Storage: IStorageService;
    property Dialogs: IDialogService;
    property Keybindings: IKeybindingService;
    property Services: IServiceRegistry;
    property Progress: IProgressService;
    property Log: ILogService;
    property Theme: IThemeService;
    property Permissions: IPermissionService;
    { --- Data --- }
    property Connections: IWvdSConnectionManager;
  end;

Jeder Service ist über eine Property zugänglich. Das IHost-Objekt erzeugt diese Services nicht bei jedem Zugriff neu — sie sind Singletons, die der Host einmal erzeugt und über die gesamte Laufzeit bereitstellt.

Die vollständige Referenz aller Services steht unter Service-Referenz.

IDisposable

type
  IDisposable = interface
    procedure Dispose;
  end;

IDisposable ist das zentrale Muster für Ressourcenverwaltung im gesamten SDK. Jede Registrierung — ob Command, Menüpunkt, Event-Abonnement oder Service — gibt ein IDisposable zurück. Solange dieses Token existiert, ist die Registrierung aktiv. Sobald Dispose aufgerufen wird, wird die Registrierung entfernt.

Dieses Muster hat zwei Vorteile. Erstens muss sich das Add-in nicht merken, wie eine Registrierung rückgängig gemacht wird — es braucht nur das Token. Zweitens stellt der IExtensionContext sicher, dass beim Deaktivieren des Add-ins alle Tokens automatisch aufgeräumt werden. Das verhindert dangling References über die DLL-Grenze hinweg.

Lebenszyklus im Überblick

  1. Der Host ruft GetAbiInfo auf und prüft Magic, ABI-Version und FPC-Version.
  2. Der Host ruft CreatePlugin(IHost) auf. Das Add-in erzeugt sein Plugin-Objekt.
  3. Der Host ruft IPlugin.Activate(IExtensionContext) auf. Das Add-in registriert Command-Handler, Factories und Event-Abonnements.
  4. Das Add-in arbeitet: Benutzer lösen Commands aus, öffnen Dokumente, interagieren mit Panels.
  5. Beim Herunterfahren ruft der Host IPlugin.Deactivate auf, dann IExtensionContext.DisposeAll.
  6. Der Host ruft FreeLibrary auf. Die DLL wird entladen.

Weiter zur Service-Referenz, zum Document-Frame oder zum ScriptHost & Host-Contract.