Complete reference for all interfaces, records, types, classes, and helpers. For a quick overview see README.md. For practical examples see the Examples/ directories.
- Interfaces
- Records
- Callback Types
- Core Classes
- Helpers
- Custom Services
- plugins.json
- Shutdown Sequence
- Platform Support
- Security Considerations
- Delphi-Specific Notes
All interfaces are defined in Plugin.Interfaces.pas. Every interface has a GUID for Supports() / GetService<T> queries.
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'));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;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:
IStartupView-- if any plugin implements it, that frame is shown- First
IFrameProvider-- fallback if noIStartupViewexists
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;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).
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);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;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);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.
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.
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), notreference 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
TMonitorlocking
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.
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.
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;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;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;// 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.
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.
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/*.bplYou 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 ModulesCustom 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.jsonLoad 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 neededPer-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':
CorePluginloads fromC:\App\Modules\CorePlugin.bplDashboardPluginloads fromC:\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.
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;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.
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;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')); // → AppDataDefault 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
ILoggerif registered
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
TryExecutereturns True on first match whereCanExecutereturns True
All helpers are in Helpers/ and are not part of the framework package (.dpk). Host apps include them via uses in their .dpr.
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.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 defaultReady-made shell form implementing INavigationHost. Defined in Plugin.UI.MainForm.pas.
- Registers itself as
INavigationHoston first show - Builds View menu from all
IFrameProviderplugins - Builds custom menus from all
IMenuProviderplugins - Shows
IStartupViewframe if available, otherwise firstIFrameProvider NavigateBackuses a frame stack (not single-step) -- supports arbitrary navigation depth- Status bar shows plugin/frame/settings count
- Settings menu item triggers
ShowSettings(usesTPluginSettingsFrame)
Override AfterPluginsLoaded for custom initialization.
TreeView-based settings UI. Defined in Plugin.UI.Settings.pas.
- Queries all
IPluginSettingsproviders from the registry - Supports
IPluginSettingsTreefor hierarchical navigation - OK / Cancel / Apply buttons
ShowSettingsbuilds the tree and loads all providers
Simple splash screen with progress bar. Defined in Plugin.UI.Splash.pas.
SetAppInfo(Name, Version)sets title and version labelUpdateProgress(PluginName, Current, Total)updates progress bar and status text- Compatible with
TBPLLoader.OnProgress(same signature)
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 footerUpdateProgresssignature matchesTBPLLoader.OnProgress
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
WinVerifyTrustAPI - Returns
Trueon non-Windows platforms (no-op) - Use with
TBPLLoader.OnValidateModuleto validate plugin BPLs before loading - See SignedPlugins and SecureBootstrap
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
sha256fields fromplugins.jsonon creation - Empty string or missing field = no check for that plugin (backwards-compatible)
- Hash mismatch → returns
False→ loader skips or raises depending onrequired
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.
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.
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.
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 |
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.
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.
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.OnValidateModuleto check every BPL beforeLoadPackage. The framework provides two helpers:TSecurityValidator.VerifyAuthenticode-- Windows Authenticode signaturesTManifestValidator.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.
TSecurityValidatorandTManifestValidatordemonstrate 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
FMXPluginFrameworkviaDCC_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.
- Plugin
.dprojmust includeGenDll=true,GenPackage=true,RuntimeOnlyPackage=true-- without these, MSBuild produces an EXE instead of a BPL - Host
.dprojmust link runtime packages:<DCC_UsePackage>rtl;fmx;FMXPluginFramework;$(DCC_UsePackage)</DCC_UsePackage>