feat(plugin): load external cdylib plugins at runtime (phase 2)

Move BuiltinPlugin into ocs_plugin_api (host feature) so out-of-tree
crates can implement it, and add export_plugin! to emit the two C
symbols a cdylib exposes: ocs_plugin_api_version (checked first, so an
ABI-incompatible build never runs) and ocs_plugin_register -> boxed
BuiltinPlugin. The host loads every compatible package from the plugins
folder at startup via libloading (desktop only), keeps the library
resident for the session, and merges its ribbon tab + command dispatch
into the same paths as built-ins (honouring enable/disable). The Plugin
Manager shows external packages with a Loaded / incompatible status.

Approach B: the plugin hands back a boxed trait object, assuming a
matching toolchain + ocs_plugin_api version (the version symbol enforces
the latter). crates/ocs_example_plugin is the reference cdylib.

Part of the #100 extensibility epic (phase 2).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
Hakan SevenandClaude Opus 4.8 committed 2026-06-17 14:48:56 +03:00
1 parent f6219a9609
commit 29c71be101
14 files changed
+402 -36

No files matched your search

+7 -4
View File
@@ -156,13 +156,15 @@ fn status_badge<'a>(label: &str, color: Color) -> Element<'a, Message> {
.into()
}
fn external_card<'a>(p: &ExternalPlugin) -> Element<'a, Message> {
let (status, color) = if !p.api_compatible() {
fn external_card<'a>(p: &ExternalPlugin, loaded: bool) -> Element<'a, Message> {
let (status, color) = if loaded {
("Loaded", Color { r: 0.2, g: 0.5, b: 0.3, a: 1.0 })
} else if !p.api_compatible() {
("API incompatible", Color { r: 0.55, g: 0.28, b: 0.28, a: 1.0 })
} else if !p.lib_present {
("No library", Color { r: 0.5, g: 0.42, b: 0.2, a: 1.0 })
} else {
("Ready to load", Color { r: 0.2, g: 0.45, b: 0.28, a: 1.0 })
("Restart to load", Color { r: 0.5, g: 0.42, b: 0.2, a: 1.0 })
};
let header = row![
text(p.name.clone()).size(15).color(WHITE),
@@ -201,6 +203,7 @@ pub fn view_window<'a>(
plugins: &[&'static PluginManifest],
disabled: &FxHashSet<String>,
externals: &[ExternalPlugin],
loaded: &FxHashSet<String>,
) -> Element<'a, Message> {
let title = text("Installed Plugins").size(20).color(WHITE);
let subtitle = text(format!(
@@ -229,7 +232,7 @@ pub fn view_window<'a>(
.color(DIM),
);
for p in externals {
list = list.push(external_card(p));
list = list.push(external_card(p, loaded.contains(&p.id)));
}
}
let body: Element<'_, Message> = scrollable(list.width(Fill)).height(Fill).into();