Skip to content

Latest commit

 

History

History
1180 lines (904 loc) · 39.7 KB

File metadata and controls

1180 lines (904 loc) · 39.7 KB

API Reference -- FMXPluginFramework

Complete reference for all interfaces, records, types, classes, and helpers. For a quick overview see README.md. For practical examples see the Examples/ directories.


Table of Contents


Interfaces

All interfaces are defined in Plugin.Interfaces.pas. Every interface has a GUID for Supports() / GetService<T> queries.

IPlugin

Core lifecycle interface. Every plugin must implement this.

IPlugin = interface
  ['{8A1B2C3D-4E5F-4A6B-8C7D-9E0F1A2B3C4D}']
  function GetPluginID: string;        // Unique technical ID (e.g. 'mycompany.editor.3d')
  function GetPluginName: string;      // Display name (e.g. '3D Editor Pro')
  function GetPluginVersion: string;   // Semver (e.g. '1.0.0')
  function GetDescription: string;     // Short description
  function GetPluginIcon: string;      // SVG markup or '' for default icon
  procedure Initialize;
  procedure Finalize;
end;
Method Purpose
GetPluginID Unique technical identifier -- used by EventBroker topics, ShortcutRegistry, logging. Convention: matches name in plugins.json.
GetPluginName Human-readable display name for menus, status bar, settings UI
GetPluginVersion Version string for display and compatibility
GetDescription Short description for plugin lists and about dialogs
GetPluginIcon SVG markup string for plugin icon. Return '' to use DefaultPluginIcon (puzzle piece).
Initialize Called after all BPLs are loaded, before UI. Safe to query other plugins' services here.
Finalize Called during shutdown, before BPL unload

Convention: GetPluginID must be unique across all loaded plugins. Use reverse-domain notation (e.g. 'mycompany.editor.3d'). Duplicate IDs are not enforced by the framework but will cause unexpected behavior with EventBroker topics, ShortcutRegistry, and logging. GetDescription may be empty. GetPluginIcon may be empty -- the DefaultPluginIcon constant in Plugin.Interfaces.pas provides a fallback SVG.

Registration pattern:

initialization
  var Plugin := TMyPlugin.Create;
  TServiceRegistry.Instance.RegisterPlugin(Plugin);
  TServiceRegistry.Instance.RegisterPackageInfo(
    TPackageInfo.Create('MyPlugin', 'My Plugin', '1.0.0', 'Description'));

IFrameProvider

Plugin provides a TFrame for the host to display.

IFrameProvider = interface
  ['{1A2B3C4D-5E6F-4A7B-8C9D-0E1F2A3B4C5D}']
  function GetDisplayName: string;
  function GetFrame(AOwner: TComponent): TFrame;
end;
Method Purpose
GetDisplayName Text for View menu / tab
GetFrame Returns the plugin's main frame

Frame caching is mandatory. GetFrame will be called multiple times (menu clicks, navigation). Return the same instance to avoid "duplicate component name" errors:

private
  FFrame: TFrame;
public
  function GetFrame(AOwner: TComponent): TFrame;

function TMyPlugin.GetFrame(AOwner: TComponent): TFrame;
begin
  if FFrame = nil then
    FFrame := TMyFrame.Create(AOwner);
  Result := FFrame;
end;

IStartupView

Optional: Plugin declares itself as the startup view. If no plugin implements IStartupView, TPluginMainForm falls back to the first IFrameProvider (backwards-compatible).

IStartupView = interface
  ['{7F8A9B0C-1D2E-4F3A-8B5C-6D7E8F9A0B1C}']
  function GetStartupFrame(AOwner: TComponent): TFrame;
end;

Priority in TPluginMainForm.AfterPluginsLoaded:

  1. IStartupView -- if any plugin implements it, that frame is shown
  2. First IFrameProvider -- fallback if no IStartupView exists

Use cases:

  • Dashboard plugin that should always be the landing page
  • Login/Auth screen that must appear before any other plugin
  • Different startup views per user role (multiple BPLs, only one loaded)

Example:

TDashboardPlugin = class(TInterfacedObject, IPlugin, IFrameProvider, IStartupView)
  // IStartupView
  function GetStartupFrame(AOwner: TComponent): TFrame;
end;

function TDashboardPlugin.GetStartupFrame(AOwner: TComponent): TFrame;
begin
  Result := GetFrame(AOwner);  // same frame, or a dedicated startup frame
end;

IFrameLifecycle

Optional: Frame reacts to visibility changes (iOS-style Will/Did model). Implemented by the Frame class itself, not by the plugin. Called by TPluginMainForm.SwitchToFrame.

IFrameLifecycle = interface
  ['{9A0B1C2D-3E4F-4A5B-6C7D-8E9F0A1B2C3D}']
  procedure BeforeActivate;    // before Visible := True
  procedure AfterActivate;     // after Visible := True
  procedure BeforeDeactivate;  // before Visible := False
  procedure AfterDeactivate;   // after Visible := False
end;

Call sequence during frame switch:

1. OldFrame.BeforeDeactivate   (save state, pause render loop)
2. OldFrame.Visible := False
3. OldFrame.AfterDeactivate    (release GPU resources, stop timers)
4. NewFrame.BeforeActivate     (load data, prepare renderer)
5. NewFrame.Visible := True
6. NewFrame.AfterActivate      (set focus, start animations)

Example -- 3D editor frame:

TMy3DEditorFrame = class(TFrame, IFrameLifecycle)
private
  FRenderTimer: TTimer;
public
  procedure BeforeActivate;
  procedure AfterActivate;
  procedure BeforeDeactivate;
  procedure AfterDeactivate;
end;

procedure TMy3DEditorFrame.BeforeActivate;
begin
  // Prepare scene data before frame becomes visible
end;

procedure TMy3DEditorFrame.AfterActivate;
begin
  FRenderTimer.Enabled := True;  // start render loop (needs visible context)
end;

procedure TMy3DEditorFrame.BeforeDeactivate;
begin
  FRenderTimer.Enabled := False;  // pause render loop
end;

procedure TMy3DEditorFrame.AfterDeactivate;
begin
  // Release heavy GPU resources if needed
end;

Important: This interface is optional. Frames that don't implement it work as before (show/hide without notification).

IPluginSettings

Plugin provides one or more settings pages.

IPluginSettings = interface
  ['{6C3D4E5F-6A7B-4C8D-9E0F-1A2B3C4D5E6F}']
  function GetSettingsCount: Integer;
  function GetSettingsCaption(AIndex: Integer): string;
  function GetSettingsFrame(AIndex: Integer; AOwner: TComponent): TFrame;
  procedure LoadSettings;
  procedure SaveSettings;
end;
Method Purpose
GetSettingsCount Number of settings pages this plugin provides
GetSettingsCaption Label for the TreeView node at index
GetSettingsFrame Returns the settings frame at index (cache it!)
LoadSettings Called when settings UI opens -- read from disk/registry
SaveSettings Called when user clicks OK/Apply

Registration:

if Supports(Plugin, IPluginSettings) then
  TServiceRegistry.Instance.RegisterSettingsProvider(Plugin as IPluginSettings);

IPluginSettingsTree

Optional extension for hierarchical settings navigation. If a plugin implements both IPluginSettings and IPluginSettingsTree, the settings UI builds a tree instead of a flat list.

IPluginSettingsTree = interface
  ['{8E4F5A6B-7C8D-4E9F-0A1B-2C3D4E5F6A7B}']
  function GetSettingsParent(AIndex: Integer): Integer;  // -1 = Root
end;

Example with 3 pages (General, Appearance, Fonts):

function TMyPlugin.GetSettingsParent(AIndex: Integer): Integer;
begin
  case AIndex of
    0: Result := -1;  // "General" → Root
    1: Result := -1;  // "Appearance" → Root
    2: Result :=  1;  // "Fonts" → child of "Appearance"
  else
    Result := -1;
  end;
end;

IMenuProvider

Plugin defines menu entries; the host builds the actual UI from them.

IMenuProvider = interface
  ['{2F3A4B5C-6D7E-4F8A-9B0C-1D2E3F4A5B6C}']
  function GetMenuItems: TArray<TMenuItemDef>;
  procedure ExecuteMenuItem(const AID: string);
end;
Method Purpose
GetMenuItems Returns array of menu definitions
ExecuteMenuItem Called by host when user clicks a menu item (matched by ID)

Registration:

TServiceRegistry.Instance.RegisterMenuProvider(Plugin as IMenuProvider);

INavigationHost

Implemented by the host app (not plugins). Plugins retrieve it via GetService to trigger frame switches.

INavigationHost = interface
  ['{5D4E5F6A-7B8C-4D9E-0F1A-2B3C4D5E6F7A}']
  procedure ShowFrame(AFrame: TFrame);
  procedure NavigateBack;
  procedure ShowSettings;
end;

Usage from a plugin:

var
  Svc: IInterface;
  Nav: INavigationHost;
begin
  if TServiceRegistry.Instance.GetService(INavigationHost, Svc) then
    if Supports(Svc, INavigationHost, Nav) then
      Nav.ShowFrame(MyFrame);
end;

The helper TPluginMainForm implements INavigationHost and registers itself automatically.

ILogger

Logging interface with 4 severity levels.

ILogger = interface
  ['{3A4B5C6D-7E8F-4A9B-0C1D-2E3F4A5B6C7D}']
  procedure Info(const AMessage: string);
  procedure Warn(const AMessage: string);
  procedure Error(const AMessage: string); overload;
  procedure Error(const AMessage: string; AException: Exception); overload;
end;

Usage from a plugin:

var
  Svc: IInterface;
  Logger: ILogger;
begin
  if TServiceRegistry.Instance.GetService(ILogger, Svc) then
  begin
    Logger := Svc as ILogger;
    Logger.Info('Plugin initialized');
  end;
end;

The framework provides TFileLogger as default implementation. You can register your own ILogger implementation instead.

IEventBroker

Publish/Subscribe event system for decoupled plugin-to-plugin communication.

TEventCallback = procedure(const ATopic: string; const AData: TObject) of object;

IEventBroker = interface
  ['{4B5C6D7E-8F9A-4B0C-1D2E-3F4A5B6C7D8E}']
  procedure Subscribe(const ATopic: string; const ACallback: TEventCallback);
  procedure Unsubscribe(const ATopic: string; const ACallback: TEventCallback);
  procedure Publish(const ATopic: string; const AData: TObject = nil);
end;
Method Purpose
Subscribe Register callback for a topic
Unsubscribe Remove callback for a topic
Publish Fire all callbacks for a topic. AData is optional sender/payload.

Important details:

  • Callbacks are procedure of object (BPL-safe), not reference to
  • Callbacks are invoked outside the lock to prevent deadlocks
  • Exception isolation: one crashing callback does not block others
  • Thread-safe: Subscribe/Unsubscribe/Publish use TMonitor locking

Example -- Publisher:

procedure TMyPlugin.DoSomething;
var
  Svc: IInterface;
begin
  if TServiceRegistry.Instance.GetService(IEventBroker, Svc) then
    (Svc as IEventBroker).Publish('data.changed', Self);
end;

Example -- Subscriber:

procedure TMyPlugin.Initialize;
var
  Svc: IInterface;
begin
  if TServiceRegistry.Instance.GetService(IEventBroker, Svc) then
    (Svc as IEventBroker).Subscribe('data.changed', HandleDataChanged);
end;

procedure TMyPlugin.HandleDataChanged(const ATopic: string; const AData: TObject);
begin
  // React to event. AData may be the sender or nil.
end;

See EventBrokerDemo for a complete 4-plugin example.

IShortcutRegistry

Context-aware keyboard shortcut management.

TShortcutExecuteEvent = procedure of object;
TShortcutCanExecuteFunc = function: Boolean of object;

IShortcutRegistry = interface
  ['{5C6D7E8F-9A0B-4C1D-2E3F-4A5B6C7D8E9F}']
  procedure RegisterAction(const APluginName, AActionName, ADescription: string;
    ADefaultShortcut: TShortCut; AOnExecute: TShortcutExecuteEvent;
    AOnCanExecute: TShortcutCanExecuteFunc);
  procedure UnregisterPlugin(const APluginName: string);
  function GetAllActions: TArray<TShortcutAction>;
  procedure UpdateShortcut(const APluginName, AActionName: string; ANewShortcut: TShortCut);
  function TryExecute(AShortcut: TShortCut): Boolean;
end;
Method Purpose
RegisterAction Plugin registers a named action with default shortcut
UnregisterPlugin Remove all actions for a plugin (called during Finalize)
GetAllActions List all registered actions (for settings UI)
UpdateShortcut Remap a shortcut at runtime
TryExecute Called by host on KeyUp. Finds matching action, checks CanExecute, runs OnExecute. Returns True if handled.

Context-awareness: Multiple plugins can register the same shortcut. TryExecute checks OnCanExecute for each match and runs the first one that returns True. This means Ctrl+S can save a document in an editor plugin or save settings in a settings plugin, depending on which is active.

Host integration:

procedure TMainForm.FormKeyUp(Sender: TObject; var Key: Word; ...);
var
  Svc: IInterface;
begin
  if TServiceRegistry.Instance.GetService(IShortcutRegistry, Svc) then
    if (Svc as IShortcutRegistry).TryExecute(ShortCut(Key, Shift)) then
      Key := 0;
end;

See ShortcutDemo for a complete example.


Records

TMenuItemDef

Describes a single menu entry. Used by IMenuProvider.GetMenuItems.

TMenuItemDef = record
  ID: string;         // Unique ID (e.g. 'myPlugin.doThing')
  Caption: string;    // Display text
  ParentID: string;   // '' = top-level, otherwise ID of parent entry
  Order: Integer;     // Sort order within the same level
  constructor Create(const AID, ACaption, AParentID: string; AOrder: Integer);
end;

Example:

function TMyPlugin.GetMenuItems: TArray<TMenuItemDef>;
begin
  Result := [
    TMenuItemDef.Create('myPlugin', 'My Plugin', '', 100),           // Top-level menu
    TMenuItemDef.Create('myPlugin.action1', 'Action 1', 'myPlugin', 1),  // Sub-item
    TMenuItemDef.Create('myPlugin.action2', 'Action 2', 'myPlugin', 2)   // Sub-item
  ];
end;

TShortcutAction

Runtime state of a registered shortcut action. Returned by IShortcutRegistry.GetAllActions.

TShortcutAction = record
  PluginName: string;
  ActionName: string;
  Description: string;
  DefaultShortcut: TShortCut;
  CurrentShortcut: TShortCut;
  OnExecute: TShortcutExecuteEvent;
  OnCanExecute: TShortcutCanExecuteFunc;
end;

TPackageInfo

Metadata for a loaded plugin. Purely informational (splash screen, about dialog).

TPackageInfo = record
  Name: string;         // Internal name (e.g. 'PluginEditor')
  DisplayName: string;  // Human-readable name (e.g. 'Text Editor')
  Version: string;      // Semantic version (e.g. '1.0.0')
  Description: string;  // Short description
  constructor Create(const AName, ADisplayName, AVersion, ADescription: string);
end;

Callback Types

// Event broker callback -- must be 'of object' (BPL-safe)
TEventCallback = procedure(const ATopic: string; const AData: TObject) of object;

// Loader progress -- bound to splash screen
TLoadProgressEvent = procedure(const APluginName: string;
  ACurrent, ATotal: Integer) of object;

// Pre-load validation hook -- return False to reject a BPL
TValidateModuleEvent = function(const AFilePath: string): Boolean of object;

// Called when a BPL is skipped (not found, validation failed, load error)
TModuleSkippedEvent = procedure(const APluginName, AReason: string) of object;

// Shortcut callbacks
TShortcutExecuteEvent = procedure of object;
TShortcutCanExecuteFunc = function: Boolean of object;

Why of object instead of reference to? Anonymous method references are not BPL-safe. When a BPL is unloaded, the captured closure memory becomes invalid. Method-of-object pointers are tied to the object's VMT, which is cleaned up properly during shutdown.


Core Classes

TServiceRegistry

Thread-safe singleton that holds all plugin registrations and service instances. Defined in Plugin.ServiceRegistry.pas.

TServiceRegistry = class
  // Singleton
  class function Instance: TServiceRegistry;
  class procedure ReleaseInstance;

  // Release all references (MUST be called before UnloadAll)
  procedure ClearAll;

  // Plugin management
  procedure RegisterPlugin(const APlugin: IPlugin);
  function GetPlugins: TArray<IPlugin>;

  // Generic service registry (ILogger, IEventBroker, INavigationHost, custom...)
  procedure RegisterService(const AIID: TGUID; const AService: IInterface);
  function GetService(const AIID: TGUID; out AService: IInterface): Boolean;

  // Menu providers
  procedure RegisterMenuProvider(const AProvider: IMenuProvider);
  function GetMenuProviders: TArray<IMenuProvider>;

  // Settings providers
  procedure RegisterSettingsProvider(const AProvider: IPluginSettings);
  function GetSettingsProviders: TArray<IPluginSettings>;

  // Package metadata
  procedure RegisterPackageInfo(const AInfo: TPackageInfo);
  function GetPackageInfos: TArray<TPackageInfo>;
end;

Thread safety: All methods use TMonitor locking internally.

Shared singleton: Because the host links FMXPluginFramework as a runtime package, all BPLs share the same TServiceRegistry.FInstance. Without runtime-package linking, the EXE gets its own copy -- plugins register in one instance, host reads from another.

TBPLLoader

Reads plugins.json and loads BPL files. Defined in Plugin.Loader.pas.

TBPLLoader = class
  constructor Create; overload;                              // exe dir, plugins.json
  constructor Create(const ABinPath: string); overload;      // custom dir, plugins.json
  constructor Create(const ABinPath, ADefaultFileName: string); overload; // custom dir + filename

  procedure LoadAll; overload;                               // Loads BinPath/DefaultFileName
  procedure LoadAll(const APluginsJsonPath: string); overload; // Loads from file
  procedure LoadFromString(const AJsonContent: string);      // Loads from JSON string
  procedure UnloadAll;                                       // Unloads in reverse order

  property OnProgress: TLoadProgressEvent;          // Splash progress
  property OnValidateModule: TValidateModuleEvent;  // Pre-load validation
  property OnModuleSkipped: TModuleSkippedEvent;    // Skip notification
end;

Platform-specific filenames are built automatically:

  • Windows: Name.bpl
  • macOS: bplName.dylib
  • Linux: bplName.so

Custom plugin directory: By default, the loader looks for BPLs next to the EXE. Pass a custom path to load from a subdirectory:

// Plugins in a separate folder, Delphi runtime packages next to the EXE
Loader := TBPLLoader.Create(TPath.Combine(ExtractFilePath(ParamStr(0)), 'Plugins'));
Loader.LoadAll;  // reads Plugins/plugins.json, loads Plugins/*.bpl

You can also separate the JSON location from the BPL path:

Loader := TBPLLoader.Create('C:\App\Modules');
Loader.LoadAll('C:\App\Config\plugins.json');  // JSON from Config, BPLs from Modules

Custom default filename: Override the default plugins.json filename via the 2-parameter constructor:

Loader := TBPLLoader.Create(ModulesPath, 'modules.json');
Loader.LoadAll;  // reads ModulesPath/modules.json instead of plugins.json

Load from string: Skip the file entirely and pass JSON content directly (e.g., from an API response):

Loader := TBPLLoader.Create(ModulesPath);
Loader.LoadFromString(ApiResponseJson);  // no file needed

Per-plugin path override: Each entry in the JSON can specify a path relative to BinPath. If omitted, BPLs are loaded from BinPath directly (backwards-compatible):

{
  "plugins": [
    { "name": "Core", "file": "CorePlugin", "required": true },
    { "name": "Dashboard", "file": "DashboardPlugin", "required": false, "path": "Dashboard/1.0.0" }
  ]
}

With BinPath = 'C:\App\Modules':

  • CorePlugin loads from C:\App\Modules\CorePlugin.bpl
  • DashboardPlugin loads from C:\App\Modules\Dashboard\1.0.0\DashboardPlugin.bpl

Required vs optional: If required: true in plugins.json and loading fails, LoadAll raises an exception. If required: false, the plugin is skipped and OnModuleSkipped is called.

OnProgress

Fired after each plugin load attempt (success or skip). Signature matches TPluginSplash.UpdateProgress, so you can bind it directly:

Loader := TBPLLoader.Create(BinPath);
Loader.OnProgress := Splash.UpdateProgress;
Loader.LoadAll;

OnValidateModule

Called before LoadPackage for each BPL. Return False to reject the module. If the plugin is required, rejection raises an exception. If optional, it is skipped and OnModuleSkipped fires.

type
  TMyApp = class
    function ValidateModule(const AFilePath: string): Boolean;
  end;

function TMyApp.ValidateModule(const AFilePath: string): Boolean;
begin
  // Authenticode signature check
  Result := TSecurityValidator.VerifyAuthenticode(AFilePath);
end;

// Setup:
Loader.OnValidateModule := MyApp.ValidateModule;

Use cases: Authenticode signatures (SignedPlugins), SHA256 hash validation (HashValidation), or any custom check.

OnModuleSkipped

Called when a BPL is skipped for any reason: file not found, validation failed, or load error. Only fires for optional plugins (required: false). Required plugins raise exceptions instead.

procedure TMyApp.HandleSkipped(const APluginName, AReason: string);
begin
  Logger.Warn(Format('Plugin "%s" skipped: %s', [APluginName, AReason]));
end;

Loader.OnModuleSkipped := MyApp.HandleSkipped;

TFileLogger

Default ILogger implementation. Defined in Plugin.Logger.pas.

TFileLogger = class(TInterfacedObject, ILogger)
  constructor Create; overload;                     // Logs to EXE-dir/Logs/
  constructor Create(const ALogDir: string); overload;
  // ILogger
  procedure Info(const AMessage: string);
  procedure Warn(const AMessage: string);
  procedure Error(const AMessage: string); overload;
  procedure Error(const AMessage: string; AException: Exception); overload;
end;
  • One file per day: YYYY-MM-DD.log
  • Format: YYYY-MM-DD HH:NN:SS [LEVEL] Message
  • Auto-rotation: files older than 7 days are deleted on startup
  • Thread-safe via TMonitor

Custom log directory:

// Default: logs next to EXE
TFileLogger.Create;                          // → EXE-dir/Logs/

// Custom path
TFileLogger.Create('C:\App\Logs');           // → C:\App\Logs/
TFileLogger.Create(TPath.Combine(AppData, 'MyApp\Logs'));  // → AppData

TEventBroker

Default IEventBroker implementation. Defined in Plugin.EventBroker.pas.

TEventBroker = class(TInterfacedObject, IEventBroker)
  procedure Subscribe(const ATopic: string; const ACallback: TEventCallback);
  procedure Unsubscribe(const ATopic: string; const ACallback: TEventCallback);
  procedure Publish(const ATopic: string; const AData: TObject = nil);
end;
  • Callbacks invoked outside lock (deadlock prevention)
  • Exception isolation per callback (one crash does not block others)
  • Errors are logged via ILogger if registered

TShortcutRegistry (class)

Default IShortcutRegistry implementation. Defined in Plugin.ShortcutRegistry.pas.

TShortcutRegistry = class(TInterfacedObject, IShortcutRegistry)
  procedure RegisterAction(...);
  procedure UnregisterPlugin(const APluginName: string);
  function GetAllActions: TArray<TShortcutAction>;
  procedure UpdateShortcut(const APluginName, AActionName: string; ANewShortcut: TShortCut);
  function TryExecute(AShortcut: TShortCut): Boolean;
end;
  • Candidates collected under lock, execution outside lock
  • TryExecute returns True on first match where CanExecute returns True

Helpers

All helpers are in Helpers/ and are not part of the framework package (.dpk). Host apps include them via uses in their .dpr.

TPluginBootstrap

One-call app startup + shutdown. Defined in Plugin.Bootstrap.pas.

TPluginBootstrap = class
  class procedure Run(const AConfig: TBootstrapConfig;
    AMainFormClass: TComponentClass); static;
  class procedure Shutdown; static;
end;

Run does everything: splash, logger + event broker registration, BPL loading, plugin initialization, MainForm creation, Application.Run (blocks). Call Shutdown after Run returns.

begin
  Application.Initialize;
  TPluginBootstrap.Run(
    TBootstrapConfig.Create('MyApp', ExtractFilePath(ParamStr(0))),
    TPluginMainForm);
  TPluginBootstrap.Shutdown;
end.

TBootstrapConfig

Configuration record for TPluginBootstrap.Run.

TBootstrapConfig = record
  AppName: string;        // Logger path + splash title
  AppVersion: string;     // Splash display (default: '1.0.0')
  Copyright: string;      // Splash footer (default: '')
  BinPath: string;        // Path to BPLs and plugins.json
  PluginsJson: string;    // Filename (default: 'plugins.json')
  constructor Create(const AAppName, ABinPath: string;
    const APluginsJson: string = 'plugins.json');
end;

BinPath determines where the loader looks for BPLs and plugins.json. Default is the EXE directory, but you can point it to a subdirectory:

// Plugins in a subfolder
TBootstrapConfig.Create('MyApp', TPath.Combine(ExtractFilePath(ParamStr(0)), 'Plugins'));

// Logger goes to BinPath/Logs/ by default

TPluginMainForm

Ready-made shell form implementing INavigationHost. Defined in Plugin.UI.MainForm.pas.

  • Registers itself as INavigationHost on first show
  • Builds View menu from all IFrameProvider plugins
  • Builds custom menus from all IMenuProvider plugins
  • Shows IStartupView frame if available, otherwise first IFrameProvider
  • NavigateBack uses a frame stack (not single-step) -- supports arbitrary navigation depth
  • Status bar shows plugin/frame/settings count
  • Settings menu item triggers ShowSettings (uses TPluginSettingsFrame)

Override AfterPluginsLoaded for custom initialization.

TPluginSettingsFrame

TreeView-based settings UI. Defined in Plugin.UI.Settings.pas.

  • Queries all IPluginSettings providers from the registry
  • Supports IPluginSettingsTree for hierarchical navigation
  • OK / Cancel / Apply buttons
  • ShowSettings builds the tree and loads all providers

TPluginSplash

Simple splash screen with progress bar. Defined in Plugin.UI.Splash.pas.

  • SetAppInfo(Name, Version) sets title and version label
  • UpdateProgress(PluginName, Current, Total) updates progress bar and status text
  • Compatible with TBPLLoader.OnProgress (same signature)

TPluginSplashDetail

RAD Studio-style splash with plugin list. Defined in Plugin.UI.SplashDetail.pas.

  • Shows each plugin as a line item (green = loaded, red = skipped/error)
  • SetAppInfo(Name, Version, Copyright) sets title, version, and footer
  • UpdateProgress signature matches TBPLLoader.OnProgress

TSecurityValidator

Authenticode signature validation for Windows. Defined in Plugin.Security.pas.

TSecurityValidator = class
  class function VerifyAuthenticode(const AFilePath: string): Boolean;
  class function VerifySelf: Boolean;
end;
  • Uses WinVerifyTrust API
  • Returns True on non-Windows platforms (no-op)
  • Use with TBPLLoader.OnValidateModule to validate plugin BPLs before loading
  • See SignedPlugins and SecureBootstrap

TManifestValidator

SHA256 hash validation against a manifest (plugins.json). Defined in Plugin.Manifest.pas. Cross-platform (uses System.Hash, no Windows APIs).

TManifestValidator = class
  constructor Create(const AManifestPath: string);

  // OnValidateModule-compatible callback
  function ValidateModule(const AFilePath: string): Boolean;

  // Standalone check
  function IsValid(const AFilePath: string): Boolean;

  // Compute SHA256 hash of any file
  class function ComputeHash(const AFilePath: string): string;
end;
Method Purpose
ValidateModule TBPLLoader.OnValidateModule-compatible: validates BPL against manifest
IsValid Same check, standalone call
ComputeHash Class function: returns lowercase hex SHA256 of a file

Expected JSON format: TManifestValidator reads the same plugins.json as the loader, but looks for the additional sha256 field. This keeps everything in one file without changing the loader's behavior (it ignores unknown fields).

{
  "plugins": [
    { "name": "MyPlugin", "file": "MyPlugin", "required": true, "sha256": "a1b2c3d4..." }
  ]
}

Behavior:

  • Reads sha256 fields from plugins.json on creation
  • Empty string or missing field = no check for that plugin (backwards-compatible)
  • Hash mismatch → returns False → loader skips or raises depending on required

Usage:

Manifest := TManifestValidator.Create(TPath.Combine(BinPath, 'plugins.json'));
Loader.OnValidateModule := Manifest.ValidateModule;

Generating hashes (after build):

// Delphi code
Writeln(TManifestValidator.ComputeHash('MyPlugin.bpl'));
// Windows command line
certutil -hashfile MyPlugin.bpl SHA256

See HashValidation for a working example.

TPreLoadValidator

All-in-one pre-load validation combining Authenticode signature checks and SHA256 hash verification. Defined in Plugin.PreLoader.pas. Designed for the SecureBootstrap pattern where a thin EXE validates every module (including the framework BPL) before LoadPackage is called.

Unlike TSecurityValidator and TManifestValidator (which are separate classes used via TBPLLoader.OnValidateModule), TPreLoadValidator is compiled directly into the EXE and runs before any BPL is loaded. This makes it the only way to validate the framework package itself.

TPreLoadValidator = class
  constructor Create(const AManifestPath: string);
  destructor Destroy; override;

  // BPL hash against manifest
  function VerifyHash(const AFilePath: string): Boolean;

  // Authenticode signature (Windows only)
  class function VerifyAuthenticode(const AFilePath: string): Boolean;

  // Verify the host EXE itself
  class function VerifySelf: Boolean;

  // Compute SHA256 hash of any file
  class function ComputeHash(const AFilePath: string): string;
end;
Method Purpose
Create Loads plugins.json and builds an internal dictionary of file -> sha256 entries
VerifyHash Checks a BPL's SHA256 against the manifest. Returns True if the file has no hash entry (backwards-compatible). Returns False on mismatch or missing file.
VerifyAuthenticode Class function: Windows Authenticode check via WinVerifyTrust. Returns True on non-Windows (no-op).
VerifySelf Class function: calls VerifyAuthenticode(ParamStr(0)) to verify the running EXE's signature.
ComputeHash Class function: returns lowercase hex SHA256 of a file (uses THashSHA2).

Key difference from TSecurityValidator + TManifestValidator:

TSecurityValidator / TManifestValidator TPreLoadValidator
Lives in Framework helpers (BPL-loaded) Compiled directly into EXE
Can validate framework BPL No (already loaded) Yes
Authenticode TSecurityValidator only Built-in
SHA256 manifest TManifestValidator only Built-in
Typical usage TBPLLoader.OnValidateModule SecureBootstrap thin EXE

Usage (SecureBootstrap pattern):

program SecureApp;
uses
  Plugin.PreLoader;  // compiled into EXE, no BPL dependency
var
  Validator: TPreLoadValidator;
begin
  // 1. Verify the EXE itself
  if not TPreLoadValidator.VerifySelf then
    Halt(1);

  // 2. Verify framework BPL before loading
  if not TPreLoadValidator.VerifyAuthenticode('FMXPluginFramework.bpl') then
    Halt(1);

  // 3. Create validator with manifest for hash checks
  Validator := TPreLoadValidator.Create('plugins.json');
  try
    // 4. For each plugin: check signature + hash
    if TPreLoadValidator.VerifyAuthenticode('MyPlugin.bpl')
       and Validator.VerifyHash('MyPlugin.bpl') then
      LoadPackage(LoadLibrary('MyPlugin.bpl'));
  finally
    Validator.Free;
  end;
end.

Manifest format: Same plugins.json as the loader and TManifestValidator. Hash entries are optional per plugin:

{
  "plugins": [
    { "name": "MyPlugin", "file": "MyPlugin", "required": true, "sha256": "a1b2c3d4..." },
    { "name": "OptionalPlugin", "file": "OptionalPlugin", "required": false }
  ]
}

Plugins without a sha256 field (or with an empty value) pass VerifyHash unconditionally. The file field is matched case-insensitively against the base filename (without extension).

See SecureBootstrap for a complete working example.


Custom Services

Plugins can define their own interfaces and register them via TServiceRegistry.RegisterService, enabling plugin-to-plugin communication without the host knowing.

// In a shared unit (e.g. DataStore.Interfaces.pas):
type
  IDataStore = interface
    ['{AAAABBBB-CCCC-DDDD-EEEE-FFFFFFFFFFFF}']
    procedure SetValue(const AKey, AValue: string);
    function GetValue(const AKey: string): string;
  end;

// Plugin A implements + registers:
TServiceRegistry.Instance.RegisterService(IDataStore, TMyDataStore.Create);

// Plugin B consumes:
var
  Svc: IInterface;
begin
  if TServiceRegistry.Instance.GetService(IDataStore, Svc) then
    (Svc as IDataStore).SetValue('key', 'value');
end;

GUID is mandatory! Interfaces without a GUID compile fine, but Supports() and GetService() will not work at runtime -- there is no compiler error. In the Delphi IDE, Ctrl+Shift+G generates a new GUID.

// WRONG -- compiles, but Supports() will never find the interface:
IMyService = interface
  procedure DoSomething;
end;

// CORRECT:
IMyService = interface
  ['{12345678-ABCD-EFGH-IJKL-1234567890AB}']
  procedure DoSomething;
end;

BPL dependency: Plugin B's .dpk must requires Plugin A's package (or a shared package containing the interface unit) so the interface GUID matches at runtime.

See CustomServiceDemo for a full example.


plugins.json

Configuration file read by TBPLLoader.LoadAll. The loader uses only the three fields below. Additional fields are ignored, so the JSON can be extended freely (e.g. by helpers like TManifestValidator).

{
  "plugins": [
    { "name": "MyPlugin",   "file": "MyPlugin",   "required": false },
    { "name": "CorePlugin", "file": "CorePlugin", "required": true  }
  ]
}
Field Type Description
name string Display name (progress events, error messages)
file string Base name -- loader builds platform-specific filename
required boolean true: app aborts on load failure. false: skip, app continues.

Platform filename mapping:

Platform file: "MyPlugin" becomes
Windows MyPlugin.bpl
macOS bplMyPlugin.dylib
Linux bplMyPlugin.so

Shutdown Sequence

BPL unloading is dangerous: interface references pointing into unloaded memory cause Access Violations. The correct shutdown order:

1. Plugin.Finalize     -- Plugins save data (BPLs + frames still alive)
2. MainForm.Free       -- Destroys all child frames (BPL code still loaded!)
3. Local refs := nil   -- Clear interface variables pointing to BPL objects
4. ClearAll            -- Release all registry refs (BPLs still loaded, _Release safe)
5. UnloadAll           -- Unloads BPLs (everything already cleaned up)
6. ReleaseInstance     -- Frees the empty registry singleton

TPluginBootstrap.Shutdown handles this automatically. For manual shutdown:

// 1. Finalize
for P in TServiceRegistry.Instance.GetPlugins do
  P.Finalize;
// 2. Free MainForm
Application.MainForm.Free;
// 3. Clear local refs
P := nil; Plugins := nil;
// 4. ClearAll (WHILE BPLs still loaded!)
TServiceRegistry.Instance.ClearAll;
// 5. Unload BPLs
Loader.UnloadAll;
// 6. Release singleton
TServiceRegistry.ReleaseInstance;

Why ClearAll before UnloadAll? Interface references hold pointers to VMTs inside BPL memory. If you unload the BPL first, _Release / Destroy calls jump into freed memory. ClearAll drops all references while the BPL code is still resident, so destructors execute safely.


Platform Support

FMX and Delphi runtime packages are cross-platform by design. The framework uses no platform-specific code in its core -- only the security helper (TSecurityValidator) uses Windows APIs and returns True (no-op) on other platforms.

Platform Runtime package extension Status
Windows (x86/x64) .bpl Tested, primary development target
Linux (x64) bpl<Name>.so Tested
macOS (x64/ARM) bpl<Name>.dylib Theoretically supported by FMX + BPL, not tested

The loader builds platform-specific filenames automatically from the file field in plugins.json.

Security on non-Windows platforms: TSecurityValidator uses Windows Authenticode (WinVerifyTrust) and returns True (no-op) on Linux/macOS. For cross-platform module verification, use the hash-based approach (OnValidateModule with SHA256 manifest). See HashValidation.

Security Considerations

BPL plugins are native code running in-process -- there is no sandbox or isolation. A loaded BPL has full access to the host's memory and OS APIs.

For production use:

  • Validate before loading. Use TBPLLoader.OnValidateModule to check every BPL before LoadPackage. The framework provides two helpers:
    • TSecurityValidator.VerifyAuthenticode -- Windows Authenticode signatures
    • TManifestValidator.ValidateModule -- SHA256 hash checking (cross-platform)
    • Both can be combined in a single callback
  • Same Delphi version. Framework and all plugins must be compiled with the exact same Delphi version and matching runtime package set. Mismatched BPL ABIs cause crashes at load time or random memory corruption.
  • Extend the helpers. TSecurityValidator and TManifestValidator demonstrate the patterns but are not production-hardened. Consider: signing the manifest itself, certificate thumbprint pinning, version checks against downgrades.
  • The framework BPL loads before your checks. Because the host links FMXPluginFramework via DCC_UsePackage, the OS loader loads it before your code runs. For full pre-load validation of everything including the framework, see SecureBootstrap (thin EXE without framework link).

See SignedPlugins, HashValidation, and SecureBootstrap for working examples.

Delphi-Specific Notes

  • Plugin .dproj must include GenDll=true, GenPackage=true, RuntimeOnlyPackage=true -- without these, MSBuild produces an EXE instead of a BPL
  • Host .dproj must link runtime packages: <DCC_UsePackage>rtl;fmx;FMXPluginFramework;$(DCC_UsePackage)</DCC_UsePackage>