diff --git a/crates/ocs_plugin_api/ARCHITECTURE.md b/crates/ocs_plugin_api/ARCHITECTURE.md index 9db51f42a..08761e7db 100644 --- a/crates/ocs_plugin_api/ARCHITECTURE.md +++ b/crates/ocs_plugin_api/ARCHITECTURE.md @@ -44,11 +44,12 @@ The host and each plugin run in separate OS processes. The host re-executes itse ## Versioning and ABI stability -- `API_VERSION` (currently `4`) is the host's advertised major. +- `API_VERSION` (currently `5`) is the host's advertised major. - `API_VERSION_MIN_SUPPORTED` (currently `2`) is the oldest plugin major the host loads. -- `OCS_PLUGIN_MAX_API_VERSION` can cap the accepted major at runtime (e.g. `3` to disable V4). +- `OCS_PLUGIN_MAX_API_VERSION` can cap the accepted major at runtime (e.g. `4` to disable V5). - A plugin built against major `N` runs on a host whose major is `>= N` because new vtable entries and enum variants are appended at the end. - V4 introduces the **acadrust gate**: plugins targeting API v4 or later must resolve the same `acadrust` source as the host (see [`src/version_info.rs`](src/version_info.rs)). +- V5 introduces `BuiltinPlugin::on_load`, invoked once after the runner connects. The runtime enforces three gates: @@ -72,18 +73,21 @@ sequenceDiagram PP->>R: spawn --ocs-plugin-runner R->>L: unsafe { load(cdylib_path) } L-->>R: Box - alt API v4 + alt API v4+ R->>PP: connect + RunnerHandshake::TokenV4 PP->>PP: verify token + V4 protocol/API gate else API v2/v3 R->>PP: connect + RunnerHandshake::Token PP->>PP: verify token end + opt API v5+ + R->>L: on_load(HostApi) + end PP->>R: HostToPlugin::Request(GetManifest) R->>L: manifest() L-->>R: PluginManifest { api_version } R-->>PP: HostResponse::Manifest - PP->>PP: verify api_version + acadrust gate + PP->>PP: verify api_version + ABI metadata gates alt accepted PP-->>PM: success, keep alive else rejected diff --git a/crates/ocs_plugin_api/README.md b/crates/ocs_plugin_api/README.md index 25021c8ec..1e01c6438 100644 --- a/crates/ocs_plugin_api/README.md +++ b/crates/ocs_plugin_api/README.md @@ -9,7 +9,7 @@ cheaply. The runtime host surface is enabled by the `host` feature. ## Plugin Architecture OCS is scriptable and extensible through a versioned plugin API. The API -supports three major protocol generations: **V2**, **V3**, and **V4**. Each +supports four major generations: **V2**, **V3**, **V4**, and **V5**. Each generation keeps the previous ABI stable by appending new vtable entries and new enum variants at the end, so older plugins continue to load on newer hosts. @@ -18,12 +18,17 @@ enum variants at the end, so older plugins continue to load on newer hosts. | **V2** | Out-of-process runner | Synchronous commands + interactive point collection (`HostApi::start_interactive`) | Baseline | | **V3** | Out-of-process runner | Local cached `document()`/`document_mut()` + shared-memory `DocumentReader`/`document_view` for large reads | New vtable entries appended; new `PluginRequest`/`PluginResponse` variants appended | | **V4** | Out-of-process runner over a multiplexed local socket | Full-duplex notifications + asynchronous REPL `ExecuteCode` | New `HostToPluginV4`/`PluginToHostV4` frame layer; `BuiltinPlugin::on_notification` and `start_execute_code` appended to trait | +| **V5** | V4 multiplexed runner | One-shot `BuiltinPlugin::on_load` initialization | Lifecycle callback appended to `BuiltinPlugin` | The host controls which generations are accepted at runtime through -`OCS_PLUGIN_MAX_API_VERSION` (e.g. `2` for V2-only mode, `3` to disable V4). The -current host advertises [`API_VERSION = 4`](src/manifest.rs) and supports plugins +`OCS_PLUGIN_MAX_API_VERSION` (e.g. `2` for V2-only mode, `4` to disable V5). The +current host advertises [`API_VERSION = 5`](src/manifest.rs) and supports plugins back to `API_VERSION_MIN_SUPPORTED = 2`. +V5 calls `BuiltinPlugin::on_load` once after the runner connects and before the +first user command. Plugins can cache `HostApi::plugin_request_sender()` there +for worker-thread access. A panic in the callback fails plugin loading. + ### High-level layout ```mermaid @@ -263,7 +268,7 @@ static MANIFEST: PluginManifest = PluginManifest { name: "Hello Plugin", version: "0.1.0", description: "A minimal example plugin.", - api_version: ApiVersion { major: 4 }, + api_version: ApiVersion::CURRENT, ribbon_order: 100, xdata_apps: &[], command_prefixes: &["HELLO"], diff --git a/crates/ocs_plugin_api/build.rs b/crates/ocs_plugin_api/build.rs index 3a1a38e50..867318fd0 100644 --- a/crates/ocs_plugin_api/build.rs +++ b/crates/ocs_plugin_api/build.rs @@ -404,7 +404,7 @@ fn generate_version_info(out_dir: &Path) { "acadrust_version": acadrust.version.to_string(), "acadrust_source": acadrust.source.as_ref().map(|s| s.to_string()), "rustc_version": rustc_version, - "api_version": 4, + "api_version": 5, "api_version_min_supported": 2, "build_timestamp": build_timestamp, }); diff --git a/crates/ocs_plugin_api/src/host.rs b/crates/ocs_plugin_api/src/host.rs index 39ae9a04f..0d00c85e3 100644 --- a/crates/ocs_plugin_api/src/host.rs +++ b/crates/ocs_plugin_api/src/host.rs @@ -299,14 +299,12 @@ pub trait BuiltinPlugin: Send + Sync { /// API major 4 or newer. fn on_notification(&mut self, _command_id: Option, _notification: HostNotification) {} - /// Lifecycle callback invoked once after the plugin has been loaded and its - /// manifest/ribbon have been accepted by the host. The plugin receives the - /// active document tab's `HostApi` so it can set up worker threads or cache - /// `plugin_request_sender()` before any user command runs. + /// Lifecycle callback invoked once after the runner connects. The plugin + /// receives the active document tab's `HostApi` so it can set up worker + /// threads or cache `plugin_request_sender()` before any user command runs. /// - /// Added after API v4. The default implementation does nothing, so existing - /// plugins remain source-compatible. The runner only calls this for plugins - /// that report API major 4 or newer. + /// Added in API v5. The runner only calls this for plugins that report API + /// major 5 or newer. fn on_load(&mut self, _host: &mut dyn HostApi) {} } diff --git a/crates/ocs_plugin_api/src/manifest.rs b/crates/ocs_plugin_api/src/manifest.rs index 39352e8ae..c0113e126 100644 --- a/crates/ocs_plugin_api/src/manifest.rs +++ b/crates/ocs_plugin_api/src/manifest.rs @@ -17,8 +17,9 @@ /// `document()` / `document_mut()` to local cached copies for out-of-process /// plugins and appends `document_reader` / `document_view` at the end of the /// vtable so API v2 plugins keep working. v4 adds full-duplex notifications on -/// a multiplexed socket while leaving the V2/V3 ABI and protocol untouched. -pub const API_VERSION: u32 = 4; +/// a multiplexed socket while leaving the V2/V3 ABI and protocol untouched. v5 +/// adds the `BuiltinPlugin::on_load` lifecycle callback. +pub const API_VERSION: u32 = 5; /// Oldest plugin API major the current host still loads. This keeps previously /// compiled cdylibs usable as long as their vtable layout is a prefix of the @@ -26,9 +27,8 @@ pub const API_VERSION: u32 = 4; pub const API_VERSION_MIN_SUPPORTED: u32 = 2; /// Environment variable that caps the API major accepted by the host at -/// runtime. Set to `3` to disable V4 plugins while keeping V2/V3 plugins -/// working; set to `2` for V2-only mode. The same mechanism will apply to -/// future API majors. +/// runtime. Set to `4` to disable V5 plugins, `3` to disable V4, or `2` for +/// V2-only mode. pub const MAX_API_VERSION_ENV: &str = "OCS_PLUGIN_MAX_API_VERSION"; #[derive(Clone, Copy, Debug, PartialEq, Eq)] diff --git a/crates/ocs_plugin_api/src/runner.rs b/crates/ocs_plugin_api/src/runner.rs index dc90e4b34..b1d89ab0c 100644 --- a/crates/ocs_plugin_api/src/runner.rs +++ b/crates/ocs_plugin_api/src/runner.rs @@ -56,9 +56,9 @@ pub fn run(socket_name: &str, cdylib_path: &Path) -> Result<(), Box= 4 { let client = V4Client::connect_handshake(socket_name, &token)?; - // Give the plugin a chance to set up worker threads / cache the request - // sender before the host starts dispatching user commands. - invoke_on_load(&mut *plugin, &client, &interactive); + if version >= 5 { + invoke_on_load(&mut *plugin, &client, &interactive)?; + } run_v4(&mut *plugin, &interactive, client) } else { let client = IpcClient::connect(socket_name)?; @@ -70,11 +70,17 @@ pub fn run(socket_name: &str, cdylib_path: &Path) -> Result<(), Box Result<(), Box> { let mut proxy = client.plugin_host_api(0, interactive.clone()); - let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { plugin.on_load(&mut proxy) - })); + })) + .map_err(|_| std::io::Error::other("plugin on_load() panicked"))?; + Ok(()) } fn run_v3( diff --git a/crates/ocs_plugin_api/src/version_info.rs b/crates/ocs_plugin_api/src/version_info.rs index f358d17da..aa74a81ab 100644 --- a/crates/ocs_plugin_api/src/version_info.rs +++ b/crates/ocs_plugin_api/src/version_info.rs @@ -147,7 +147,7 @@ mod tests { value["ocs_plugin_api_version"].as_str(), Some(env!("CARGO_PKG_VERSION")) ); - assert_eq!(value["api_version"].as_i64(), Some(4)); + assert_eq!(value["api_version"].as_i64(), Some(5)); assert_eq!(value["api_version_min_supported"].as_i64(), Some(2)); // acadrust_version should have a patch component (e.g. "0.4.0"). diff --git a/docs/plugin-architecture.md b/docs/plugin-architecture.md index 6cf934d7d..d27612f24 100644 --- a/docs/plugin-architecture.md +++ b/docs/plugin-architecture.md @@ -229,7 +229,7 @@ version = "0.1.0" description = "…" [opencad] -api_version = 4 +api_version = 5 ribbon_order = 50 command_prefixes = ["EX_"] xdata_apps = [] diff --git a/docs/plugin-template/plugin.toml b/docs/plugin-template/plugin.toml index c594522d9..d07f0b702 100644 --- a/docs/plugin-template/plugin.toml +++ b/docs/plugin-template/plugin.toml @@ -8,7 +8,7 @@ license = "GPL-3.0-only" repository = "your-account/your-plugin-repo" [opencad] -api_version = 4 +api_version = 5 rustc_version = "__RUSTC_VERSION__" ribbon_order = 60 command_prefixes = ["MP_"]