1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
use polyplug_utils::BundleId;
use polyplug_abi::SupportedLanguage;
use crate::{
error::LoaderError,
loader::{bundle_source::BundleSource, manifest::ManifestData},
runtime::Runtime,
};
/// Trait implemented by all bundle loaders (native, python, lua, js, .net).
///
/// The runtime dispatches each bundle to the loader whose `loader_name()`
/// matches the `loader` field in the bundle's `manifest.toml`.
pub trait BundleLoader: Send + Sync {
/// The loader identifier this loader handles.
///
/// Must match the `loader` field in `manifest.toml` exactly (case-sensitive).
fn loader_name(&self) -> &'static str;
/// The plugin language/runtime this loader serves.
///
/// A capability claim for language-aware introspection and routing: it lets
/// callers identify a loader by language without string-matching
/// [`BundleLoader::loader_name`]. Currently informational — no runtime path
/// consumes it yet; it exists as the typed seam for future language-aware
/// dispatch. (Each loader's once-per-process external-runtime state, where it
/// has any, lives in that loader's own crate as a documented Rule 12
/// limitation — not in shared runtime state.)
fn loader_language(&self) -> SupportedLanguage;
/// Whether this loader supports hot-reload.
///
/// The runtime consults this *before* calling [`BundleLoader::reload`]: when it
/// returns `false`, the runtime fails the reload with
/// `RuntimeError::HotReloadDisabled` and never invokes `reload()`.
fn supports_hot_reload(&self) -> bool;
/// Load a bundle for the first time.
///
/// The manifest carries the bundle metadata:
/// - `manifest.file` - the plugin file (relative to the bundle directory)
/// - `manifest.id` - the bundle ID
///
/// `source` selects where the executable artifact comes from:
/// - [`BundleSource::Path`] - an on-disk bundle directory (path-based loading).
/// `manifest.path` holds the same directory and remains the resolution root.
/// - [`BundleSource::Code`] / [`BundleSource::Bytes`] - in-memory sources with no
/// bundle directory. The native loader rejects these; VM loaders reject them
/// until they gain real in-memory support.
///
/// # Errors
/// Returns `Err(LoaderError::...)` on any failure, including
/// `LoaderError::UnsupportedBundleSource { .. }` when the loader does not support
/// the given source kind.
fn load(
&self,
manifest: &ManifestData,
source: &BundleSource,
runtime: &Runtime,
) -> Result<(), LoaderError>;
/// Reload a bundle - MANDATORY for all loaders.
///
/// Called when a bundle needs to be hot-reloaded (e.g., file changed).
///
/// Implementation must:
/// 1. Load/reload the bundle code (loader-specific mechanism)
/// 2. Call init to get new interfaces
/// 3. Register new interfaces with registry (interface swap happens in registry)
/// 4. Return Ok(()) - runtime handles callback and quiescence wait
///
/// # Safety Contract
/// After return, old resources should be cleaned up:
/// - Native: drop old library (caller must not have cached raw pointers)
/// - VMs: let GC handle cleanup
///
/// The runtime gates hot-reload before calling this: it only invokes `reload()`
/// when the config has hot-reload enabled AND [`BundleLoader::supports_hot_reload`]
/// returns `true`. Loaders therefore do not re-check the config here.
///
/// # Errors
/// Returns `Err(LoaderError::...)` on any failure.
fn reload(&self, manifest: &ManifestData, runtime: &Runtime) -> Result<(), LoaderError>;
/// Reclaim a bundle's loader-owned resources after it has been invalidated.
///
/// The runtime calls this from [`Runtime::unload_bundle`] *after*
/// `RuntimeStore::invalidate_bundle` has removed the bundle from the registry
/// indices and bumped its slots' generations. By that point no *new* dispatch
/// can resolve to this bundle, so the loader only has to account for dispatches
/// already in flight.
///
/// The default implementation is a no-op, for a hypothetical loader with no
/// per-bundle resources to reclaim. Every shipped loader overrides it, and each
/// reclaim is epoch-deferred so the actual free runs only once no reader is still
/// pinned in the epoch that preceded the unload — previously resolved pointers stay
/// valid until no in-flight dispatch can reference the bundle:
/// - **Native loader:** `dlclose`s the dylib (drops the `libloading::Library`),
/// releasing the on-disk file lock.
/// - **VM loaders (Lua, JS):** drop the bundle's per-bundle VM.
/// - **Python loader:** purges the bundle's re-keyed `sys.modules` entries (CPython
/// is single-init per process and cannot be torn down).
/// - **.NET loader:** unloads the bundle's collectible `AssemblyLoadContext`.
///
/// # Errors
/// Returns `Err(LoaderError::...)` if reclamation fails.
fn unload(&self, _bundle_id: BundleId, _runtime: &Runtime) -> Result<(), LoaderError> {
Ok(())
}
}