cordis-loader
English | 简体中文
Config-file driven plugin loader for the cordis-rs plugin framework.
This crate is the assembly half of porting upstream Cordis' loader: it
connects cordis-include entry
trees to cordis fibers and re-exports everything needed on top
(cordis-include, cordis-group), so applications depend on this crate
alone.
┌─ cordis-loader ← this crate: plugin registry + fiber state machine
├─ cordis-group group plugin (nesting marker)
├─ cordis-include entry trees + config files
└─ cordis-rs core runtime (zero dependencies)
Example
use ;
use ;
use ;
#
Imports
An entry with name: import and config: { url: "…" } mounts another
config file as its subtree — same diff machinery, same id reuse:
# main.yml
entries:
- id: extra
name: import
config:
url: extra.yml
# extra.yml — entries become children of `extra`
entries:
- id: adapter
name: adapter-http
Reloads compose all involved files into one tree diff; write-back always
routes mounted entries back to the file they came from (generated ids
included). Import cycles are reported via last_error() and skipped.
Document sources
LoaderConfig::with_document composes from an in-memory document
instead of reading the entry file, and Loader::update reconciles a
fresh composition the same way — the layer-based assembly model:
compose layers offline, then mount the result. Boot then neither reads
nor writes the file (concurrent
boots on one shared draft cannot race, and the directory may stay
read-only), reloads recompose from the stored document while import
files are still read, and update persists nothing — the file stays a
pure write-back draft:
use ;
use ;
use ;
#
Dynamic library plugins
With the dynamic feature, entries can also resolve to plugins compiled
as cdylib libraries — Rust has no stable ABI, so this is gated on a
strict build fingerprint (cordis-rs version, exact rustc including commit
hash, target triple, panic strategy): a library built by anything other
than the loading process' own toolchain is rejected instead of causing
undefined behavior.
On the plugin side, a crate-type = ["cdylib"] crate depending on
cordis-loader with the dynamic feature implements Plugin and ends
with the export macro (file name decides the plugin name):
// greeter-plugin/src/lib.rs — builds libgreeter.so / libgreeter.dylib /
// greeter.dll
use ;
;
export_plugin!;
On the loading side, attach directories to the registry; names missing
from the static registry resolve from lib<name>.so (.dylib on macOS,
<name>.dll on Windows) there:
# use PluginRegistry;
let registry = new.with_dynamic_dirs;
# assert!;
Each resolve asks the library for a fresh plugin instance in a fresh
handle. Panics are contained on the plugin side — a cdylib links its
own std, so the export macro wraps every callback in a guard that turns
panics into errors and fallback values before they can cross the
boundary. Libraries are never unloaded within a process, and a replaced
library file requires a fresh process — which is exactly the HMR flow
cordis-cli drives with cordis run --plugin-dir <dir>: it watches the
directories and hot-restarts the worker (exit code 51) when a library
changes. See the dynamic module docs for the full safety model.
What the state machine does
- open — read (or create) the entry file, build the tree, start every enabled entry; group children start beneath their group fiber's context, so disposing a group cascades. A corrupt or unreadable main file fails the open instead of silently starting an empty tree.
- reload — re-read the file and reconcile (serialized against
update_configanddispose): created entries start, removed subtrees stop, moved entries restart under their new parent, entries whose plugin name / inject declaration / enabled flag changed stop and restart with their new options, and config-only changes patch in place — a patch the plugin rejects keeps the fiber on its old config and is retried by the next reload. Generated ids are persisted afterwards so the next reload matches them. - document sources —
LoaderConfig::with_documentboots from an in-memory document (the file becomes a pure write-back draft, never a composition input), andLoader::updatereconciles a fresh composition through the same machinery without write-back — the HMR primitive for layer-based composition. Reloads of a document-backed loader recompose from the stored document; only import files are re-read. - expressions —
!!jsscalars (config values and thedisabledslot) evaluate at activation through cordis-include'sprocess.*subset; a disabled expression decides whether the entry (and its subtree) starts, and an out-of-subset or failing expression fails that entry's start and is recorded inlast_error()(on the patch path the fiber keeps its current config and the next reload retries). Files and dumps keep the raw expression text. - dispose — stop every entry, stop watching files, and release the
loader's root-level effects (the status listener and the
loaderservice); a freshLoader::openon the same root works afterwards. - self-kill — a fiber that reaches
Disposedoutside loader operation was killed by its own plugin; the loader persistsdisabled: truefor that entry shortly after (deferred off the dying fiber's transition lock). Removing an entry from the file just stops it. - inject — an entry's
injectlist is merged with the plugin's own declaration (both gate startup), so services going away or coming back reconciles entries through the core machinery. The import graph must be a tree: cycles and duplicate mounts of one file are reported distinctly throughlast_error(). - update_config — the runtime entry point for changing config: updates the fiber and persists to the file.
Events and write coalescing
The loader emits loader/entry-init, loader/before-patch,
loader/after-patch, loader/partial-dispose, and
loader/config-update on the root context's event bus — every listener
gets the affected entry, config-update also the new config node. Write
debouncing coalesces rapid write-backs:
# use ;
# use Duration;
# let root = new;
# let config = new.with_write_debounce;
let loader = open?;
// loader.update_config(...) calls now merge into one disk write
// after 300ms of quiet; loader.file().flush_deferred() waits it out.
# Ok::
Feature flags
watch— hot reload: wiresLoaderFile's debounced watcher toLoader::reload. Reload errors are recorded inlast_error().dynamic— resolve entries from dynamic-library plugins (libloading): fingerprint-checked loading throughPluginRegistry::with_dynamic_dirs, theexport_plugin!macro for plugin crates, and panic containment on the plugin side.
The cordis-cli runner builds its
cordis run command on top of this crate.