Sign in Join

Live chat for Free Pascal, Lazarus, Delphi and every Pascal programmer.

Catalog · How the catalog works

Random project

tangram-plugin-framework

Automatically exported from code.google.com/p/tangram-plugin-framework

View on GitHub

Anyone may press Interesting, signed in or not. It helps others find this project.

Member rating

No ratings yet.

Sign in to rate this project.

README

Tangram Plugin Framework (V3)

A BPL-only plugin runtime for Delphi XE7 / Win32 VCL applications, built around mandatory plugin descriptors, a single lifecycle, owner-scoped registrations, service leases, lazy activation, and deterministic verification.

Tangram V3 replaces the legacy Tangram framework (XML registry modules, DLL adapters, TModule, SysService, framework-global Application). The new runtime loads plugins only as BPL packages, validates every plugin before any binary is loaded, starts plugins in a stable dependency order, and rolls back deterministically when startup fails. Old plugins must be rewritten; there is no compatibility adapter. The migrated Demo/Simple is the V3 reference demo; the other Demo projects, Utils, IDEWiz, and ConfigTool are not migrated and are not part of the V3 baseline.

Contents

Status and compatibility

Area V3 contract
Delphi XE7 (dcc32 file version major 21), Win32.
UI VCL.
Plugin format BPL only; DLL plugins are not supported.
Package boundary One generated Tangram_Runtime.bpl/.dcp shared by the host and every plugin.
Unloading No UnloadPackage; loaded BPL images remain until process exit.
Compatibility No source or binary compatibility with legacy Tangram plugins.

Architecture

mermaid
flowchart TD
    Host["TTangramHost (VCL facade)"] --> Runtime["TPluginRuntime"]
    Runtime --> Catalog["Descriptor Catalog"]
    Runtime --> Resolver["PluginResolver"]
    Runtime --> Containers["PluginContainer per plugin"]
    Runtime --> Services["PluginServiceRegistry"]
    Runtime --> Extensions["PluginExtensionRegistry"]
    Runtime --> Loader["IPluginBinaryLoader"]
    Loader --> Windows["TWindowsBplLoader"]
    Loader --> Fake["Fake loader (tests)"]

TTangramHost is the only object a host application needs to understand. It delegates discovery, dependency resolution, startup, lazy activation, and shutdown to one deep TPluginRuntime module. All public contracts live in framework/Units and are compiled exactly once into Tangram_Runtime; the descriptor parser, resolver, containers, registries, activation scopes, and BPL loader stay behind the runtime seam in framework/Core.

Repository layout

Path Purpose
framework/Units V3 public contracts and the TTangramHost facade.
framework/Core Runtime implementation and the Tangram_Runtime.dpk/.dproj package project.
framework/Core/Tests Unit runner, real-BPL integration runner, fixtures, and verification scripts.
samples/v3 Runnable V3 migration smoke sample: shared interface package, Startup provider plugin, Lazy consumer plugin, and a TTangramHost host.
Demo/Simple Migrated V3 reference demo: shared SimpleIntf package, a Startup BPL plugin, a Lazy BPL plugin (converted from the former DLL module), and a TTangramHost host.
framework/Core/D12 Historical non-XE7 project; not migrated and not verification evidence.
framework/Interfaces, Demo (except Demo/Simple), Utils, IDEWiz, ConfigTool Legacy artifacts kept in the repository but not migrated to V3.

Public contracts

Unit Contracts
TangramRuntimeTypes.pas TANGRAM_RUNTIME_API = 3, TPluginState, TPluginStage, TPluginLogLevel, TPluginError, TPluginStatus.
TangramPluginIntf.pas ITangramPlugin, IPluginContext, IRegistrationToken, IServiceLease.
TangramExtensionIntf.pas IExtensionContribution, IExtensionFactory, IExtensionLease.
TangramRuntimeIntf.pas Host-facing IPluginRuntime.
uTangramFramework.pas Concrete TTangramHost VCL facade.

The runtime package Tangram_Runtime.dpk requires rtl, vcl, and xmlrtl, and contains every public and implementation unit. The host and each plugin BPL must reference the same generated Tangram_Runtime.bpl/.dcp. Public interface units must never be compiled statically into an EXE or plugin BPL; doing so breaks interface identity, RTTI, and reference counting across the binary boundary.

Required plugin layout

Every plugin must ship exactly two artifacts side by side:

text
plugins/User/User.bpl
plugins/User/User.plugin.xml

A missing, unreadable, or invalid descriptor is a discovery failure. The runtime never derives plugin metadata from BPL exports or package initialization.

Descriptor contract

xml
<Plugin RuntimeApi="3" Id="com.company.user" Version="1.2.0"
  Activation="Startup" Bpl="User.bpl">
  <Requires>
    <Plugin Id="com.company.database" Version="[1.0.0,2.0.0)" />
    <Service Interface="{11111111-1111-1111-1111-111111111111}" />
  </Requires>
  <Provides>
    <Service Interface="{22222222-2222-2222-2222-222222222222}"
             ThreadSafe="False" />
  </Provides>
  <Extensions>
    <Extension Point="tangram.commands" Id="user.open"
               Factory="UserOpenCommand" />
  </Extensions>
  <Configuration>
    <Add Name="Mode" Value="Safe" />
  </Configuration>
</Plugin>
Field Rule
RuntimeApi Must be exactly "3".
Id Stable, globally unique; [A-Za-z0-9][A-Za-z0-9._-]*.
Version Strict major.minor.patch ASCII decimal form.
Activation Startup or Lazy.
Bpl Path to the plugin BPL, resolved relative to the descriptor directory.
Requires/Plugin Required plugin ID plus an optional version range.
Requires/Service Required service GUID.
Provides/Service Declared service GUID and ThreadSafe flag.
Extensions/Extension Read-only contribution metadata: point ID, contribution ID, factory key.
Configuration/Add Immutable name/value pairs; duplicate names are rejected.

Version ranges support exact versions and the bracket forms [min,max), [min,max], (min,max), (min,max]; empty bounds mean unbounded. External entity declarations (<!DOCTYPE, <!ENTITY) are rejected before XML parsing.

Binary contract

Each plugin package must requires Tangram_Runtime and export exactly one Tangram entry point:

pascal
procedure CreateTangramPlugin(out APlugin: ITangramPlugin); stdcall;

exports
  CreateTangramPlugin name 'CreateTangramPlugin';

TWindowsBplLoader calls LoadPackage once per normalized path, caches the HMODULE for the process lifetime, resolves CreateTangramPlugin with GetProcAddress, and maps failures to pgBinaryLoad, pgEntryPointResolution, or pgModuleCreation errors. The runtime never calls UnloadPackage in success, failure, destructor, or finalization paths; the OS reclaims the BPL image at process exit.

Plugin lifecycle

mermaid
stateDiagram-v2
    [*] --> Discovered
    Discovered --> Resolved
    Resolved --> Loaded
    Loaded --> Starting
    Starting --> Active
    Active --> Stopping
    Stopping --> Inactive
    Inactive --> Starting: reactivate (new instance)
    Starting --> Failed
    Loaded --> Failed
    Inactive --> Failed

A plugin implements ITangramPlugin:

pascal
ITangramPlugin = interface
  ['{A222ADFD-3153-49EA-B69E-23AD7BAFCA32}']
  procedure Start(const AContext: IPluginContext);
  procedure Stop;
end;
  • Start must succeed and its activation scope must be committed before the plugin becomes Active.
  • Reactivation from Inactive calls CreateTangramPlugin again and creates a fresh plugin instance; the old instance and context are released.
  • A failed plugin stays Failed; it is not retried without a new runtime.
  • StopAll stops active plugins in reverse dependency order, and a Stop exception never skips the remaining cleanup.

Plugin context

Each activation receives a dedicated IPluginContext that provides:

  • Plugin ID, version, and BPL path.
  • A plugin-private data directory under the runtime data root.
  • Read-only descriptor configuration lookup.
  • Logging.
  • Service registration (RegisterService) and leased acquisition (AcquireService).
  • Extension factory registration.
  • Main-thread ID and the runtime stopping flag.

The context never exposes the runtime, registries, resolver, or loader; it does not hand out TApplication, forms, controls, database connections, or other plugins' instances. After deactivation the context is invalid and any use fails with a contract error.

Services and leases

Services are keyed by GUID. Each GUID may have at most one active provider, and registration must match the descriptor's Provides declaration (including ThreadSafe).

Consumers never receive a bare interface. AcquireService returns an IServiceLease:

pascal
IServiceLease = interface
  ['{A9EE9267-055E-4275-BB9B-C90A2CA96372}']
  function QueryService(const AIID: TGUID; out AService): Boolean;
  function GetProviderId: string;
  property ProviderId: string read GetProviderId;
end;

The returned interface must not outlive the lease. Lease counts block deactivation: a provider cannot be deactivated while a consumer depends on it or while its services have active leases. Registration tokens and leases are owner-scoped: the activation scope revokes registrations in reverse order and drains leases during rollback and shutdown.

Extensions

Contribution metadata (point ID, contribution ID, contributor ID, title, parent, icon, factory key, order) is loaded during Resolve, before any Lazy BPL is loaded. The host can query GetContributions(APointId) at any time.

Creating an extension activates the contributing plugin first (main thread only), then looks up the declared factory key and returns an IExtensionLease:

pascal
IExtensionLease = interface
  ['{3ABC83B8-250B-467B-9DEA-8145DFA19CEC}']
  function GetProviderId: string;
  function GetContributionId: string;
  function QueryExtension(const AIID: TGUID; out AExtension): Boolean;
  property ProviderId: string read GetProviderId;
  property ContributionId: string read GetContributionId;
end;

The built-in extension points are tangram.commands, tangram.menus, and tangram.views. Factory registration is only allowed for descriptor-declared keys owned by the active scope.

Threading rules

  • The runtime records the creator (main) thread at construction.
  • Discover, Resolve, StartAll, StopAll, activation, deactivation, and lazy activation may only run on that thread.
  • Worker threads can only query services that are already active and declared ThreadSafe="True".
  • No plugin callback, factory, notification, or logger is ever invoked while the runtime holds an internal lock; the runtime snapshots under lock, calls out, then commits.

Errors and rollback

Failures are reported with a stage, plugin ID, descriptor and BPL paths, dependency chain, original exception class/code, rollback errors, and a fix hint:

  • pgDiscovery, pgDescriptorValidation, pgDependencyResolution, pgBinaryLoad, pgEntryPointResolution, pgModuleCreation, pgActivation, pgContractValidation, pgServiceResolution, pgExtensionInstantiation, pgDeactivation, pgShutdown.

If a required startup plugin fails, the runtime stops the failing plugin if its code was entered, drains its scope, then stops already-active plugins in reverse order and preserves the primary error with cleanup errors attached. Descriptor and dependency errors are reported before any BPL is loaded.

Host facade

pascal
program Host;

uses
  Vcl.Forms,
  uTangramFramework,
  TangramRuntimeIntf,
  PluginRuntime in '..\Core\PluginRuntime.pas',
  PluginBplLoader in '..\Core\PluginBplLoader.pas';

{$R *.res}

var
  Runtime: IPluginRuntime;
  Host: TTangramHost;
begin
  Runtime := TPluginRuntime.Create(TWindowsBplLoader.Create, 'data');
  Host := TTangramHost.Create(Runtime, 'plugins');
  try
    Host.Initialize;
    Host.Start;
    Host.Run; // Stop is guaranteed in a finally block around the VCL loop
  finally
    Host.Free;
  end;
end.

TTangramHost rejects a nil runtime or blank plugin directory, delegates Start to Discover -> Resolve -> StartAll, guarantees Stop around the VCL message loop, and exposes the runtime read-only. Login and business startup belong to the application, not the framework.

Building and verifying

Requires Delphi XE7 (dcc32 file version major 21) on PATH.

powershell
# Unit behavior suite (contracts, descriptor, resolver, services, lifecycle, extensions)
powershell -NoProfile -ExecutionPolicy Bypass -File framework/Core/Tests/Run-RuntimeV3Tests.ps1 -Suite Unit

# Real BPL integration suite (runtime package, fixture BPLs, integration EXE)
powershell -NoProfile -ExecutionPolicy Bypass -File framework/Core/Tests/Run-RuntimeV3Tests.ps1 -Suite Integration

# Authoritative completion command: source/package verifier, then Unit, then Integration
powershell -NoProfile -ExecutionPolicy Bypass -File framework/Core/Tests/Run-RuntimeV3Tests.ps1 -Suite All

The scripts build Tangram_Runtime.dpk plus the test runner into framework/Core/Tests/.test-output (and .integration-output / .integration-plugins for the integration suite), print the compiler path/version and built artifacts, and fail on the first nonzero exit code. framework/Core/Tests/Verify-RuntimeV3.ps1 checks the source and package contracts: exact package unit allowlist, absence of legacy entry points, no UnloadPackage, fixture packages requires Tangram_Runtime with exactly the CreateTangramPlugin export, RuntimeApi="3" descriptors, Win32 target, and compiler major 21.

Smoke migration sample

samples/v3 is a runnable, minimal real migration example that exercises the full V3 boundary on XE7 Win32:

  • SmokeIntf.dpk — a business interface package (ISmokeGreeter, ISmokeCommand) that requires Tangram_Runtime and is shared by the host and both plugins.
  • plugins/SmokeProvider — a Startup plugin that registers the greeter service.
  • plugins/SmokeConsumer — a Lazy plugin that leases the greeter service during Start and registers a tangram.commands extension factory.
  • host/SmokeHost.dpr — a TTangramHost application that acquires the service lease, creates the extension (lazily activating the consumer), then stops in reverse dependency order.

Build and run:

powershell
powershell -NoProfile -ExecutionPolicy Bypass -File samples/v3/Build-Smoke.ps1

The script builds Tangram_Runtime, SmokeIntf, both plugin BPLs, and SmokeHost.exe into samples/v3/.out, deploys the plugins beside their descriptors, and verifies the service lease, lazy activation, extension product, and reverse-order shutdown. Expected output ends with Smoke migration sample passed. and exit code 0.

Simple demo

Demo/Simple is the migrated V3 reference demo. It replaces the legacy TTangramApp / TModule / SysService / Reg.xml flow with the V3 runtime:

  • Interfaces/SimpleIntf.dpk — the shared business interface package (IIntf2, IIntf3) that requires Tangram_Runtime and is shared by the host and both plugins.
  • BPL_Module — a Startup plugin that provides IIntf3 and shows its own form on demand.
  • BPL_Module2 — a Lazy plugin converted from the former DLL module that provides IIntf2 and is activated on its first service acquire.
  • Host/SimpleHost.dpr — a TTangramHost application; -verify mode acquires both service leases, asserts provider identity and the lazy activation, then exits 0.

Build and verify:

powershell
powershell -NoProfile -ExecutionPolicy Bypass -File Demo/Simple/Build-Simple.ps1

The script builds Tangram_Runtime, SimpleIntf, both plugin BPLs, and SimpleHost.exe into Demo/Simple/out, deploys the plugins beside their descriptors, and runs SimpleHost.exe -verify. Expected output ends with Simple demo V3 suite passed. and exit code 0.

Migration notes

  • Old plugins must be rewritten: the legacy contracts (TTangramApp, framework-global Application, SysService, GetModuleClass, TModule.Init/final, TModuleMgr, old GUIDs, Reg.xml, legacy factories) were removed from framework/Core and framework/Units and do not exist in V3.
  • DLL plugins are not supported; convert them to BPL plugins with descriptors.
  • The runtime no longer unloads BPLs; design for deactivation instead of unloading.
  • Demo/Simple is migrated: it now uses the V3 runtime (shared SimpleIntf package, a Startup BPL plugin, a Lazy BPL plugin converted from the former DLL module, and a TTangramHost host). The other demos (Demo/AdvancedDemo and the rest) are not migrated and are not completion evidence for V3.
  • framework/Core/Tangram_Core.res remains only as an inert historical artifact.

Known limitations

  • No runtime UnloadPackage, BPL hot replacement, or side-by-side plugin versions.
  • No DLL plugins and no process/class-loader style isolation.
  • Worker threads cannot trigger lazy activation or query non-thread-safe services.
  • Plugin business objects are only as thread-safe as the plugin makes them.

Completion evidence

V3 is considered complete only when the following criteria hold; the authoritative command is -Suite All, which runs the source/package verifier, the Unit suite, and the real-BPL Integration suite, and stops on the first failure.

# Criterion Evidence
1 Core and Units expose or call no legacy compatibility entry point. Verify-RuntimeV3.ps1 legacy-path and legacy-pattern checks; TestRuntimeContracts host checks.
2 No BPL is loaded without a valid descriptor. Descriptor validation tests (MissingRuntimeApiIsRejected, RuntimeApiOtherThanThreeIsRejected, etc.) run before any loader use.
3 All dependency and declaration conflicts are found before loading any BPL. TestPluginResolver duplicate/cycle/missing-dependency/stable-order cases.
4 Plugins participate in the lifecycle only through ITangramPlugin.Start/Stop and IPluginContext. Public contract tests and lifecycle tests.
5 Services and extensions have owners and are revoked deterministically by activation scopes. TestPluginServices scope/token/registry tests.
6 Active leases and dependents block unsafe single-plugin deactivation. ActiveLeaseAndDependentRejectNormalDeactivate, ActiveDependentAloneRejectsProviderDeactivate, HostLeaseAloneRejectsProviderDeactivate.
7 Lazy activation, main-thread limits, and thread-safe service rules work as designed. LazyExtensionActivatesOnceAndLeaseBlocksDeactivation, LazyServiceAndWorkerThreadPolicies, worker rejection tests.
8 The runtime never calls UnloadPackage. Verify-RuntimeV3.ps1 loader check; RuntimeDestructionInvalidatesContextAndNeverUnloads; integration records no unload event.
9 Fake-loader behavior tests and real-BPL integration tests both pass. Unit suite (Runtime V3 tests passed.) and Integration suite (Runtime V3 real BPL integration passed.).
10 Delphi XE7 Win32 compiles and test programs exit 0. -Suite All exits 0 using dcc32 file version 21.0.17707.5020 (XE7).
11 README fully documents the V3 plugin contract and that old plugins must be rewritten. This document; enforced by Verify-RuntimeV3.ps1.
12 Demo migration status is explicit: Demo/Simple is migrated; the other demos are not reported as compatible or verified. Status and Migration notes sections; enforced by Verify-RuntimeV3.ps1.