Merge pull request #932 from schoeller/plugin_on_load

feat(plugins): add BuiltinPlugin::on_load lifecycle callback
This commit is contained in:
Hakan Seven authored and GitHub committed 2026-08-29 15:58:00 +03:00
commit 0431d98531
9 files changed
+52 -17

No files matched your search

+8 -4
View File
@@ -44,11 +44,12 @@ The host and each plugin run in separate OS processes. The host re-executes itse
## Versioning and ABI stability ## 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. - `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. - 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)). - 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: The runtime enforces three gates:
@@ -72,18 +73,21 @@ sequenceDiagram
PP->>R: spawn --ocs-plugin-runner <socket> <cdylib> PP->>R: spawn --ocs-plugin-runner <socket> <cdylib>
R->>L: unsafe { load(cdylib_path) } R->>L: unsafe { load(cdylib_path) }
L-->>R: Box<dyn BuiltinPlugin> L-->>R: Box<dyn BuiltinPlugin>
alt API v4 alt API v4+
R->>PP: connect + RunnerHandshake::TokenV4 R->>PP: connect + RunnerHandshake::TokenV4
PP->>PP: verify token + V4 protocol/API gate PP->>PP: verify token + V4 protocol/API gate
else API v2/v3 else API v2/v3
R->>PP: connect + RunnerHandshake::Token R->>PP: connect + RunnerHandshake::Token
PP->>PP: verify token PP->>PP: verify token
end end
opt API v5+
R->>L: on_load(HostApi)
end
PP->>R: HostToPlugin::Request(GetManifest) PP->>R: HostToPlugin::Request(GetManifest)
R->>L: manifest() R->>L: manifest()
L-->>R: PluginManifest { api_version } L-->>R: PluginManifest { api_version }
R-->>PP: HostResponse::Manifest R-->>PP: HostResponse::Manifest
PP->>PP: verify api_version + acadrust gate PP->>PP: verify api_version + ABI metadata gates
alt accepted alt accepted
PP-->>PM: success, keep alive PP-->>PM: success, keep alive
else rejected else rejected
+9 -4
View File
@@ -9,7 +9,7 @@ cheaply. The runtime host surface is enabled by the `host` feature.
## Plugin Architecture ## Plugin Architecture
OCS is scriptable and extensible through a versioned plugin API. The API 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 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. 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 | | **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 | | **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 | | **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 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 `OCS_PLUGIN_MAX_API_VERSION` (e.g. `2` for V2-only mode, `4` to disable V5). The
current host advertises [`API_VERSION = 4`](src/manifest.rs) and supports plugins current host advertises [`API_VERSION = 5`](src/manifest.rs) and supports plugins
back to `API_VERSION_MIN_SUPPORTED = 2`. 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 ### High-level layout
```mermaid ```mermaid
@@ -263,7 +268,7 @@ static MANIFEST: PluginManifest = PluginManifest {
name: "Hello Plugin", name: "Hello Plugin",
version: "0.1.0", version: "0.1.0",
description: "A minimal example plugin.", description: "A minimal example plugin.",
api_version: ApiVersion { major: 4 }, api_version: ApiVersion::CURRENT,
ribbon_order: 100, ribbon_order: 100,
xdata_apps: &[], xdata_apps: &[],
command_prefixes: &["HELLO"], command_prefixes: &["HELLO"],
+1 -1
View File
@@ -404,7 +404,7 @@ fn generate_version_info(out_dir: &Path) {
"acadrust_version": acadrust.version.to_string(), "acadrust_version": acadrust.version.to_string(),
"acadrust_source": acadrust.source.as_ref().map(|s| s.to_string()), "acadrust_source": acadrust.source.as_ref().map(|s| s.to_string()),
"rustc_version": rustc_version, "rustc_version": rustc_version,
"api_version": 4, "api_version": 5,
"api_version_min_supported": 2, "api_version_min_supported": 2,
"build_timestamp": build_timestamp, "build_timestamp": build_timestamp,
}); });
+8
View File
@@ -298,6 +298,14 @@ pub trait BuiltinPlugin: Send + Sync {
/// the V4 runner guarantees it is only called for plugins that report /// the V4 runner guarantees it is only called for plugins that report
/// API major 4 or newer. /// API major 4 or newer.
fn on_notification(&mut self, _command_id: Option<u64>, _notification: HostNotification) {} fn on_notification(&mut self, _command_id: Option<u64>, _notification: HostNotification) {}
/// 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 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) {}
} }
/// A point-driven interactive command a plugin starts via /// A point-driven interactive command a plugin starts via
+5 -5
View File
@@ -17,8 +17,9 @@
/// `document()` / `document_mut()` to local cached copies for out-of-process /// `document()` / `document_mut()` to local cached copies for out-of-process
/// plugins and appends `document_reader` / `document_view` at the end of the /// 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 /// 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. /// a multiplexed socket while leaving the V2/V3 ABI and protocol untouched. v5
pub const API_VERSION: u32 = 4; /// 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 /// 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 /// 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; pub const API_VERSION_MIN_SUPPORTED: u32 = 2;
/// Environment variable that caps the API major accepted by the host at /// 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 /// runtime. Set to `4` to disable V5 plugins, `3` to disable V4, or `2` for
/// working; set to `2` for V2-only mode. The same mechanism will apply to /// V2-only mode.
/// future API majors.
pub const MAX_API_VERSION_ENV: &str = "OCS_PLUGIN_MAX_API_VERSION"; pub const MAX_API_VERSION_ENV: &str = "OCS_PLUGIN_MAX_API_VERSION";
#[derive(Clone, Copy, Debug, PartialEq, Eq)] #[derive(Clone, Copy, Debug, PartialEq, Eq)]
+18
View File
@@ -56,15 +56,33 @@ pub fn run(socket_name: &str, cdylib_path: &Path) -> Result<(), Box<dyn std::err
if version >= 4 { if version >= 4 {
let client = V4Client::connect_handshake(socket_name, &token)?; let client = V4Client::connect_handshake(socket_name, &token)?;
if version >= 5 {
invoke_on_load(&mut *plugin, &client, &interactive)?;
}
run_v4(&mut *plugin, &interactive, client) run_v4(&mut *plugin, &interactive, client)
} else { } else {
let client = IpcClient::connect(socket_name)?; let client = IpcClient::connect(socket_name)?;
eprintln!("[runner] connected to host"); eprintln!("[runner] connected to host");
client.send_handshake(&token)?; client.send_handshake(&token)?;
// V2/V3 plugins receive HostApi only during dispatch, so on_load is not
// meaningful there; skip it to keep the old protocol unchanged.
run_v3(&*plugin, &interactive, client) run_v3(&*plugin, &interactive, client)
} }
} }
fn invoke_on_load(
plugin: &mut dyn BuiltinPlugin,
client: &V4Client,
interactive: &InteractiveRegistry,
) -> Result<(), Box<dyn std::error::Error>> {
let mut proxy = client.plugin_host_api(0, interactive.clone());
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( fn run_v3(
plugin: &dyn BuiltinPlugin, plugin: &dyn BuiltinPlugin,
interactive: &InteractiveRegistry, interactive: &InteractiveRegistry,
+1 -1
View File
@@ -147,7 +147,7 @@ mod tests {
value["ocs_plugin_api_version"].as_str(), value["ocs_plugin_api_version"].as_str(),
Some(env!("CARGO_PKG_VERSION")) 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)); assert_eq!(value["api_version_min_supported"].as_i64(), Some(2));
// acadrust_version should have a patch component (e.g. "0.4.0"). // acadrust_version should have a patch component (e.g. "0.4.0").
+1 -1
View File
@@ -229,7 +229,7 @@ version = "0.1.0"
description = "…" description = "…"
[opencad] [opencad]
api_version = 4 api_version = 5
ribbon_order = 50 ribbon_order = 50
command_prefixes = ["EX_"] command_prefixes = ["EX_"]
xdata_apps = [] xdata_apps = []
+1 -1
View File
@@ -8,7 +8,7 @@ license = "GPL-3.0-only"
repository = "your-account/your-plugin-repo" repository = "your-account/your-plugin-repo"
[opencad] [opencad]
api_version = 4 api_version = 5
rustc_version = "__RUSTC_VERSION__" rustc_version = "__RUSTC_VERSION__"
ribbon_order = 60 ribbon_order = 60
command_prefixes = ["MP_"] command_prefixes = ["MP_"]