The simplest way to build a plugin host application.
BasicApp.exe
│ DCC_UsePackage = rtl;fmx;FMXPluginFramework
│
└── TPluginBootstrap.Run(Config, TPluginMainForm)
├── Show splash screen
├── Register Logger + EventBroker
├── TBPLLoader.LoadAll(plugins.json)
├── Initialize plugins
├── Application.CreateForm + Application.Run
└── Shutdown (automatic)
- 5 lines of host code:
TPluginBootstrap.Runhandles the entire lifecycle - TBootstrapConfig: App name, version, paths -- all configurable via a record
- 5 plugin types: Empty, Frame, Menu, Settings, Full (Frame+Settings) -- showcases different IPlugin implementations
- Splash screen: Automatic progress during loading
| File | Purpose |
|---|---|
BasicApp.dpr |
Host app (~15 lines) |
BasicApp.groupproj |
Build order: Framework -> 5 Plugins -> App |
Plugins/DemoEmpty/ |
Minimal plugin (just name + version) |
Plugins/DemoFrame/ |
Plugin with editor frame |
Plugins/DemoMenu/ |
Plugin with menu entries |
Plugins/DemoSettings/ |
Plugin with settings frame |
Plugins/DemoFull/ |
Plugin with frame + settings combined |
- Framework is statically linked (
DCC_UsePackage) TPluginBootstrap.Runreadsplugins.json, loads BPLs, initializes plugins- Plugins self-register in their
initializationsection - MainForm displays loaded plugins + their frames/menus
Every plugin must implement this interface. Called by the host at startup and shutdown.
IPlugin = interface
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
procedure Initialize; // Called after all BPLs are loaded
procedure Finalize; // Called before BPLs are unloaded
end;Plugin provides a TFrame that the host displays in its content area.
IFrameProvider = interface
function GetDisplayName: string;
function GetFrame(AOwner: TComponent): TFrame;
end;Important: Cache the frame instance! Creating a new frame on every call causes "component name already exists" errors.
function TMyPlugin.GetFrame(AOwner: TComponent): TFrame;
begin
if FEditorFrame = nil then
FEditorFrame := TMyFrame.Create(AOwner);
Result := FEditorFrame;
end;Plugin provides one or more settings pages for the host's settings dialog.
IPluginSettings = interface
function GetSettingsCount: Integer;
function GetSettingsCaption(AIndex: Integer): string;
function GetSettingsFrame(AIndex: Integer; AOwner: TComponent): TFrame;
procedure LoadSettings;
procedure SaveSettings;
end;Optional extension for tree-structured settings navigation.
IPluginSettingsTree = interface
function GetSettingsParent(AIndex: Integer): Integer; // -1 = Root
end;Plugin defines menu entries; the host builds the actual UI from them.
TMenuItemDef = record
ID: string; // Unique ID (e.g. 'tools.export')
Caption: string; // Display text
ParentID: string; // '' = top-level, otherwise parent ID
Order: Integer; // Sort order within level
end;
IMenuProvider = interface
function GetMenuItems: TArray<TMenuItemDef>;
procedure ExecuteMenuItem(const AID: string);
end;Implemented by the host app, registered by the host, retrieved by plugins via ServiceRegistry.
INavigationHost = interface
procedure ShowFrame(AFrame: TFrame);
procedure NavigateBack;
procedure ShowSettings;
end;Plugin usage:
var Nav: INavigationHost;
begin
Nav := TServiceRegistry.Instance.GetService<INavigationHost>;
if Assigned(Nav) then
Nav.ShowFrame(GetFrame(Application.MainForm));
end;ILogger = interface
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;The built-in TFileLogger writes one file per day with auto-rotation (7 days):
TFileLogger.Create; // Default: EXE-directory/Logs/
TFileLogger.Create('C:\ProgramData\MyApp\Logs'); // Custom pathPlugins retrieve the logger via ServiceRegistry:
var Logger: ILogger;
begin
Logger := TServiceRegistry.Instance.GetService<ILogger>;
if Assigned(Logger) then
Logger.Info('Plugin successfully initialized!');
end;TBPLLoader reads this file to discover which BPLs to load at startup.
{ "plugins": [
{ "name": "MyPlugin", "file": "MyPlugin", "required": false }
] }fileis the base name -- the loader builds the platform-specific filenamerequired: true-- App aborts if plugin fails to loadrequired: false-- Warning logged, app continues
For professional deployments where plugins live in a separate directory:
Loader := TBPLLoader.Create(TPath.Combine(Root, 'Plugins'));
Loader.LoadAll(TPath.Combine(Root, 'Config\plugins.json'));- No validation hook: Every BPL is loaded regardless of whether it's signed or not
- Framework statically linked: The OS loader loads
FMXPluginFramework.bplbefore the EXE can execute any code -- there's no point in time for pre-load checks - No protection against tampered plugins: A malicious BPL has full access
For hash-based validation, use Plugin.Manifest (from Helpers/) together with OnValidateModule -- TManifestValidator checks SHA256 hashes against a plugins.json with sha256 fields.
For signature verification, see SignedPlugins. For pre-load validation, see SecureBootstrap.