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 <noreply@anthropic.com>
This commit is contained in:
parent
f6219a9609
commit
29c71be101
14 changed files with 402 additions and 36 deletions
16
crates/ocs_example_plugin/Cargo.toml
Normal file
16
crates/ocs_example_plugin/Cargo.toml
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
[package]
|
||||
name = "ocs_example_plugin"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
description = "Reference external add-on for Open CAD Studio — builds a cdylib the host loads at runtime."
|
||||
license = "GPL-3.0-only"
|
||||
|
||||
# A dynamically-loaded plugin is a C-ABI dynamic library. `cargo build -p
|
||||
# ocs_example_plugin` produces target/<profile>/libocs_example_plugin.so (or
|
||||
# .dll/.dylib); drop it beside a plugin.toml in the plugins folder.
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
# The stable contract, with the runtime `host` surface (HostApi/BuiltinPlugin).
|
||||
ocs_plugin_api = { path = "../ocs_plugin_api", features = ["host"] }
|
||||
37
crates/ocs_example_plugin/README.md
Normal file
37
crates/ocs_example_plugin/README.md
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
# ocs_example_plugin
|
||||
|
||||
Reference **dynamically-loaded** add-on for Open CAD Studio. It depends only on
|
||||
`ocs_plugin_api` (with the `host` feature) — never on the `OpenCADStudio`
|
||||
binary — so it shows the full surface an out-of-tree plugin targets: a
|
||||
`PluginManifest`, a `CadModule` ribbon tab, a `BuiltinPlugin` entry point, and
|
||||
the `export_plugin!` C-ABI export.
|
||||
|
||||
## Build & install
|
||||
|
||||
```sh
|
||||
cargo build -p ocs_example_plugin # → target/debug/libocs_example_plugin.so
|
||||
```
|
||||
|
||||
Copy the library and `plugin.toml` into a folder named after the plugin id under
|
||||
the user plugins directory:
|
||||
|
||||
```
|
||||
<config>/OpenCADStudio/plugins/opencad.example/
|
||||
plugin.toml
|
||||
libocs_example_plugin.so # .dll on Windows, .dylib on macOS
|
||||
```
|
||||
|
||||
`<config>` is `%APPDATA%` (Windows), `~/Library/Application Support` (macOS), or
|
||||
`$XDG_CONFIG_HOME` / `~/.config` (Linux).
|
||||
|
||||
Restart Open CAD Studio. The host loads the cdylib at startup (after checking
|
||||
`ocs_plugin_api_version`), adds the **Example** ribbon tab, and routes `EX_`
|
||||
commands to it. `PLUGINS` lists it under *External* as **Loaded**; run `EX_HELLO`
|
||||
to see it respond.
|
||||
|
||||
## Contract
|
||||
|
||||
- `ocs_plugin_api::export_plugin!(MyPlugin)` emits `ocs_plugin_api_version()` and
|
||||
`ocs_plugin_register()`.
|
||||
- The package must be built with the **same toolchain and `ocs_plugin_api`
|
||||
version** as the host (approach B — the version symbol enforces the latter).
|
||||
13
crates/ocs_example_plugin/plugin.toml
Normal file
13
crates/ocs_example_plugin/plugin.toml
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
[plugin]
|
||||
id = "opencad.example"
|
||||
name = "Example Plugin"
|
||||
version = "0.1.0"
|
||||
description = "Reference dynamically-loaded add-on"
|
||||
author = "Open CAD Studio contributors"
|
||||
license = "GPL-3.0-only"
|
||||
|
||||
[opencad]
|
||||
api_version = 1
|
||||
ribbon_order = 50
|
||||
command_prefixes = ["EX_"]
|
||||
xdata_apps = []
|
||||
68
crates/ocs_example_plugin/src/lib.rs
Normal file
68
crates/ocs_example_plugin/src/lib.rs
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
//! Reference external add-on, built as a `cdylib` the host loads at runtime.
|
||||
//!
|
||||
//! It depends only on `ocs_plugin_api` (with the `host` feature) — never on the
|
||||
//! `OpenCADStudio` binary — so it demonstrates the stable contract an
|
||||
//! out-of-tree plugin targets: a `PluginManifest`, a `CadModule` ribbon tab, a
|
||||
//! `BuiltinPlugin` entry point, and the `export_plugin!` C-ABI export.
|
||||
|
||||
use ocs_plugin_api::host::{BuiltinPlugin, HostApi};
|
||||
use ocs_plugin_api::manifest::{ApiVersion, PluginManifest};
|
||||
use ocs_plugin_api::ribbon::{CadModule, IconKind, ModuleEvent, RibbonGroup, RibbonItem, ToolDef};
|
||||
|
||||
static MANIFEST: PluginManifest = PluginManifest {
|
||||
id: "opencad.example",
|
||||
name: "Example Plugin",
|
||||
version: "0.1.0",
|
||||
description: "Reference dynamically-loaded add-on",
|
||||
api_version: ApiVersion::CURRENT,
|
||||
ribbon_order: 50,
|
||||
xdata_apps: &[],
|
||||
command_prefixes: &["EX_"],
|
||||
};
|
||||
|
||||
/// Ribbon tab for the example plugin.
|
||||
struct ExampleModule;
|
||||
|
||||
impl CadModule for ExampleModule {
|
||||
fn id(&self) -> &'static str {
|
||||
"example"
|
||||
}
|
||||
fn title(&self) -> &'static str {
|
||||
"Example"
|
||||
}
|
||||
fn ribbon_groups(&self) -> Vec<RibbonGroup> {
|
||||
vec![RibbonGroup {
|
||||
title: "Demo",
|
||||
tools: vec![RibbonItem::LargeTool(ToolDef {
|
||||
id: "EX_HELLO",
|
||||
label: "Hello",
|
||||
icon: IconKind::Glyph("◆"),
|
||||
event: ModuleEvent::Command("EX_HELLO".to_string()),
|
||||
})],
|
||||
}]
|
||||
}
|
||||
}
|
||||
|
||||
/// The plugin entry point handed to the host.
|
||||
struct ExamplePlugin;
|
||||
|
||||
impl BuiltinPlugin for ExamplePlugin {
|
||||
fn manifest(&self) -> &'static PluginManifest {
|
||||
&MANIFEST
|
||||
}
|
||||
fn ribbon(&self) -> Box<dyn CadModule> {
|
||||
Box::new(ExampleModule)
|
||||
}
|
||||
fn dispatch(&self, host: &mut dyn HostApi, cmd: &str) -> bool {
|
||||
match cmd {
|
||||
"EX_HELLO" => {
|
||||
host.push_info("Hello from the external example plugin (cdylib loaded).");
|
||||
true
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Emit the C-ABI symbols the host loader looks for.
|
||||
ocs_plugin_api::export_plugin!(ExamplePlugin);
|
||||
|
|
@ -16,6 +16,47 @@ use std::any::Any;
|
|||
use acadrust::xdata::ExtendedDataRecord;
|
||||
use acadrust::{CadDocument, EntityType, Handle};
|
||||
|
||||
use crate::manifest::PluginManifest;
|
||||
use crate::ribbon::CadModule;
|
||||
|
||||
/// An add-on package's entry point: its manifest, optional ribbon tab, and
|
||||
/// command dispatch. Built-in (in-tree) and dynamically-loaded (cdylib) plugins
|
||||
/// implement the same trait from this crate, so an out-of-tree add-on targets
|
||||
/// the stable contract rather than the host binary.
|
||||
pub trait BuiltinPlugin: Send + Sync {
|
||||
fn manifest(&self) -> &'static PluginManifest;
|
||||
fn ribbon(&self) -> Box<dyn CadModule>;
|
||||
fn dispatch(&self, host: &mut dyn HostApi, cmd: &str) -> bool;
|
||||
}
|
||||
|
||||
/// Export a `BuiltinPlugin` from a `cdylib` so the host can load it at runtime.
|
||||
///
|
||||
/// Emits the two C symbols the loader looks for: `ocs_plugin_api_version`
|
||||
/// (checked before anything else, so an ABI-incompatible build is rejected
|
||||
/// without running its code) and `ocs_plugin_register` (constructs the plugin
|
||||
/// and hands ownership to the host as a boxed trait object).
|
||||
///
|
||||
/// ```ignore
|
||||
/// ocs_plugin_api::export_plugin!(MyPlugin::new());
|
||||
/// ```
|
||||
#[macro_export]
|
||||
macro_rules! export_plugin {
|
||||
($ctor:expr) => {
|
||||
#[no_mangle]
|
||||
pub extern "C" fn ocs_plugin_api_version() -> u32 {
|
||||
$crate::API_VERSION
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn ocs_plugin_register(
|
||||
) -> *mut ::std::boxed::Box<dyn $crate::host::BuiltinPlugin> {
|
||||
let plugin: ::std::boxed::Box<dyn $crate::host::BuiltinPlugin> =
|
||||
::std::boxed::Box::new($ctor);
|
||||
::std::boxed::Box::into_raw(::std::boxed::Box::new(plugin))
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// The plugin-facing runtime surface for one active document tab.
|
||||
pub trait HostApi {
|
||||
/// Index of the tab this session targets.
|
||||
|
|
|
|||
Loading…
Reference in a new issue