Catalog · How the catalog works
oasis_framework
A Delphi plugin framework inspired by [Cordis](https://github.com/cordiverse/cordis).
- ★ 2 on GitHub
Anyone may press Interesting, signed in or not. It helps others find this project.
README
Oasis
A Delphi plugin framework inspired by Cordis.
Oasis brings Cordis's core design ideas to Delphi:
- Context — a shared service/event host; plugins mount on it and share a GUID-keyed service registry, so dependency injection works across plugins.
- Reversible effects — every registration (
Effect,On) has a paired disposer that runs automatically (LIFO) on teardown; a cleanup that raises does not abort the rest (failures aggregate intoEOasisDisposeError). - Fibers — each plugin runs under its own
IEffectScope("fiber") so it can unload independently; side effects registered duringApplyattach to the active fiber. - Services — strongly-typed, GUID-keyed registry with parent-chain lookup
and child-shadowing (
Ctx.Services.Register(IConfig, inst)/Ctx.Services.Get(IConfig) as IConfig). - Events —
Emit/Serial/Waterfall/Bail(first truthy result wins and stops the chain) with per-listener fault isolation and bubbling;Waterfallshort-circuits when a listener skipsNext. AsyncParallel/SerialAsync(concurrent / sequential on the OmniThreadLibrary pool) via the optionalOasis.Otlpackage. Strongly-typed events viaTOasisEvent<TPayload>(compile-time payload safety both ends). - Fiber states —
PluginState(name)exposes the Cordis lifecycle machine:fsPending(Host: dependency not yet available) →fsLoading→fsActive→fsUnloading→fsDisposed, plusfsFailedfor Apply-time failures (queryable; the failed entry is kept). - Dependency activation — a plugin declares the service GUIDs it needs
(
Inject); it activates as soon as they are resolvable. Mount order does not matter — the host rescans the pending queue on every service registration. - Loaders — pluggable
IPluginLoaderabstraction: in-process (TInProcPluginLoader) and dynamic BPL (TBplPluginLoader, phase 3) with the same contract. - Reload —
IContext.Reload(phase 4): tears down every plugin's effects + listeners and re-runs allApplycalls (hot-restart the context);Reload(name)/Unload(name)do it for a single plugin. - Dependency cascade — services registered during Apply are fiber-owned: unloading a provider unregisters its services, the Host deactivates the affected consumers (re-queued), and they re-activate when the dependency returns.
- IoC/DI —
[Inject]attribute field injection (declare the field, the host fills it beforeApplyand nils it on deactivate/reload — declaration merges withAddInject); factory lifetimesRegisterFactory(lazy singleton: dependency probes never trigger builds) /RegisterTransient; typed pull helperTOasisDI.Need<T>. A transient service injected into a field gives the plugin ONE fresh instance per activation (Apply), held until its fiber tears down — not a new instance per access. Optional mORMot2 bridge unitOasis.Mormot(MIT/MPL boundary: uses-only, zero copied code):TMormotServicesPluginmirrors a mORMot container into Oasis as per-resolve factories (the container keeps owning sic* lifetimes; unload cascades),TOasisResolverexposes the Oasis registry to mORMot-side DI. Constraint (spec §3.4/N4): a field-injected plugin instance must be mounted at most once. - Config —
TJsonConfigPluginregisters anIOasisConfigservice (disabledflag + per-plugin values);Host.TryMountskips disabled plugins (cordis.yml semantics, JSON edition). Optional env layers merge overrides on top of the base config (Create(path, 'production')); typed readersInt/Bool/Floatfall back to defaults on missing/unparseable values. - UI marshaling —
Oasis.UI(optional package):TUIInvokerPluginregisters anIUIInvokerservice withQueue/Syncclosures marshaled to the mounting (main) thread. RTL-only implementation (TThread.Queue/ Synchronize), so the same package serves VCL and FMX hosts; console hosts must pumpCheckSynchronize.
Status
| Phase | Scope | Status |
|---|---|---|
| MVP | Oasis.Core + Oasis.Hosting (in-process loader + THost) |
Done — 25/25 tests, demo runs |
| Phase 2 | Oasis.Otl — async Parallel/SerialAsync via OTL |
Done — 6/6 tests, async demo runs |
| Phase 3 | Oasis.Bpl — dynamic BPL loading |
Done — sample BPL loads, demo runs |
| Phase 4 | Context.Reload hot-reload |
Done — tests |
| Roadmap 1 | per-plugin Reload(name) / Unload(name), fiber-owned listeners |
Done — tests |
| Roadmap 2 | dependency deactivation cascade (Unregister/OnServiceRemoved) |
Done — tests |
| Roadmap 4 | JSON config plugin + Host.TryMount (disabled support) |
Done — tests |
| Hardening | Apply-failure visibility (FailedPlugins + EV_HOST_PLUGIN_FAILED); runtime packages (Oasis.Core/.Hosting/.Otl/.Bpl/.UI.dpk) + one-click build.cmd |
Done |
| Roadmap 3 | Oasis.UI — IUIInvoker (Queue/Sync) main-thread marshaling bridge (RTL-only, VCL/FMX-agnostic) |
Done — tests |
| Roadmap 4b | Config env layers (base + override) + typed readers (Int/Bool/Float) |
Done — tests |
| Parity | bail dispatch (first truthy wins); strongly-typed On<TPayload>; Fiber.State machine (PluginState) |
Done — 44/44 tests |
| IoC/DI | [Inject] field injection; factory lifetimes; mORMot2 bridge (Oasis.Mormot, optional) |
Done — 79/79 tests (+9/9 mormot runner when mormot2 present) |
| Future | — | — |
Requirements
- Delphi 10.4 Sydney or newer (built/tested on Delphi 13 Florence).
- DUnitX (ships with RAD Studio) for tests.
Layout
src/
Oasis.Core/ zero-dependency core: Types, Errors, Effects, Services,
Events, Context (IContext+IPlugin+TContext), Plugin base
+ Oasis.Core.dpk (runtime package)
Oasis.Hosting/ IPluginLoader + TInProcPluginLoader, THost, Oasis.Config
(JSON config plugin + TryMount) + Oasis.Hosting.dpk
Oasis.Otl/ (phase 2) IAsyncEventBus + TAsyncEventBus ??? Parallel/SerialAsync
via OmniThreadLibrary; inherits TEventBus + Oasis.Otl.dpk
Oasis.Bpl/ (phase 3) TBplPluginLoader + IOasisPluginFactory contract
+ Oasis.Bpl.dpk
Oasis.UI/ (roadmap 3) IUIInvoker + TUIInvokerPlugin ??? main-thread
marshaling (Queue/Sync), RTL-only + Oasis.UI.dpk
build.cmd one-click build & test script (packages -> bin\)
samples/BplPlugin/ (phase 3) a sample BPL plugin (.dpk/.pas) + shared contract
tests/ DUnitX unit tests (Oasis.Tests.dpr ??? Core/Hosting, OTL-free)
tests/otl/ OTL test runner (Oasis.Otl.Tests.dpr ??? needs OmniThreadLibrary)
demos/ConsoleDemo/ 3-plugin dependency chain + lifecycle event (OTL-free)
demos/OtlDemo/ async Parallel demo (needs OmniThreadLibrary)
demos/BplDemo/ dynamic BPL-loading demo (needs rtl.bpl on PATH; writes bpldemo_out.txt)
demos/CascadeDemo/ dependency cascade + per-plugin Reload/Unload (4 phases)
demos/ConfigDemo/ config-driven assembly: disabled skip, env layers, typed values
(run with no args = base layer; `ConfigDemo production` = env layer)
demos/MormotBridgeDemo/ mORMot2 DI bridge both directions (needs local mormot2 checkout)
demos/WaterfallDemo/ middleware pipeline: logging/auth-veto/rate-limit/handler
demos/UiMarshalDemo/ background workers -> main-thread rendering via IUIInvoker
demos/VclHostDemo/ **VCL plugin-manager GUI** - the Windows-app showcase:
plugin list w/ status, plugin-contributed tabs, mount /
unload / reload buttons, Load-BPL-from-disk, event log.
`HostApp.exe /selftest` = headless end-to-end verification
demos/VclShowroom/ **Cordis ?????????????????????** - six clickable scenario cards,
one per advantage: mount-order independence (live
PENDING???ACTIVE), failure isolation + Reload heal, two-hop
dependency cascade, config gating via TryMount, Fork scope
tree with service shadowing + event bubbling, worker-thread
???IUIInvoker marshaling; dispatch playground with call
counters for emit/bail/waterfall. Statically linked (no BPLs).
`Showroom.exe /selftest` = 32-check headless report
demos/BplSuiteDemo/ **Tangram-style BPL suite** - host EXE + three feature BPL
modules (SuiteCore services / SuiteDashboard / SuiteOrders)
coupled ONLY through the shared SuiteContract interfaces:
sub-UIs dock as host tabs via IShellHost, cross-BPL deps via
[Inject] fields, a transient service with per-resolve
instances, settings-change events, cascade unload of the
service module and in-process remount. Multi-BPL factory
protocol: unique factory class names + Mount('bpl:...;
factory=T...Factory'). `SuiteHost.exe /selftest` = 11-check
headless report
samples/VclBplPlugin/ VCL BPL plugin adding a tab to the running host app
docs/superpowers/ design spec + implementation planBuild & run
One click (builds runtime packages + runs both test suites + builds all demos):
build.cmd # expect: ALL GREEN (79/79 + 6/6 + 32/32; BPL suite 11/11;
# mormot runner 9/9 + bridge demo when mormot2 present, else SKIP)The Delphi 13 toolchain is used directly (dcc32). From the respective folder:
# tests (DUnitX console runner) — from tests/
dcc32 -B -U"<BDS>\source\DunitX" Oasis.Tests.dpr
Oasis.Tests.exe # expect: Tests Passed: 75, Failed: 0, Errored: 0
# demo — from demos/ConsoleDemo/
dcc32 -B ConsoleDemo.dpr
ConsoleDemo.exe # expect: "OasisDemo is running." / Pending: 0 / Failed: 0Phase 2 (async, needs OmniThreadLibrary) — add OTL to the search path and the
Winapi namespace:
# OTL tests — from tests/otl/ (OTL at D:\code\awesome-pascal\OmniThreadLibrary)
dcc32 -B -NSWinapi;System;System.Win;Vcl \
-U"<DUnitX>" -U"<OTL>" -U"<OTL>\src" Oasis.Otl.Tests.dpr
Oasis.Otl.Tests.exe # expect: Tests Passed: 6
# async demo — from demos/OtlDemo/
dcc32 -B -NSWinapi;System;System.Win;Vcl -U"<OTL>" -U"<OTL>\src" OasisOtlDemo.dpr
OasisOtlDemo.exe # expect: 5 listeners across 5 distinct worker threadsPhase 3 (BPL plugin, dynamic loading) — build the sample BPL, then the host
(the host must use the RTL runtime package so it shares rtl.bpl's class list
with the BPL; rtl.bpl and the Oasis .bpls must be on PATH at run time):
# runtime packages first (produces .bpl/.dcp next to each .dpk or in bin/)
dcc32 -B -NSWinapi;System;System.Win;Vcl;System.Classes \
-U"<repo>/src/Oasis.Core" -E"<repo>/bin" src/Oasis.Core/Oasis.Core.dpk
# ... likewise Oasis.Hosting.dpk and Oasis.Bpl.dpk (or just run build.cmd)
# sample BPL — from samples/BplPlugin/ (requires Oasis.Core + Oasis.Bpl)
dcc32 -B -NSWinapi;System;System.Win;Vcl;System.Classes \
-U"<repo>/src/Oasis.Core" -U"<repo>/src/Oasis.Bpl" \
-U"<repo>/src/Oasis.Hosting" -U. SamplePlugin.dpk
# -> SamplePlugin.bpl
# host — from demos/BplDemo/ (note -LUrtl)
dcc32 -B -NSWinapi;System;System.Win;Vcl;System.Classes -LUrtl BplDemo.dpr
BplDemo.exe # writes bpldemo_out.txt: "Hello from BPL, world" / 0/0VCL host demo (plugin-manager GUI) — the Windows-app showcase. Build via
build.cmd (step [+]), or by hand:
# host (uses rtl+vcl runtime packages; needs VclPluginContract on the path)
dcc32 -B -NSWinapi;System;System.Win;Vcl -LUrtl -LUvcl \
-U"<repo>/samples/VclBplPlugin" HostApp.dpr
# VCL BPL plugin (requires rtl + vcl + Oasis.Core packages)
dcc32 -B -NSWinapi;System;System.Win;Vcl \
-U"<repo>/src/Oasis.Core" -U"<repo>/src/Oasis.Bpl" -U"<repo>/bin" -U. \
VclBplPlugin.dpkRun HostApp.exe interactively (buttons: add Settings/Clock/Greet plugins,
load the BPL from disk, reload/unload a selected plugin; watch the Greet tab
disappear when Settings is unloaded = dependency cascade in the UI), or
HostApp.exe /selftest headless — writes vclhost_selftest.txt:
mounted-inproc / mounted-bpl / views:4
after-unload:pending=1,views=2 <- Settings unloaded: Greet cascade-deactivated
after-remount:pending=0,views=4 <- Settings back: Greet re-activated, tab returnsRun-time PATH needs rtl370.bpl, vcl370.bpl (RAD Studio bin) and
Oasis.Core.bpl (repo bin\) — or copy them next to HostApp.exe.
A plugin in 30 seconds
type
IGreeter = interface
['{...}']
function Greet(const AWho: string): string;
end;
TGreeterPlugin = class(TOasisPlugin)
public
constructor Create;
procedure Apply(const Ctx: IContext); override;
end;
constructor TGreeterPlugin.Create;
begin
inherited Create('greeter');
end;
procedure TGreeterPlugin.Apply(const Ctx: IContext);
begin
Ctx.Services.Register(IGreeter, TGreeterImpl.Create);
end;
// host
Host := THost.Create;
try
Host.Mount(TConsumerPlugin.Create); // injects IGreeter - stays pending until ready
Host.Mount(TGreeterPlugin.Create); // provides IGreeter -> consumer activates
Host.Start;
Host.Shutdown;
finally
Host.Free;
end;Breaking change (DI release)
IServiceRegistry gained RegisterFactory/RegisterTransient and its
Has became a pure existence check (never triggers a factory build); the
interface GUID changed — BPL plugins compiled against older Oasis.Core
must be recompiled (run build.cmd, it rebuilds everything).
Lifetime note
Interface reference cycles exist between an event bus and its owning effect
scope (and between a context and its fibers). They are broken by calling
Dispose — THost.Shutdown / TContext.Dispose / IEffectScope.Dispose all
do this. Always dispose contexts/hosts; do not rely solely on interface
refcounting to reclaim them.

